"""web_runs disk lifecycle: measure, prune, operator clear (Phase 3C)."""

from __future__ import annotations

import logging
import os
import shutil
import time
from datetime import datetime, timezone
from typing import Any

_log = logging.getLogger(__name__)


def _workspace_root() -> str:
    try:
        from core.config import get_workspace_path

        return os.path.abspath(os.path.expanduser(get_workspace_path()))
    except Exception:
        return os.path.abspath(os.path.expanduser("~/.mugi/workspace"))


def _web_runs_root(workspace: str | None = None) -> str:
    return os.path.join(workspace or _workspace_root(), "web_runs")


def _retention_cfg() -> dict[str, Any]:
    try:
        from core.config import get_web_act_config

        return get_web_act_config()
    except Exception:
        return {}


def _dir_size_bytes(path: str) -> int:
    total = 0
    try:
        for root, _dirs, files in os.walk(path):
            for name in files:
                fp = os.path.join(root, name)
                try:
                    total += os.path.getsize(fp)
                except OSError:
                    pass
    except OSError:
        pass
    return total


def _scope_has_run_lock(scope_dir: str) -> bool:
    return os.path.isfile(os.path.join(scope_dir, ".run.lock"))


def _collect_protected_run_dirs(*, scan_limit: int = 500) -> set[str]:
    """Run directories referenced by open kanban tasks (best-effort)."""
    protected: set[str] = set()
    try:
        import kanban_storage as ks

        open_statuses = {"todo", "ready", "in_progress", "blocked"}
        rows = ks.list_tasks(limit=scan_limit, max_limit=scan_limit, order="desc")
        for task in rows:
            if (task.get("status") or "") not in open_statuses:
                continue
            meta = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
            for key in ("web_act_run_dir", "run_dir", "output"):
                raw = str(meta.get(key) or "").strip()
                if raw:
                    protected.add(os.path.abspath(os.path.expanduser(raw)))
            for run in ks.list_runs(str(task.get("id") or ""), limit=5):
                rmeta = run.get("metadata") if isinstance(run.get("metadata"), dict) else {}
                raw = str(rmeta.get("web_act_run_dir") or "").strip()
                if raw:
                    protected.add(os.path.abspath(os.path.expanduser(raw)))
    except Exception:
        _log.debug("protected run dir scan failed", exc_info=True)
    return protected


def measure_web_act_disk_usage(workspace: str | None = None) -> dict[str, Any]:
    root = _web_runs_root(workspace)
    if not os.path.isdir(root):
        return {
            "web_runs_disk_bytes": 0,
            "web_runs_scope_count": 0,
            "web_runs_largest_scope": "",
            "web_runs_largest_scope_bytes": 0,
        }
    scopes: list[tuple[str, int]] = []
    total = 0
    for name in os.listdir(root):
        scope_dir = os.path.join(root, name)
        if not os.path.isdir(scope_dir):
            continue
        size = _dir_size_bytes(scope_dir)
        scopes.append((name, size))
        total += size
    scopes.sort(key=lambda x: x[1], reverse=True)
    largest = scopes[0][0] if scopes else ""
    largest_bytes = scopes[0][1] if scopes else 0
    return {
        "web_runs_disk_bytes": total,
        "web_runs_scope_count": len(scopes),
        "web_runs_largest_scope": largest,
        "web_runs_largest_scope_bytes": largest_bytes,
    }


def _run_dirs_in_scope(scope_dir: str) -> list[tuple[str, float]]:
    runs_root = os.path.join(scope_dir, "runs")
    if not os.path.isdir(runs_root):
        return []
    out: list[tuple[str, float]] = []
    for name in os.listdir(runs_root):
        if not name.startswith("run_"):
            continue
        run_dir = os.path.join(runs_root, name)
        if not os.path.isdir(run_dir):
            continue
        try:
            mtime = os.path.getmtime(run_dir)
        except OSError:
            mtime = 0.0
        out.append((run_dir, mtime))
    out.sort(key=lambda x: x[1])
    return out


def prune_web_act_scopes(
    workspace: str | None = None,
    *,
    dry_run: bool = False,
) -> dict[str, Any]:
    """Delete old run_* dirs per retention policy; skip locked/protected scopes."""
    cfg = _retention_cfg()
    if not dry_run and not bool(cfg.get("retention_enabled", False)):
        return {"deleted_bytes": 0, "deleted_paths": [], "dry_run": dry_run}
    max_age_days = int(cfg.get("max_run_age_days") or 0)
    max_runs = int(cfg.get("max_runs_per_scope") or 0)
    max_scope_bytes = int(cfg.get("max_scope_bytes") or 0)
    if max_age_days <= 0 and max_runs <= 0 and max_scope_bytes <= 0:
        return {"deleted_bytes": 0, "deleted_paths": [], "dry_run": dry_run}

    root = _web_runs_root(workspace)
    if not os.path.isdir(root):
        return {"deleted_bytes": 0, "deleted_paths": [], "dry_run": dry_run}

    protected = _collect_protected_run_dirs()
    now = time.time()
    age_cutoff = now - (max_age_days * 86400) if max_age_days > 0 else 0.0
    deleted_bytes = 0
    deleted_paths: list[str] = []

    for scope_name in os.listdir(root):
        scope_dir = os.path.join(root, scope_name)
        if not os.path.isdir(scope_dir):
            continue
        if _scope_has_run_lock(scope_dir):
            continue

        run_dirs = _run_dirs_in_scope(scope_dir)
        keep_newest: set[str] = set()
        if max_runs > 0 and len(run_dirs) > max_runs:
            for run_dir, _mtime in run_dirs[-max_runs:]:
                keep_newest.add(run_dir)

        candidates: set[str] = set()
        for run_dir, mtime in run_dirs:
            if run_dir in keep_newest:
                continue
            if os.path.abspath(run_dir) in protected:
                continue
            if max_runs > 0 and len(run_dirs) > max_runs:
                candidates.add(run_dir)
            if age_cutoff and mtime < age_cutoff:
                candidates.add(run_dir)

        if max_scope_bytes > 0:
            scope_size = _dir_size_bytes(scope_dir)
            for run_dir, _mtime in run_dirs:
                if scope_size <= max_scope_bytes:
                    break
                if run_dir in keep_newest or os.path.abspath(run_dir) in protected:
                    continue
                if run_dir in candidates:
                    size = _dir_size_bytes(run_dir)
                    scope_size -= size
                    continue
                candidates.add(run_dir)
                scope_size -= _dir_size_bytes(run_dir)

        for run_dir in sorted(candidates):
            size = _dir_size_bytes(run_dir)
            if dry_run:
                deleted_bytes += size
                deleted_paths.append(run_dir)
            else:
                try:
                    shutil.rmtree(run_dir)
                    deleted_bytes += size
                    deleted_paths.append(run_dir)
                except OSError:
                    _log.debug("prune failed for %s", run_dir, exc_info=True)

    return {
        "deleted_bytes": deleted_bytes,
        "deleted_paths": deleted_paths[:50],
        "dry_run": dry_run,
        "retention_would_reclaim_bytes": deleted_bytes if dry_run else 0,
    }


def clear_web_act_scope(
    scope_key: str,
    *,
    workspace: str | None = None,
    force_terminal_kanban: bool = True,
) -> tuple[bool, str]:
    """Remove ``web_runs/{scope_key}/`` when safe (operator clear).

    For ``kanban_*`` scopes, refuses when the task is still in_progress unless
    *force_terminal_kanban* is False.
    """
    key = (scope_key or "").strip()
    if not key:
        return False, "scope key required"
    if key.startswith("kanban_"):
        task_id = key[len("kanban_") :]
        try:
            import kanban_storage as ks

            task = ks.get_task(task_id)
            if not task:
                return False, f"task {task_id} not found"
            status = str(task.get("status") or "")
            if force_terminal_kanban and status not in ks.TERMINAL_STATUSES:
                return (
                    False,
                    f"refusing clear while task status is {status} (done or cancelled required)",
                )
        except Exception as exc:
            return False, str(exc)[:200]

    from tools.web_act import sanitize_scope

    safe = sanitize_scope(key)
    scope_dir = os.path.join(_web_runs_root(workspace), safe)
    if not os.path.isdir(scope_dir):
        return True, "scope directory already absent"
    if _scope_has_run_lock(scope_dir):
        return False, "scope has an active run lock"
    try:
        shutil.rmtree(scope_dir)
        return True, f"removed {scope_dir}"
    except OSError as exc:
        return False, str(exc)[:200]
