from __future__ import annotations

import json
import os
import threading
import time
from datetime import datetime, timezone
from typing import Any

from persistence import secure_atomic_write
from utils.skill_usage import mutate_usage

_LOCK = threading.Lock()
_STATE_FILE = ".lifecycle_state.json"
_ARCHIVE_DIR = ".archive"

_DEFAULT_STALE_DAYS = 30
_DEFAULT_ARCHIVE_DAYS = 90
_DEFAULT_CHECK_INTERVAL_SECONDS = 6 * 60 * 60


def _now_utc() -> datetime:
    return datetime.now(timezone.utc)


def _skills_base() -> str:
    from core import get_workspace_path

    workspace = get_workspace_path()
    base = os.path.join(workspace, "skills")
    os.makedirs(base, exist_ok=True)
    return base


def _state_path(skills_base: str) -> str:
    return os.path.join(skills_base, _STATE_FILE)


def _read_state(skills_base: str) -> dict[str, Any]:
    try:
        with open(_state_path(skills_base), "r", encoding="utf-8") as f:
            data = json.load(f)
        return data if isinstance(data, dict) else {}
    except Exception:
        return {}


def _write_state(skills_base: str, state: dict[str, Any]) -> None:
    secure_atomic_write(_state_path(skills_base), json.dumps(state, indent=2, ensure_ascii=True))


def _parse_iso_dt(value: str | None) -> datetime | None:
    if not value or not isinstance(value, str):
        return None
    try:
        return datetime.fromisoformat(value.replace("Z", "+00:00"))
    except Exception:
        return None


def _prefs(app_prefs: dict[str, Any] | None) -> dict[str, Any]:
    p = app_prefs or {}
    return {
        "enabled": bool(p.get("skills_lifecycle_enabled", True)),
        "allow_idle_tick_mutations": bool(p.get("skills_allow_idle_tick_mutations", True)),
        "stale_after_days": max(1, int(p.get("skills_stale_after_days", _DEFAULT_STALE_DAYS))),
        "archive_after_days": max(1, int(p.get("skills_archive_after_days", _DEFAULT_ARCHIVE_DAYS))),
        "check_interval_seconds": max(
            300, int(p.get("skills_lifecycle_check_interval_seconds", _DEFAULT_CHECK_INTERVAL_SECONDS))
        ),
        "reject_pending_on_archive": bool(p.get("skills_archive_reject_pending_proposals", True)),
    }


def should_run_lifecycle(*, app_prefs: dict[str, Any] | None = None, now: datetime | None = None) -> tuple[bool, str]:
    cfg = _prefs(app_prefs)
    if not cfg["enabled"]:
        return False, "disabled"

    ts_now = (now or _now_utc()).timestamp()
    state = _read_state(_skills_base())
    last = _parse_iso_dt(str(state.get("last_run_at") or ""))
    if last and (ts_now - last.timestamp()) < float(cfg["check_interval_seconds"]):
        return False, "interval"
    return True, "ok"


def _archive_skill_dir(skills_base: str, skill_name: str) -> str:
    src = os.path.join(skills_base, skill_name)
    archive_root = os.path.join(skills_base, _ARCHIVE_DIR)
    os.makedirs(archive_root, exist_ok=True)
    dest = os.path.join(archive_root, skill_name)
    if os.path.exists(dest):
        suffix = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
        dest = os.path.join(archive_root, f"{skill_name}-{suffix}")
    os.replace(src, dest)
    return dest


def _reject_pending_for_skill(skill_name: str, reason: str) -> int:
    try:
        from evolution.pending_store import reject_pending_for_skill

        return int(reject_pending_for_skill(skill_name, reason=reason) or 0)
    except Exception:
        return 0


def apply_automatic_skill_transitions(
    *,
    app_prefs: dict[str, Any] | None = None,
    now: datetime | None = None,
    from_idle_tick: bool = False,
    max_scan: int = 200,
) -> dict[str, Any]:
    """Apply stale/archive transitions for user skills in workspace/skills.

    Keeps runtime bounded for idle tick by limiting scan size and by checking
    an interval gate in .lifecycle_state.json.
    """
    cfg = _prefs(app_prefs)
    if not cfg["enabled"]:
        return {"ok": False, "reason": "disabled"}
    if from_idle_tick and not cfg["allow_idle_tick_mutations"]:
        return {"ok": False, "reason": "idle_tick_mutations_disabled"}

    skills_base = _skills_base()
    with _LOCK:
        # cheap re-check under lock to prevent duplicate runs
        can_run, reason = should_run_lifecycle(app_prefs=app_prefs, now=now)
        if not can_run:
            return {"ok": False, "reason": reason}

        started = time.monotonic()
        now_dt = now or _now_utc()
        stale_cutoff = now_dt.timestamp() - (cfg["stale_after_days"] * 86400)
        archive_cutoff = now_dt.timestamp() - (cfg["archive_after_days"] * 86400)

        archived: list[str] = []
        stale: list[str] = []
        reactivated: list[str] = []
        rejected_pending = 0

        try:
            entries = sorted(os.listdir(skills_base))
        except OSError:
            entries = []
        names: list[str] = []
        for name in entries[: max(0, int(max_scan))]:
            if not name or name.startswith("."):
                continue
            skill_md = os.path.join(skills_base, name, "SKILL.md")
            if os.path.isfile(skill_md):
                names.append(name)

        def _mutate(data: dict[str, Any]) -> None:
            nonlocal rejected_pending
            skills = data.setdefault("skills", {})
            for name in names:
                rec = skills.get(name)
                if not isinstance(rec, dict):
                    rec = {}
                    skills[name] = rec
                rec.setdefault("use_count", 0)
                rec.setdefault("view_count", 0)
                rec.setdefault("patch_count", 0)
                created = _parse_iso_dt(str(rec.get("created_at") or ""))
                if created is None:
                    skill_md = os.path.join(skills_base, name, "SKILL.md")
                    try:
                        created = datetime.fromtimestamp(os.path.getmtime(skill_md), tz=timezone.utc)
                    except Exception:
                        created = now_dt
                    rec["created_at"] = created.isoformat()
                rec.setdefault("pinned", False)
                rec.setdefault("state", "active")

                if rec.get("pinned"):
                    continue

                last_candidates = [
                    _parse_iso_dt(str(rec.get("last_use_at") or "")),
                    _parse_iso_dt(str(rec.get("last_view_at") or "")),
                    _parse_iso_dt(str(rec.get("last_patch_at") or "")),
                    _parse_iso_dt(str(rec.get("last_activity_at") or "")),
                    created,
                ]
                anchor_dt = max((d for d in last_candidates if d is not None), default=created or now_dt)
                anchor_ts = anchor_dt.timestamp()

                current_state = str(rec.get("state") or "active")
                if anchor_ts <= archive_cutoff:
                    rec["state"] = "archived"
                    rec["archived_at"] = now_dt.isoformat()
                    rec["last_activity_at"] = now_dt.isoformat()
                    try:
                        _archive_skill_dir(skills_base, name)
                        archived.append(name)
                        if cfg["reject_pending_on_archive"]:
                            rejected_pending += _reject_pending_for_skill(
                                name,
                                reason="Auto-rejected because skill was auto-archived by lifecycle policy.",
                            )
                    except Exception:
                        # Keep record state unchanged if move failed.
                        rec["state"] = current_state
                        rec.pop("archived_at", None)
                    continue

                if anchor_ts <= stale_cutoff and current_state == "active":
                    rec["state"] = "stale"
                    stale.append(name)
                elif anchor_ts > stale_cutoff and current_state == "stale":
                    rec["state"] = "active"
                    reactivated.append(name)

        mutate_usage(skills_base, _mutate)
        rewrite_summary: dict[str, Any] | None = None
        if archived:
            try:
                from chat_storage import rewrite_scheduled_job_prompts

                rewrite_summary = rewrite_scheduled_job_prompts({name: None for name in archived}, active_only=True)
            except Exception:
                rewrite_summary = {"ok": False}
        _write_state(
            skills_base,
            {
                "last_run_at": now_dt.isoformat(),
                "last_duration_ms": int((time.monotonic() - started) * 1000),
                "last_counts": {
                    "archived": len(archived),
                    "stale": len(stale),
                    "reactivated": len(reactivated),
                    "pending_rejected": int(rejected_pending),
                    "scheduled_prompts_updated": int((rewrite_summary or {}).get("updated") or 0),
                },
            },
        )
        return {
            "ok": True,
            "archived": archived,
            "stale": stale,
            "reactivated": reactivated,
            "pending_rejected": int(rejected_pending),
            "scheduled_prompt_rewrite": rewrite_summary or {"ok": True, "updated": 0},
        }


def acquire_lifecycle_mutex(*, blocking: bool = False) -> bool:
    return _LOCK.acquire(blocking=blocking)


def release_lifecycle_mutex() -> None:
    if _LOCK.locked():
        _LOCK.release()


def list_archived_workflows(*, limit: int = 100) -> list[str]:
    skills_base = _skills_base()
    archive_root = os.path.join(skills_base, _ARCHIVE_DIR)
    if not os.path.isdir(archive_root):
        return []
    out: list[str] = []
    try:
        for name in sorted(os.listdir(archive_root)):
            if not name or name.startswith("."):
                continue
            if os.path.isdir(os.path.join(archive_root, name)):
                out.append(name)
            if len(out) >= max(1, int(limit)):
                break
    except OSError:
        return []
    return out


def get_skill_lifecycle_status() -> dict[str, Any]:
    skills_base = _skills_base()
    state = _read_state(skills_base)
    return {
        "last_run_at": str(state.get("last_run_at") or ""),
        "last_duration_ms": int(state.get("last_duration_ms") or 0),
        "last_counts": dict(state.get("last_counts") or {}),
        "archived_count": len(list_archived_workflows(limit=1000)),
    }
