"""Phase 2: auto-fix proposer. Runs `claude -p` in an isolated git worktree
to propose a fix for a detected WoW patch breakage, commits the diff, and
returns a structured FixResult for the verifier and detector to consume.

The bot never invokes claude with --dangerously-skip-permissions; the tool
allowlist is restricted to read/write/grep operations only.
"""
import json
import subprocess
from dataclasses import dataclass, field
from pathlib import Path

CLAUDE_MAX_TURNS = 30
CLAUDE_TIMEOUT_SECONDS = 300
CLAUDE_ALLOWED_TOOLS = "Read,Edit,Write,Glob,Grep"
WORKTREE_PARENT_NAME = "CritMatic-bot"


@dataclass(frozen=True)
class FixResult:
    branch: str
    worktree_path: Path
    commit_sha: str | None
    confidence: int
    rationale: str
    files_changed: list[str] = field(default_factory=list)
    claude_exit_code: int = 0


def _build_prompt(report_text: str, removed_symbols: set[str]) -> str:
    symbol_list = ", ".join(sorted(removed_symbols)) if removed_symbols else "(none listed)"
    return (
        "You are fixing a WoW addon (CritMatic) that broke when a new WoW patch "
        "was released. Below is the patch report. Your job: edit the addon source "
        "to handle the removed/renamed APIs, then output a self-rated confidence "
        "number.\n\n"
        "PATCH REPORT:\n"
        f"{report_text}\n\n"
        "REMOVED API SYMBOLS THE ADDON USES:\n"
        f"{symbol_list}\n\n"
        "SCOPE RULES:\n"
        "- ONLY edit files in the addon's own source. Do NOT touch:\n"
        "  Libs/, Localization/, .release/, .pkgmeta.yaml, bot/, *.toc files\n"
        "- For each removed symbol, the patch report lists possible renames in the\n"
        "  \"Possibly renamed\" table. Use those when they look right.\n"
        "- For removed symbols with no rename candidate, add a defensive fallback:\n"
        "  use the new API if available, else degrade gracefully.\n"
        "- Keep changes MINIMAL and SURGICAL. Do not refactor unrelated code.\n\n"
        "OUTPUT REQUIREMENT:\n"
        "After all edits, output exactly one JSON object as the LAST LINE of your\n"
        "response, with no surrounding text. Schema:\n"
        '{"confidence": <integer 0-100>, "rationale": "<one to two sentences>", "files_changed": ["<repo-relative path>", ...]}\n'
    )


def _parse_claude_output(stdout: str) -> tuple[int, str, list[str]]:
    for line in reversed(stdout.strip().splitlines()):
        line = line.strip()
        if not line.startswith("{"):
            continue
        try:
            data = json.loads(line)
        except json.JSONDecodeError:
            continue
        try:
            confidence = int(data.get("confidence", 0))
        except (TypeError, ValueError):
            confidence = 0
        rationale = str(data.get("rationale", ""))
        files = list(data.get("files_changed") or [])
        return confidence, rationale, files
    return 0, "claude output unparseable", []


def _git(repo: Path, *args: str) -> subprocess.CompletedProcess:
    return subprocess.run(
        ["git", "-C", str(repo), *args],
        capture_output=True, text=True, check=False,
    )


def propose_fix(
    *,
    report_path: Path,
    branch_name: str,
    removed_symbols: set[str],
    repo_root: Path,
) -> FixResult:
    worktree_path = repo_root.parent / WORKTREE_PARENT_NAME / branch_name.replace("/", "_")

    # Create the worktree off master. Surface git failures explicitly so the
    # detector logs a useful error instead of an opaque FileNotFoundError from
    # the subsequent claude subprocess.
    wt_proc = subprocess.run(
        ["git", "-C", str(repo_root), "worktree", "add", "-b", branch_name, str(worktree_path), "master"],
        capture_output=True, text=True, check=False,
    )
    if wt_proc.returncode != 0:
        raise RuntimeError(f"git worktree add failed: {wt_proc.stderr.strip() or wt_proc.stdout.strip()}")

    report_text = report_path.read_text(encoding="utf-8")
    prompt = _build_prompt(report_text, removed_symbols)

    claude_proc = subprocess.run(
        [
            "claude", "-p", prompt,
            "--allowedTools", CLAUDE_ALLOWED_TOOLS,
            "--max-turns", str(CLAUDE_MAX_TURNS),
        ],
        cwd=str(worktree_path),
        capture_output=True, text=True,
        timeout=CLAUDE_TIMEOUT_SECONDS,
        check=False,
    )

    confidence, rationale, files_changed = _parse_claude_output(claude_proc.stdout)

    # The worktree is on the new branch with no commits yet. Detect Claude's
    # uncommitted changes via `git status --porcelain` instead of a diff range.
    status_proc = _git(worktree_path, "status", "--porcelain")
    has_changes = bool(status_proc.stdout.strip())

    commit_sha: str | None = None
    if has_changes:
        _git(worktree_path, "add", "-A")
        _git(worktree_path, "commit", "-m", f"bot: proposed fix for {branch_name}")
        rev = _git(worktree_path, "rev-parse", "HEAD")
        commit_sha = rev.stdout.strip() or None

    return FixResult(
        branch=branch_name,
        worktree_path=worktree_path,
        commit_sha=commit_sha,
        confidence=confidence,
        rationale=rationale,
        files_changed=files_changed,
        claude_exit_code=claude_proc.returncode,
    )
