import os
import subprocess
import time


def _command(args, timeout_seconds):
    started = time.time()
    try:
        result = subprocess.run(args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout_seconds)
        return {
            "exitCode": result.returncode,
            "stdout": result.stdout,
            "stderr": result.stderr,
            "elapsedMs": int((time.time() - started) * 1000),
        }
    except subprocess.TimeoutExpired as exc:
        return {
            "exitCode": 124,
            "stdout": exc.stdout or "" if isinstance(exc.stdout, str) else "",
            "stderr": exc.stderr or "" if isinstance(exc.stderr, str) else "",
            "elapsedMs": int((time.time() - started) * 1000),
            "timedOut": True,
        }


def _worktree_entries(main_root):
    result = _command(["git", "-C", main_root, "worktree", "list", "--porcelain"], 10)
    if result["exitCode"] != 0:
        raise RuntimeError(result["stderr"].strip() or "git worktree list failed")
    entries = []
    current = None
    for raw_line in result["stdout"].splitlines() + [""]:
        line = raw_line.strip()
        if not line:
            if current:
                entries.append(current)
                current = None
            continue
        key, _, value = line.partition(" ")
        if key == "worktree":
            current = {"path": os.path.realpath(value), "head": None, "branch": None, "bare": False}
        elif current is not None and key == "HEAD":
            current["head"] = value
        elif current is not None and key == "branch":
            current["branch"] = value
        elif current is not None and key == "bare":
            current["bare"] = True
    return entries


def _inside_roots(path, roots):
    return any(path.startswith(root.rstrip("/") + "/") for root in roots)


def _recent(path, cutoff, deadline):
    for root, dirs, files in os.walk(path):
        if time.time() > deadline:
            raise TimeoutError("scan-budget-exceeded")
        for name in dirs + files:
            try:
                if os.lstat(os.path.join(root, name)).st_mtime >= cutoff:
                    return True
            except OSError:
                return True
    try:
        return os.lstat(path).st_mtime >= cutoff
    except OSError:
        return True


def _open_or_current(path):
    resolved = os.path.realpath(path)
    prefix = resolved.rstrip("/") + "/"
    try:
        pids = [name for name in os.listdir("/proc") if name.isdigit()]
    except OSError:
        return True
    for pid in pids:
        base = os.path.join("/proc", pid)
        for name in ["cwd", "root"]:
            try:
                target = os.path.realpath(os.readlink(os.path.join(base, name)))
            except OSError:
                continue
            if target == resolved or target.startswith(prefix):
                return True
    return False


def _clean(path):
    result = _command(["git", "-C", path, "status", "--porcelain", "--untracked-files=all"], 15)
    return result["exitCode"] == 0 and not result["stdout"].strip(), result


def _absorbed(main_root, base_ref, head, timeout_seconds):
    ancestor = _command(["git", "-C", main_root, "merge-base", "--is-ancestor", head, base_ref], timeout_seconds)
    if ancestor["exitCode"] == 0:
        return True, "ancestor", 0
    cherry = _command(["git", "-C", main_root, "cherry", base_ref, head], timeout_seconds)
    if cherry["exitCode"] != 0:
        raise RuntimeError(cherry["stderr"].strip() or "git cherry failed")
    unabsorbed = [line for line in cherry["stdout"].splitlines() if line.startswith("+")]
    return len(unabsorbed) == 0, "cherry-equivalent" if not unabsorbed else "unmerged", len(unabsorbed)


def merged_worktree_plan(config, automatic=False):
    cfg = config.get("mergedWorktrees") if isinstance(config, dict) else None
    if not isinstance(cfg, dict) or cfg.get("enabled") is not True:
        return {"id": "merged-worktrees", "ok": True, "skipped": True, "candidates": [], "protected": []}
    automatic_cfg = cfg.get("automatic") if isinstance(cfg.get("automatic"), dict) else {}
    if automatic and automatic_cfg.get("enabled") is not True:
        return {"id": "merged-worktrees", "ok": True, "skipped": True, "candidates": [], "protected": []}
    main_root = os.path.realpath(str(cfg.get("mainRoot") or ""))
    roots = [os.path.realpath(item) for item in (cfg.get("workspaceRoots") or []) if isinstance(item, str) and item.startswith("/")]
    base_ref = str(cfg.get("baseRef") or "")
    min_age_hours = float(cfg.get("minAgeHours") or 0)
    scan_budget_ms = max(1, int(cfg.get("scanBudgetMs") or 1))
    timeout_seconds = max(1, int(cfg.get("cherryCheckTimeoutSeconds") or 1))
    limit_key = "maxDeletePerRun" if automatic else "manualMaxDeletePerRun"
    limit_source = automatic_cfg if automatic else cfg
    limit = max(1, int(limit_source.get(limit_key) or 1))
    if not main_root.startswith("/") or not roots or not base_ref:
        raise RuntimeError("mergedWorktrees requires absolute mainRoot/workspaceRoots and baseRef")
    deadline = time.time() + scan_budget_ms / 1000.0
    cutoff = time.time() - min_age_hours * 3600.0
    candidates = []
    protected = []
    for entry in _worktree_entries(main_root):
        path = entry["path"]
        if not _inside_roots(path, roots):
            continue
        reason = None
        detail = None
        if time.time() > deadline:
            reason = "scan-budget-exceeded"
        elif entry.get("bare") or path == main_root or _open_or_current(path):
            reason = "active-worktree"
        elif not os.path.isdir(path) or os.path.islink(path):
            reason = "missing-or-nonplain-worktree"
        else:
            try:
                if _recent(path, cutoff, deadline):
                    reason = "recent-worktree"
            except TimeoutError:
                reason = "scan-budget-exceeded"
        if reason is None:
            clean, clean_result = _clean(path)
            if not clean:
                reason = "dirty-worktree" if clean_result["exitCode"] == 0 else "worktree-status-error"
                detail = clean_result["stderr"].strip()[-400:]
        if reason is None and not entry.get("head"):
            reason = "worktree-head-missing"
        if reason is None:
            try:
                absorbed, merge_mode, unabsorbed = _absorbed(main_root, base_ref, entry["head"], timeout_seconds)
                if not absorbed:
                    reason = "unmerged-worktree"
                    detail = "unabsorbedCommitCount=%d" % unabsorbed
            except Exception as exc:
                reason = "worktree-merge-check-error"
                detail = str(exc)
        if reason is not None:
            protected.append({"path": path, "reason": reason, "detail": detail})
            continue
        candidates.append({"path": path, "head": entry["head"], "branch": entry.get("branch"), "mergeMode": merge_mode})
    return {
        "id": "merged-worktrees",
        "ok": True,
        "automatic": automatic,
        "configSource": "config/unidesk-cli.yaml#gc.remote.targets.%s.mergedWorktrees" % config.get("providerId"),
        "baseRef": base_ref,
        "candidateCount": len(candidates),
        "protectedCount": len(protected),
        "limit": limit,
        "candidates": candidates[:limit],
        "protected": protected,
    }


def run_merged_worktrees(config, automatic=False):
    plan = merged_worktree_plan(config, automatic=automatic)
    if plan.get("skipped"):
        return plan
    results = []
    for candidate in plan["candidates"]:
        refreshed = merged_worktree_plan(config, automatic=automatic)
        safe_paths = {item["path"] for item in refreshed.get("candidates") or []}
        path = candidate["path"]
        if path not in safe_paths:
            results.append({"path": path, "status": "protected-on-recheck"})
            continue
        result = _command(["git", "-C", os.path.realpath(config["mergedWorktrees"]["mainRoot"]), "worktree", "remove", path], 30)
        results.append({"path": path, "status": "removed" if result["exitCode"] == 0 else "failed", "error": result["stderr"].strip()[-400:] or None})
    return {
        **plan,
        "mutation": True,
        "attemptedCount": len(results),
        "removedCount": len([item for item in results if item["status"] == "removed"]),
        "failedCount": len([item for item in results if item["status"] == "failed"]),
        "results": results,
        "ok": all(item["status"] != "failed" for item in results),
    }
