"""Pure parsers for provider CLI output.

Kept out of the adapters so contract tests can replay recorded stdout without
spawning a process. Every function here is total: it returns an AgentResult and
never raises on malformed input, because provider output is untrusted data.
"""

from __future__ import annotations

import json
from typing import Any

from pydantic import BaseModel, ValidationError

from .base import AgentResult

def read_usage(payload: object) -> dict | None:
    """Pull provider-reported token and cost figures. Returns None rather than guessing."""
    if not isinstance(payload, dict):
        return None
    usage = payload.get("usage")
    cost = payload.get("total_cost_usd")
    if not isinstance(usage, dict) and cost is None:
        return None
    record: dict = {}
    for key in ("input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"):
        value = (usage or {}).get(key)
        if isinstance(value, int):
            record[key] = value
    if isinstance(cost, (int, float)):
        record["cost_usd"] = round(float(cost), 6)
    turns = payload.get("num_turns")
    if isinstance(turns, int):
        record["turns"] = turns
    return record or None


#: Substrings in provider stderr that mean the operator must log in, never a retry.
AUTH_MARKERS = (
    "not logged in", "please log in", "login required", "unauthorized", "401",
    "invalid api key", "authentication failed", "credentials", "expired token",
)


def classify_failure(text: str) -> str:
    """Turn provider diagnostics into an error category. Section 12 of the design."""
    lowered = text.lower()
    if any(marker in lowered for marker in AUTH_MARKERS):
        return "AUTH"
    if any(marker in lowered for marker in ("rate limit", "429", "too many requests", "selected model is at capacity")):
        return "RATE_LIMIT"
    if any(marker in lowered for marker in ("connection refused", "network", "dns", "timed out", "unreachable")):
        return "NETWORK"
    return "AGENT_FAILURE"


#: Keys providers have used for the conversation identifier, most specific first.
SESSION_KEYS = ("session_id", "thread_id", "conversation_id", "sessionId", "threadId")


def find_session_id(payload: Any) -> str | None:
    """Search a decoded payload for a session identifier at any depth."""
    if isinstance(payload, dict):
        for key in SESSION_KEYS:
            value = payload.get(key)
            if isinstance(value, str) and value:
                return value
        for value in payload.values():
            found = find_session_id(value)
            if found:
                return found
    elif isinstance(payload, list):
        for value in payload:
            found = find_session_id(value)
            if found:
                return found
    return None


def parse_codex_stream(text: str, schema: type[BaseModel], artifact_text: str | None = None) -> AgentResult:
    """Read a `codex exec --json` event stream.

    The artifact itself comes from `--output-last-message`; the stream is read only
    for the thread id and for a final message fallback when that file is unusable.
    """
    session_id, fallback, usage = None, None, None
    for line in text.splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            event = json.loads(line)
        except json.JSONDecodeError:
            continue  # provider diagnostics interleaved with the stream
        session_id = session_id or find_session_id(event)
        usage = read_usage(event) or usage
        message = event.get("last_agent_message") or event.get("message") if isinstance(event, dict) else None
        if isinstance(message, str) and message.strip():
            fallback = message
    body = artifact_text if artifact_text and artifact_text.strip() else fallback
    if not body:
        return AgentResult("PROTOCOL", error="no structured output in the Codex stream", session_id=session_id, usage=usage)
    return _validate(body, schema, session_id, usage)


def parse_claude_output(text: str, schema: type[BaseModel]) -> AgentResult:
    """Read a `claude -p --output-format json` response."""
    try:
        payload = json.loads(text)
    except json.JSONDecodeError as error:
        return AgentResult("PROTOCOL", error=f"Claude output is not JSON: {error}")
    session_id = find_session_id(payload)
    usage = read_usage(payload)
    if isinstance(payload, dict) and payload.get("is_error"):
        return AgentResult(
            "AGENT_FAILURE",
            error=str(payload.get("result") or "Claude reported an error"),
            session_id=session_id,
            usage=usage,
        )
    structured = payload.get("structured_output", payload) if isinstance(payload, dict) else payload
    if isinstance(structured, str):
        try:
            structured = json.loads(structured)
        except json.JSONDecodeError as error:
            return AgentResult("PROTOCOL", error=f"Claude result is not JSON: {error}", session_id=session_id, usage=usage)
    return _validate(structured, schema, session_id, usage)


def _validate(body: Any, schema: type[BaseModel], session_id: str | None, usage: dict | None = None) -> AgentResult:
    try:
        artifact = schema.model_validate_json(body) if isinstance(body, str) else schema.model_validate(body)
    except (ValidationError, ValueError) as error:
        return AgentResult("PROTOCOL", error=str(error), session_id=session_id, usage=usage)
    return AgentResult("SUCCESS", artifact=artifact, session_id=session_id, usage=usage)

