"""Claude CLI subprocess wrapper for repair suggestions.

All functions call the Claude CLI binary and parse its JSON output.
Raises RuntimeError if Claude is not installed or returns an error.

This module must NOT be imported from drift_detector — it is AI-assisted
and belongs only in the repair layer.
"""

from __future__ import annotations

import json
import shutil
import subprocess
import time

from maeris_mcp.drift_detector.models import DriftIssue

_AUTOMATION_SYSTEM_PROMPT = (
    "You are running in a fully automated non-interactive CLI session. "
    "Respond with ONLY valid JSON — no prose, no markdown, no explanation. "
    "Complete your assigned task and STOP."
)


def _claude_run(prompt: str, model: str, timeout: int = 120, retries: int = 2) -> str:
    """Invoke the Claude CLI subprocess and return stdout.

    Args:
        prompt: The prompt to send to Claude.
        model: Claude model ID (e.g. 'claude-sonnet-4-6').
        timeout: Seconds before giving up.
        retries: Number of retries on empty response.

    Returns:
        Raw stdout string from Claude.

    Raises:
        RuntimeError: If the Claude binary is not found, returns non-zero,
            or returns empty output after all retries.
    """
    # On Windows, `claude` may resolve to a PowerShell wrapper (`claude.ps1`) or
    # cmd shim (`claude.cmd`) which can hit command-line length limits sooner.
    # Prefer the real executable when present.
    claude_bin = shutil.which("claude.exe") or shutil.which("claude")
    if not claude_bin:
        raise RuntimeError(
            "Claude Code CLI not found on PATH. "
            "Install from https://claude.ai/code and run 'claude auth login'."
        )
    result = subprocess.run(
        [
            claude_bin,
            "--model", model,
            "--print",
            "--append-system-prompt", _AUTOMATION_SYSTEM_PROMPT,
            prompt,
        ],
        capture_output=True,
        text=True,
        encoding="utf-8",
        errors="replace",
        timeout=timeout,
    )
    if result.returncode != 0:
        stderr = (result.stderr or "").strip()
        # Windows can hit command-line length limits when we include large source snippets.
        # Fall back to the Maeris AI proxy (direct API) in that case.
        if "command line is too long" in stderr.lower():
            return _direct_ai_run(prompt, model=model)
        raise RuntimeError(f"Claude exited {result.returncode}: {stderr[:500]}")
    return result.stdout


def _direct_ai_run(prompt: str, model: str) -> str:
    """Run a single-turn request via the Maeris AI proxy (no local CLI)."""
    import asyncio
    from uuid import uuid4

    async def _call() -> str:
        from maeris_mcp.maeris.client import MaerisClient
        body = {
            "model": model,
            "max_tokens": 8192,
            "system": _AUTOMATION_SYSTEM_PROMPT,
            "messages": [{"role": "user", "content": prompt}],
        }
        async with MaerisClient() as client:
            resp = await client.proxy_anthropic_message(body=body, idempotency_key=str(uuid4()))
        content = resp.get("content", [])
        if isinstance(content, list):
            return "".join(
                block.get("text", "")
                for block in content
                if isinstance(block, dict) and block.get("type") == "text"
            )
        if isinstance(content, str):
            return content
        return ""

    return asyncio.run(_call())


def _parse_json(raw: str) -> dict | list:
    """Strip markdown fences if present and parse JSON."""
    text = raw.strip()
    if not text:
        raise ValueError("Empty response from Claude CLI")

    # Try direct parse first
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    # Strip markdown fences
    if "```" in text:
        # Extract content between first pair of fences
        parts = text.split("```")
        for part in parts[1:]:
            inner = part
            if inner.startswith("json"):
                inner = inner[4:]
            inner = inner.strip()
            if inner:
                try:
                    return json.loads(inner)
                except json.JSONDecodeError:
                    continue

    # Try to find JSON object/array in the text
    for start_char, end_char in [("{", "}"), ("[", "]")]:
        start = text.find(start_char)
        end = text.rfind(end_char)
        if start != -1 and end > start:
            try:
                return json.loads(text[start:end + 1])
            except json.JSONDecodeError:
                continue

    raise ValueError(f"Could not parse JSON from response: {text[:200]}")


def suggest_selector_fix(
    issue: DriftIssue,
    source_snippet: str,
    model: str,
    *,
    candidate_selectors: list[str] | None = None,
) -> dict:
    """Ask Claude for a replacement CSS selector.

    Args:
        issue: The SELECTOR_MISSING drift issue.
        source_snippet: Relevant source code excerpt.
        model: Claude model to use.
        candidate_selectors: Optional pre-extracted stable attribute values
            from the relevant source files (data-testid, id, name, aria-label…).

    Returns:
        dict with keys: old_selector, new_selector, confidence (0.0–1.0),
        reasoning (str).
    """
    selector = issue.detail.get("selector", "")
    tc_name = issue.resource_name
    step = issue.detail.get("step") or issue.detail.get("component") or {}
    page_url = (step.get("page_url") or step.get("pageUrl") or step.get("url") or "").strip()
    action_taken = (step.get("action_taken") or step.get("actionType") or step.get("type") or "").strip()

    context_lines = [
        "A Playwright/UI test selector no longer exists in the source code.",
        "",
        f"Test: {tc_name}",
    ]
    if page_url:
        context_lines.append(f"Page URL: {page_url}")
    if action_taken:
        context_lines.append(f"Step action: {action_taken}")
    context_lines.append(f"Missing selector: {selector!r}")

    candidates_block = ""
    if candidate_selectors:
        capped = candidate_selectors[:60]
        candidates_block = (
            "\nStable attributes found in the relevant source files "
            "(prefer these when choosing a replacement):\n"
            + "\n".join(f"  - {s}" for s in capped)
            + "\n"
        )

    instructions = (
        "\nRules:\n"
        "1. If the candidate list above is non-empty, pick the closest match from it.\n"
        "2. Fall back to the source code only if the candidate list is empty or has no match.\n"
        "3. Selector preference order: data-automation-id, data-testid, "
        "[name=...], [id=...], [aria-label=...], .class-name.\n"
        "4. Never invent selectors that are not present in the source or the candidate list.\n"
        "\n"
        'Return ONLY JSON: {"old_selector": "...", "new_selector": "...", '
        '"confidence": 0.0, "reasoning": "..."}'
    )

    prompt = (
        "\n".join(context_lines)
        + candidates_block
        + f"\nSource code context:\n{source_snippet[:7000]}"
        + instructions
    )

    raw = _claude_run(prompt, model)
    result = _parse_json(raw)
    if not isinstance(result, dict):
        raise ValueError(f"Unexpected response structure: {type(result)}")
    return result


def suggest_api_update(issue: DriftIssue, model: str) -> dict:
    """Ask Claude how to handle a missing or unregistered route.

    Returns:
        dict with keys: action ('delete'|'update'|'ignore'), reason, proposed_url.
    """
    prompt = (
        f"A Maeris API entry has drifted from the codebase.\n\n"
        f"Issue: {issue.description}\n"
        f"Kind: {issue.kind.value}\n"
        f"Resource: {issue.resource_name} (ID: {issue.resource_id})\n"
        f"Detail: {json.dumps(issue.detail, indent=2)}\n\n"
        "Recommend the best action. Options:\n"
        "  - 'delete': the API no longer exists and should be removed from Maeris\n"
        "  - 'update': the path changed (provide proposed_url)\n"
        "  - 'ignore': this route is intentionally absent from the codebase\n\n"
        'Return ONLY JSON: {"action": "delete|update|ignore", '
        '"reason": "...", "proposed_url": "..." }'
    )

    raw = _claude_run(prompt, model)
    result = _parse_json(raw)
    if not isinstance(result, dict):
        raise ValueError(f"Unexpected response structure: {type(result)}")
    return result


def suggest_flow_fix(
    issue: DriftIssue,
    available_apis: list[dict],
    model: str,
) -> dict:
    """Ask Claude which API should replace a stale flow step.

    Returns:
        dict with keys: action ('replace'|'delete'|'ignore'),
        replacement_api_id, reason.
    """
    detail = issue.detail
    apis_summary = [
        {"id": a.get("api_id") or a.get("_id", ""), "name": a.get("name", ""),
         "method": a.get("type") or a.get("method", ""), "url": a.get("url", "")}
        for a in available_apis[:50]  # cap to avoid huge prompts
    ]

    prompt = (
        f"A flow step references a deleted API.\n\n"
        f"Flow: {issue.resource_name}\n"
        f"Issue: {issue.description}\n"
        f"Step detail: {json.dumps(detail, indent=2)}\n\n"
        f"Available APIs in Maeris:\n{json.dumps(apis_summary, indent=2)}\n\n"
        "Choose the best action:\n"
        "  - 'replace': pick the closest matching API (provide replacement_api_id)\n"
        "  - 'delete': remove this step from the flow\n"
        "  - 'ignore': leave as-is (not recommended)\n\n"
        'Return ONLY JSON: {"action": "replace|delete|ignore", '
        '"replacement_api_id": "...", "reason": "..."}'
    )

    raw = _claude_run(prompt, model)
    result = _parse_json(raw)
    if not isinstance(result, dict):
        raise ValueError(f"Unexpected response structure: {type(result)}")
    return result
