"""Claude Code CLI bridge backend for schema generation.

Uses the user's already-authenticated `claude` CLI binary instead of an
API key. Mirrors anthropic_backend.py's interface (MantaraSchema output,
3-attempt retry with backoff, JSON extraction) but routes the LLM call
through a subprocess to the locally-installed CLI.

Why this exists: lets the Mantara v8 pipeline run without any
ANTHROPIC_API_KEY or AWS credentials — it piggy-backs on the user's
Claude Max OAuth session.

Config via env:
  CLAUDE_CLI_BIN          Override path to the claude binary
  CLAUDE_CLI_MODEL        Optional model alias (default: "sonnet")
  CLAUDE_CLI_TIMEOUT      Subprocess timeout in seconds (default: 600)
  MANTARA_TEMPERATURE     Not respected — CLI doesn't expose it
  MANTARA_MAX_TOKENS      Not respected — CLI uses model defaults

Registered in backends/__init__.py under key "claude-cli".
"""

import json
import os
import re
import shutil
import subprocess
import sys
import time

from models import MantaraSchema


def _log(msg: str):
    print(f"  [claude-cli] {msg}", file=sys.stderr)


def _find_claude_bin() -> str:
    """Locate the claude binary. Honor CLAUDE_CLI_BIN override."""
    override = os.getenv("CLAUDE_CLI_BIN")
    if override and os.path.isfile(override) and os.access(override, os.X_OK):
        return override

    # Common install locations in priority order
    candidates = [
        os.path.expanduser("~/.local/bin/claude"),
        "/opt/homebrew/bin/claude",
        "/usr/local/bin/claude",
    ]
    for p in candidates:
        if os.path.isfile(p) and os.access(p, os.X_OK):
            return p

    # Fall back to $PATH lookup
    via_path = shutil.which("claude")
    if via_path:
        return via_path

    raise RuntimeError(
        "claude CLI not found. Install Claude Code or set CLAUDE_CLI_BIN."
    )


class ClaudeCliBackend:
    """Schema generator that shells out to the local `claude` CLI."""

    def __init__(self):
        self.bin = _find_claude_bin()
        self.model = os.getenv("CLAUDE_CLI_MODEL", "sonnet")
        self.timeout = int(os.getenv("CLAUDE_CLI_TIMEOUT", "600"))

    def _extract_json(self, text: str) -> dict:
        """Same JSON-extraction strategy as anthropic_backend.py."""
        try:
            return json.loads(text)
        except json.JSONDecodeError:
            pass

        cleaned = text.strip()
        if cleaned.startswith("```"):
            cleaned = re.sub(r"^```(?:json)?\s*\n?", "", cleaned)
            cleaned = re.sub(r"\n?```\s*$", "", cleaned)
            try:
                return json.loads(cleaned)
            except json.JSONDecodeError:
                pass

        match = re.search(r"\{[\s\S]*\}", text)
        if match:
            try:
                return json.loads(match.group())
            except json.JSONDecodeError:
                pass

        raise ValueError(
            f"Could not extract valid JSON from CLI response (length: {len(text)})"
        )

    def _run_cli(self, system_prompt: str, user_input: str) -> tuple[str, dict]:
        """
        Shell out to `claude -p --output-format json` with:
          stdin  = user_input
          system = hardened + original system prompt

        Returns (result_text, metadata).
        """
        hardened_system = (
            "You are a headless JSON API. Return ONLY raw JSON that directly "
            "answers the prompt. Do NOT wrap output in markdown code fences. "
            "Do NOT include PAI mode headers (no banners, no emoji sections, "
            "no summary blocks). Your entire response must be parseable by "
            "json.loads() on its own.\n\n" + system_prompt
        )

        args = [
            self.bin,
            "-p",
            "--output-format", "json",
            "--append-system-prompt", hardened_system,
            "--permission-mode", "bypassPermissions",
            "--model", self.model,
        ]

        try:
            result = subprocess.run(
                args,
                input=user_input,
                capture_output=True,
                text=True,
                timeout=self.timeout,
                check=False,
            )
        except subprocess.TimeoutExpired:
            raise RuntimeError(
                f"claude CLI timed out after {self.timeout}s"
            )

        if result.returncode != 0:
            raise RuntimeError(
                f"claude CLI exit {result.returncode}: "
                f"{(result.stderr or '')[:500]}"
            )

        try:
            wrapper = json.loads(result.stdout)
        except json.JSONDecodeError as e:
            raise RuntimeError(
                f"Failed to parse CLI wrapper JSON: {e}. "
                f"Raw (first 300): {result.stdout[:300]}"
            )

        if wrapper.get("is_error") or wrapper.get("type") == "error":
            raise RuntimeError(
                f"CLI reported error: {wrapper.get('result', '(no message)')}"
            )

        content = wrapper.get("result") or ""
        if not content:
            raise RuntimeError("CLI returned empty result field")

        return content, wrapper

    def generate(
        self, system_prompt: str, user_input: str, model: str | None = None
    ) -> MantaraSchema:
        """
        Generate MantaraSchema via the claude CLI.
        Retries up to 3 times with exponential backoff on any failure.
        """
        # NOTE: `model` param is accepted for interface compat but we use the
        # backend's configured model (CLAUDE_CLI_MODEL) since the CLI picks
        # its own model via --model alias.
        del model

        last_error = None
        for attempt in range(3):
            try:
                start = time.time()
                text, wrapper = self._run_cli(system_prompt, user_input)
                elapsed = round(time.time() - start, 1)

                # Log usage (mirrors anthropic_backend.py format)
                model_usage = wrapper.get("modelUsage", {})
                used_model = next(iter(model_usage), self.model)
                tokens = model_usage.get(used_model, {})
                _log(
                    f"model={used_model}  "
                    f"input_tokens={tokens.get('inputTokens', 0)}  "
                    f"output_tokens={tokens.get('outputTokens', 0)}  "
                    f"latency={elapsed}s  "
                    f"cost=${wrapper.get('total_cost_usd', 0):.4f}"
                )

                # Parse JSON body + validate against MantaraSchema
                data = self._extract_json(text)
                schema = MantaraSchema(**data)
                return schema

            except Exception as e:
                last_error = e
                if attempt < 2:
                    wait = (2 ** attempt) * 1  # 1s, 2s
                    _log(
                        f"Attempt {attempt + 1} failed: {e}. "
                        f"Retrying in {wait}s..."
                    )
                    time.sleep(wait)

        raise RuntimeError(
            f"claude-cli backend failed after 3 attempts. Last error: {last_error}"
        )
