from __future__ import annotations

import json
import os
import subprocess
import tempfile
from typing import Any


_STDERR_EXCERPT_CHARS = 2000


class OpusOAuthError(RuntimeError):
    """Raised when the local Claude OAuth CLI call cannot produce text."""

    def __init__(
        self,
        message: str,
        *,
        model_id: str,
        returncode: int | None,
        stderr: str | bytes | None = None,
        timeout: int | float | None = None,
    ) -> None:
        self.model_id = model_id
        self.returncode = returncode
        self.return_code = returncode
        self.stderr_excerpt = _excerpt(stderr)
        self.stderr = self.stderr_excerpt
        self.timeout = timeout

        detail = f"{message} (model={model_id!r}, returncode={returncode}"
        if timeout is not None:
            detail += f", timeout={timeout}"
        if self.stderr_excerpt:
            detail += f", stderr={self.stderr_excerpt!r}"
        detail += ")"
        super().__init__(detail)


def _excerpt(text: str | bytes | None) -> str:
    if text is None:
        return ""
    if isinstance(text, bytes):
        text = text.decode("utf-8", errors="replace")
    stripped = text.strip()
    if len(stripped) <= _STDERR_EXCERPT_CHARS:
        return stripped
    return stripped[:_STDERR_EXCERPT_CHARS].rstrip()


def call_opus_oauth(
    model_id: str,
    system_prompt: str,
    user_prompt: str,
    *,
    timeout: int | float = 600,
    effort: str | None = None,
    json_schema: dict[str, Any] | None = None,
) -> str:
    argv = [
        "claude",
        "--print",
        "--permission-mode",
        "bypassPermissions",
        # Isolate the structured-output call from CLAUDE.md auto-discovery:
        # `--setting-sources project` drops the user-scope ~/.claude/CLAUDE.md
        # (the coding-assistant behavioral contract — verification blocks etc.)
        # and, combined with a clean cwd below, the project CLAUDE.md too. Without
        # this, Opus follows those instructions and pollutes the JSON (e.g. emits
        # a 'VERIFICATION' preamble instead of the schema object). OAuth auth is
        # unaffected by this flag (verified live). REC-161/REC-160 follow-up.
        "--setting-sources",
        "project",
        "--model",
        model_id,
        "--append-system-prompt",
        system_prompt,
    ]
    if effort is not None:
        argv.extend(["--effort", effort])

    effective_prompt = user_prompt
    if json_schema is not None:
        effective_prompt = (
            f"{user_prompt}\n\n"
            "# OUTPUT JSON SCHEMA (your response MUST validate against this EXACT schema)\n"
            f"```json\n{json.dumps(json_schema)}\n```\n\n"
            "Your ENTIRE response MUST be a single JSON object that validates against the "
            "schema above, INCLUDING every required top-level key named in the schema. "
            "Do not omit any top-level key. Do not deliver the output via any tool or side "
            "channel — emit the raw JSON object as your message text."
        )

    env = os.environ.copy()
    env.pop("ANTHROPIC_API_KEY", None)

    # Run in a clean, CLAUDE.md-free working directory so no project memory is
    # auto-discovered (the python cwd is usually inside ~/CLAUDE_PROJECTS, which
    # carries CLAUDE.md). `--setting-sources project` + an empty project dir =
    # zero CLAUDE.md context.
    clean_cwd = os.path.join(tempfile.gettempdir(), "recoil_opus_oauth_cwd")
    os.makedirs(clean_cwd, exist_ok=True)

    try:
        result = subprocess.run(
            argv,
            input=effective_prompt,
            capture_output=True,
            text=True,
            timeout=timeout,
            env=env,
            cwd=clean_cwd,
            check=False,
        )
    except subprocess.TimeoutExpired as exc:
        raise OpusOAuthError(
            "claude OAuth call timed out",
            model_id=model_id,
            returncode=124,
            stderr=exc.stderr,
            timeout=timeout,
        ) from exc
    except FileNotFoundError as exc:
        raise OpusOAuthError(
            "claude binary not found on PATH",
            model_id=model_id,
            returncode=-1,
            stderr=str(exc),
        ) from exc

    if result.returncode != 0:
        raise OpusOAuthError(
            "claude OAuth call failed",
            model_id=model_id,
            returncode=result.returncode,
            stderr=result.stderr,
        )

    stdout = result.stdout.strip()
    if not stdout:
        raise OpusOAuthError(
            "claude OAuth call produced empty output",
            model_id=model_id,
            returncode=result.returncode,
            stderr=result.stderr,
        )

    return stdout


__all__ = ["OpusOAuthError", "call_opus_oauth"]
