"""Codex App Server adapter. Section 16 of the design.

Speaks the app-server JSON-RPC protocol over `codex app-server proxy`, which gives the
full event stream and a long-lived thread instead of one-shot `codex exec`. The state
machine above is untouched: this satisfies the same AgentAdapter contract.

The surface is marked experimental by the CLI, so the adapter probes for it and reports
itself incompatible rather than guessing at a protocol the installed build may not speak.
"""

from __future__ import annotations

import json
import subprocess
from pathlib import Path

from ..domain import ErrorCategory
from .base import AgentRequest, AgentResult
from .capability import Capability, probe_binary
from .fake import SCHEMAS
from .parse import classify_failure, find_session_id, read_usage
from .prompt import stage_prompt

REQUIRED = {"app_server": "app-server", "proxy": "proxy"}


class CodexAppServerAdapter:
    def __init__(
        self,
        binary: str = "codex",
        log_root: str | Path = ".dual-agent/logs",
        timeout_s: float = 900,
        allow_network: bool = True,
    ) -> None:
        self.binary = binary
        self.log_root = Path(log_root)
        self.timeout_s = timeout_s
        self.allow_network = allow_network
        self._capability: Capability | None = None

    def probe(self) -> Capability:
        self._capability = probe_binary(
            [self.binary], [["--version"], ["app-server", "--help"]], REQUIRED, required=set(REQUIRED)
        )
        return self._capability

    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}")
        self.log_root.mkdir(parents=True, exist_ok=True)
        message = {
            "jsonrpc": "2.0",
            "id": request.run_id,
            "method": "thread.run",
            "params": {
                "cwd": str(request.worktree_path),
                "prompt": stage_prompt(request),
                "output_schema": schema_type.model_json_schema(),
                "thread_id": request.session_id,
            },
        }
        try:
            process = subprocess.Popen(
                [self.binary, "app-server", "proxy"],
                cwd=str(request.worktree_path),
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )
        except OSError as error:
            return AgentResult(ErrorCategory.AGENT_FAILURE.value, error=str(error))
        if on_start:
            on_start(process.pid)
        try:
            out, err = process.communicate(json.dumps(message) + "\n", timeout=self.timeout_s)
        except subprocess.TimeoutExpired:
            process.kill()
            return AgentResult("TIMEOUT", error="Codex app server timed out")
        (self.log_root / f"app-server-{request.run_id}.stdout.jsonl").write_text(out or "", encoding="utf-8")
        (self.log_root / f"app-server-{request.run_id}.stderr.log").write_text(err or "", encoding="utf-8")
        return parse_app_server_stream(out or "", schema_type, err or "")


def parse_app_server_stream(text: str, schema, stderr: str = "") -> AgentResult:
    """Read the JSON-RPC event stream and pull the final structured response out of it."""
    session_id, usage, payload, failure = None, None, None, None
    for line in text.splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            event = json.loads(line)
        except json.JSONDecodeError:
            continue
        if not isinstance(event, dict):
            continue
        session_id = session_id or find_session_id(event)
        usage = read_usage(event) or usage
        if isinstance(event.get("error"), dict):
            failure = str(event["error"].get("message") or "app server reported an error")
        result = event.get("result")
        if isinstance(result, dict):
            payload = result.get("structured_output", result.get("output", result))
    if payload is None:
        category = classify_failure(stderr) if stderr else "PROTOCOL"
        return AgentResult(
            category if failure or stderr else "PROTOCOL",
            error=failure or "no structured result in the app server stream",
            session_id=session_id,
            usage=usage,
        )
    try:
        artifact = schema.model_validate(payload) if not isinstance(payload, str) else schema.model_validate_json(payload)
    except Exception as error:  # noqa: BLE001 - provider output is untrusted
        return AgentResult("PROTOCOL", error=str(error), session_id=session_id, usage=usage)
    return AgentResult("SUCCESS", artifact=artifact, session_id=session_id, usage=usage)
