from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path

from .base import Patcher, PatchResult


@dataclass(frozen=True)
class HeuristicSnapshotPatcher(Patcher):
    """Demo patcher that only fixes the toy `off_by_one` case using snapshot locals.

    This is intentionally narrow: it exists to smoke-test the A/B harness end-to-end
    without wiring a real LLM client.
    """

    name: str = "heuristic"

    def propose_patch(self, *, prompt: str, context_dir: Path, cwd: Path) -> PatchResult:
        snapshot_path = context_dir / "snapshot.json"
        if not snapshot_path.exists():
            return PatchResult(diff=None, meta={"reason": "no_snapshot"})

        try:
            snapshot = json.loads(snapshot_path.read_text(encoding="utf-8"))
        except Exception:
            return PatchResult(diff=None, meta={"reason": "bad_snapshot_json"})

        frames = snapshot.get("frames") or []
        if not isinstance(frames, list) or not frames:
            return PatchResult(diff=None, meta={"reason": "no_frames"})

        crash = frames[0]
        if not isinstance(crash, dict):
            return PatchResult(diff=None, meta={"reason": "bad_crash_frame"})

        file_rel = crash.get("file_rel") or crash.get("file")
        if not isinstance(file_rel, str) or not file_rel:
            return PatchResult(diff=None, meta={"reason": "no_crash_file"})

        # The toy case crashes in evals/cases/off_by_one/buggy.py
        if not file_rel.endswith("buggy.py"):
            return PatchResult(diff=None, meta={"reason": "not_toy_case", "file": file_rel})

        locals_ = crash.get("locals") or {}
        if not isinstance(locals_, dict):
            return PatchResult(diff=None, meta={"reason": "no_locals"})

        i_val = locals_.get("i")
        xs_val = locals_.get("xs")
        if not isinstance(i_val, int) or not isinstance(xs_val, list):
            return PatchResult(diff=None, meta={"reason": "unexpected_locals"})

        if i_val != len(xs_val):
            return PatchResult(diff=None, meta={"reason": "not_off_by_one"})

        # Fix: clamp index to last element (minimal patch).
        target = Path(file_rel)
        if not target.is_absolute():
            target = cwd / target

        try:
            src = target.read_text(encoding="utf-8").splitlines(True)
        except Exception:
            return PatchResult(
                diff=None, meta={"reason": "cannot_read_target", "file": str(target)}
            )

        # Replace `return xs[i]` with a guarded version.
        old = "    return xs[i]\n"
        new = "    return xs[i - 1]\n"
        if old not in src:
            return PatchResult(diff=None, meta={"reason": "pattern_not_found"})

        updated = "".join([new if line == old else line for line in src])
        before = "".join(src)

        diff = _unified_diff(
            path_rel=_relpath_from_cwd(target, cwd),
            before=before,
            after=updated,
        )
        return PatchResult(diff=diff, meta={"reason": "patched"})


def _relpath_from_cwd(path: Path, cwd: Path) -> str:
    try:
        return str(path.relative_to(cwd))
    except Exception:
        return str(path)


def _unified_diff(*, path_rel: str, before: str, after: str) -> str:
    import difflib

    a_lines = before.splitlines(True)
    b_lines = after.splitlines(True)
    diff_lines = difflib.unified_diff(
        a_lines,
        b_lines,
        fromfile=f"a/{path_rel}",
        tofile=f"b/{path_rel}",
        lineterm="",
    )
    return "\n".join(diff_lines) + "\n"
