import json
from dataclasses import replace
from pathlib import Path
from uuid import uuid4

from dual_agent.infra.process import ProcessRunner

from ..base import AgentRequest, AgentResult, Reply
from ..capability import Capability, probe_binary
from ..fake import SCHEMAS
from ..parse import classify_failure, find_session_id, parse_claude_output, read_usage
from ..prompt import stage_prompt

#: Tokens the help output must contain for the adapter to be usable at all.
REQUIRED = {"print": "--print", "json": "--output-format", "schema": "--json-schema"}

#: Optional capabilities. Missing ones degrade behaviour but never fail the probe.
OPTIONAL = {"resume": "--resume"}


class ClaudeAdapter:
    def __init__(
        self,
        binary: str = "claude",
        log_root: str | Path = ".dual-agent/logs",
        timeout_s: float = 900,
        allow_network: bool = True,
        model: str = "",
        effort: str = "",
        settings_file: str = "",
        passthrough_env: tuple[str, ...] = (),
    ) -> None:
        self.binary = binary
        self.log_root = Path(log_root)
        self.timeout_s = timeout_s
        self.allow_network = allow_network
        self.model = model
        #: Passed as `--effort <level>`; empty leaves the CLI default.
        self.effort = effort
        #: Enterprise settings file, e.g. one declaring an apiKeyHelper.
        self.settings_file = settings_file
        self.passthrough_env = tuple(passthrough_env)
        self._capability: Capability | None = None

    def probe(self) -> Capability:
        self._capability = probe_binary([self.binary], [["--version"], ["--help"]], {**REQUIRED, **OPTIONAL}, required=set(REQUIRED))
        return self._capability

    def _tuning(self) -> list[str]:
        """Model and effort, in the form this CLI accepts."""
        argv = ["--model", self.model] if self.model else []
        if self.effort:
            argv += ["--effort", self.effort]
        if self.settings_file:
            argv += ["--settings", self.settings_file]
        return argv

    def supports_resume(self) -> bool:
        capability = self._capability or self.probe()
        return capability.features.get("resume", False)

    def ask(self, prompt: str, cwd: Path, session_id: str | None = None, on_start=None) -> Reply:
        """Answer a question in prose. No schema, no artifact, no state change."""
        argv = [self.binary, "-p"]
        if session_id and self.supports_resume():
            argv += ["--resume", session_id]
        argv += [prompt, "--output-format", "json", "--permission-mode", "plan", *self._tuning()]
        result = ProcessRunner(self.log_root, allow_network=self.allow_network, extra_env=self.passthrough_env).run(
            argv, cwd, self.timeout_s, name=f"claude-ask-{uuid4().hex}",
            on_start=on_start, stdout_suffix="json",
        )
        if result.status == "TIMEOUT":
            return Reply("", ok=False, error="Claude timed out")
        raw = _read(result.stdout_path)
        try:
            payload = json.loads(raw)
        except json.JSONDecodeError:
            return Reply("", ok=False, error=(_read(result.stderr_path).strip() or "Claude output is not JSON"))
        if payload.get("is_error"):
            return Reply("", ok=False, error=str(payload.get("result") or "Claude reported an error"))
        text = payload.get("result") or ""
        if not isinstance(text, str) or not text.strip():
            return Reply("", ok=False, error="Claude said nothing")
        return Reply(text.strip(), session_id=find_session_id(payload), usage=read_usage(payload))

    def run(self, request: AgentRequest, on_start=None) -> AgentResult:
        schema_type = SCHEMAS.get(request.stage)
        if schema_type is None:
            return AgentResult("PROTOCOL", error=f"unsupported stage {request.stage}")
        argv = [self.binary, "-p"]
        if request.session_id and self.supports_resume():
            argv += ["--resume", request.session_id]
        argv += [
            stage_prompt(request),
            "--output-format",
            "json",
            "--json-schema",
            json.dumps(schema_type.model_json_schema(), separators=(",", ":")),
            "--permission-mode",
            "acceptEdits" if request.stage in {"IMPLEMENT", "FIX"} else "plan",
            *self._tuning(),
        ]
        result = ProcessRunner(self.log_root, allow_network=self.allow_network, extra_env=self.passthrough_env).run(
            argv, request.worktree_path, self.timeout_s, name=f"claude-{request.run_id}", on_start=on_start, stdout_suffix="json"
        )
        evidence = {
            "exit_code": result.exit_code,
            "stdout_path": result.stdout_path,
            "stderr_path": result.stderr_path,
            "duration_ms": result.duration_ms,
        }
        if result.status == "TIMEOUT":
            return AgentResult("TIMEOUT", error="Claude timed out", **evidence)
        try:
            text = result.stdout_path.read_text(encoding="utf-8", errors="replace")
        except OSError as error:
            return AgentResult("PROTOCOL", error=str(error))
        parsed = parse_claude_output(text, schema_type)
        if result.status != "SUCCESS" and parsed.status == "SUCCESS":
            category = classify_failure(_read(result.stderr_path)) if hasattr(result, "stderr_path") else "AGENT_FAILURE"
            return AgentResult(category, error=f"Claude exited with {result.exit_code}", session_id=parsed.session_id, **evidence)
        return replace(parsed, **evidence)


def _read(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8", errors="replace")
    except OSError:
        return ""
