"""Direct Anthropic API fallback for `maeris listen`.

When Claude Code CLI is not installed, this module runs an agentic loop
that calls POST /ai/messages on the Maeris backend (which proxies to
Anthropic) and executes tool calls locally.
"""

import asyncio
import json
import random
import re
import threading

import click

from ._claude_helpers import TOOL_LABELS, BUILTIN_LABELS
from ._tool_executor import LocalToolExecutor
from maeris_mcp.maeris.client import InsufficientCreditsError, RateLimitError

MAX_TURNS = 50
MODEL = "claude-sonnet-4-6"
MAX_TOKENS = 16384
RATE_LIMIT_RETRIES = 5           # was 3
RATE_LIMIT_BASE_DELAY = 5  # seconds
SCAN_MAX_TOKENS = 8192
SCAN_INTER_TURN_DELAY = 1.5      # was 0.6 — respects Tier 1 RPM
SCAN_TOOL_DELAY = 0.8
DEFAULT_INTER_TURN_DELAY = 1.2   # safe for Tier 1 RPM (was 0 for non-scan)
DEFAULT_TOOL_DELAY = 0.3         # small per-tool pause for non-scan

PRUNE_THRESHOLD_TOKENS = 50_000
KEEP_TAIL_TURNS = 6

CONTEXT_WINDOW = 200_000
ABSOLUTE_MIN_TOKENS = 1_024


def _estimate_tokens(messages: list[dict]) -> int:
    """Rough token estimate: 1 token per 4 characters."""
    total = 0
    for msg in messages:
        content = msg.get("content", "")
        if isinstance(content, str):
            total += len(content)
        elif isinstance(content, list):
            for block in content:
                if isinstance(block, dict):
                    total += len(str(block.get("text", "")))
                    total += len(str(block.get("content", "")))
        total += 20  # per-message overhead
    return total // 4


def _prune_message_history(messages: list[dict], keep_tail_turns: int = KEEP_TAIL_TURNS) -> list[dict]:
    """Keep messages[0] (original request) + last keep_tail_turns*2 messages."""
    tail_size = keep_tail_turns * 2
    if len(messages) <= 1 + tail_size:
        return messages
    tail = messages[-tail_size:]
    dropped = len(messages) - 1 - len(tail)
    placeholder = {
        "role": "user",
        "content": (
            f"[{dropped} earlier messages were compacted to stay within "
            "token limits. The original task is in the first message above. "
            "Continue from the most recent context below.]"
        ),
    }
    return [messages[0], placeholder] + tail


def _compute_max_tokens(estimated_input: int, default_max: int) -> int:
    """Leave 10% headroom in context window."""
    available = int(CONTEXT_WINDOW * 0.90) - estimated_input
    return max(ABSOLUTE_MIN_TOKENS, min(default_max, available))


def _looks_like_security_scan_request(user_message: str) -> bool:
    q = user_message.lower()
    keywords = [
        "scan for security",
        "security scan",
        "scan security",
        "vulnerabilit",
        "owasp",
    ]
    return any(k in q for k in keywords)


def _is_explicit_deep_scan_request(user_message: str) -> bool:
    q = user_message.lower()
    deep_markers = [
        "deep security",
        "comprehensive security",
        "full security",
        "all_static",
        "deep scan",
        "full audit",
    ]
    return any(m in q for m in deep_markers)


def _build_security_scan_user_message(original: str, *, two_stage: bool) -> str:
    if not two_stage:
        return (
            f"{original}\n\n"
            "Execution policy:\n"
            "- User explicitly requested deep/comprehensive security; run deep scan directly.\n"
            "- Use moderate pacing: keep file batches small and avoid large payload bursts.\n"
        )
    return (
        f"{original}\n\n"
        "Execution policy (rate-limit safe):\n"
        "1) Run a QUICK first pass using security_scan with scan_profile='owasp_top10_static' and small batches.\n"
        "2) Finalize the quick pass and inspect severities.\n"
        "3) ONLY if quick pass finds any high/critical severity issue, escalate to a second deep pass "
        "(scan_profile='deep_security').\n"
        "4) If only low/medium findings exist, stop after quick pass and summarize.\n"
        "5) Use moderate pacing throughout (small batches, avoid large payload bursts).\n"
        "6) Keep existing tool workflow intact (include_content batching + is_last finalization).\n"
    )


def _make_structured_error(
    *,
    error_code: str,
    error_message: str,
    retry_after_seconds: int | None = None,
    suggested_next_action: str | None = None,
) -> str:
    payload = {
        "error_code": error_code,
        "error_message": error_message,
        "retry_after_seconds": retry_after_seconds,
        "suggested_next_action": suggested_next_action or "Try again in a minute.",
        # Backward-compatible plain text fallback
        "error": error_message,
    }
    return json.dumps(payload)


def _extract_scan_profile(user_message: str) -> str:
    """Extract scan profile from user message, default to owasp_top10_static."""
    q = user_message.lower()
    profiles = [
        "all_static", "owasp_top10_static", "api_static", "sast_static",
        "auth_session_static", "data_exposure_static", "infra_static",
        "deep_security",
    ]
    for p in profiles:
        if p in q:
            return p
    if any(k in q for k in ["deep", "comprehensive", "full", "thorough"]):
        return "all_static"
    return "owasp_top10_static"


def _parse_findings_json(text: str) -> list[dict]:
    """Parse AI-generated findings from text, handling markdown fences."""
    text = text.strip()
    # Strip markdown code fences
    if text.startswith("```"):
        lines = text.split("\n")
        # Remove first and last fence lines
        if lines[0].startswith("```"):
            lines = lines[1:]
        if lines and lines[-1].strip() == "```":
            lines = lines[:-1]
        text = "\n".join(lines).strip()

    try:
        parsed = json.loads(text)
        if isinstance(parsed, list):
            return parsed
        if isinstance(parsed, dict) and "findings" in parsed:
            return parsed["findings"]
        return [parsed] if parsed else []
    except json.JSONDecodeError:
        # Try to find JSON array in the text
        match = re.search(r'\[[\s\S]*\]', text)
        if match:
            try:
                return json.loads(match.group())
            except json.JSONDecodeError:
                pass
        return []


async def execute_scan_via_batch(
    client,
    cmd_id: str,
    user_message: str,
    cwd: str,
    system_prompt: str,
    on_progress,
    cancel_event: threading.Event,
) -> tuple[bool, str]:
    """Run a security scan using the Anthropic Message Batches API.

    3-phase approach:
      Phase 1: Collect all file batches locally (0 API calls)
      Phase 2: Submit all analysis as a single batch (1 API call)
      Phase 3: Parse findings and finalize (1 local MCP call)

    Returns (success, result_text).
    """
    executor = LocalToolExecutor(cwd)
    scan_profile = _extract_scan_profile(user_message)

    # ── Phase 1: Collect all file batches locally ──
    on_progress("Phase 1: Collecting file batches…")
    all_batches = []
    offset = 0
    scan_id = None

    while True:
        if cancel_event.is_set():
            return False, "Cancelled by user."

        args: dict = {
            "target_path": cwd,
            "include_content": True,
            "offset": offset,
            "limit": 5,
            "scan_profile": scan_profile,
        }
        if scan_id:
            args["scan_id"] = scan_id
            args["findings"] = []  # acknowledge previous batch for coverage tracking

        result_str = await executor.execute("security_scan", args)
        result = json.loads(result_str)

        if not scan_id:
            scan_id = result.get("scanId")

        all_batches.append(result)
        pagination = result.get("pagination", {})
        returned = pagination.get("returned", 0)
        on_progress(f"  Collected batch {len(all_batches)} ({returned} files)")

        if not pagination.get("hasMore", False):
            break
        offset = pagination["nextOffset"]

    total_files = sum(b.get("pagination", {}).get("returned", 0) for b in all_batches)
    on_progress(f"Phase 1 complete: {len(all_batches)} batches, {total_files} files")

    # ── Phase 2: Submit batch AI analysis ──
    on_progress("Phase 2: Submitting batch analysis…")
    batch_requests = []
    for i, batch in enumerate(all_batches):
        file_contents = batch.get("fileContents", {})
        if not file_contents:
            continue

        selected_checks = batch.get("selectedChecks") or batch.get("selectedChecksGrouped")
        analysis_instructions = batch.get("analysisInstructions", "")

        file_listing = "\n\n".join(
            f"### {path}\n```\n{content}\n```"
            for path, content in file_contents.items()
        )
        batch_requests.append({
            "custom_id": f"scan-batch-{i}",
            "params": {
                "model": MODEL,
                "max_tokens": SCAN_MAX_TOKENS,
                "system": [{"type": "text", "text": f"You are a security analyst. {analysis_instructions}"}],
                "messages": [{"role": "user", "content": (
                    f"Analyze for security issues:\n\n"
                    f"Checks: {json.dumps(selected_checks)}\n\n"
                    f"Files:\n{file_listing}\n\n"
                    "Return ONLY a JSON array of findings. Each finding must have: "
                    "title, description, severity (critical/high/medium/low/info), "
                    "file_path, line_number (if known), owasp_category (if applicable), "
                    "and remediation."
                )}],
            },
        })

    if not batch_requests:
        on_progress("No files to analyze.")
        return True, "No files found to scan."

    on_progress(f"  Submitting {len(batch_requests)} analysis requests as a single batch…")
    batch_meta = await client.create_message_batch(batch_requests)
    batch_id = batch_meta.get("id", "unknown")
    on_progress(f"  Batch created: {batch_id[:16]}…")

    results = await client.poll_batch_until_complete(
        batch_id,
        poll_interval=10.0,
        max_wait=600.0,
        on_progress=on_progress,
    )
    on_progress(f"Phase 2 complete: {len(results)} results received")

    # ── Phase 3: Parse findings and finalize ──
    on_progress("Phase 3: Parsing findings and finalizing…")
    all_findings = []
    for item in results:
        result_data = item.get("result", {})
        if result_data.get("type") != "succeeded":
            on_progress(f"  Batch item {item.get('custom_id', '?')} failed: {result_data.get('type', 'unknown')}")
            continue
        message = result_data.get("message", {})
        content = message.get("content", [])
        text = "".join(
            b.get("text", "")
            for b in content
            if isinstance(b, dict) and b.get("type") == "text"
        )
        parsed = _parse_findings_json(text)
        all_findings.extend(parsed)

    on_progress(f"  Parsed {len(all_findings)} findings from AI analysis")

    # Finalize the scan
    finalize_result_str = await executor.execute("security_scan", {
        "scan_id": scan_id,
        "findings": all_findings,
        "is_last": True,
    })
    finalize_result = json.loads(finalize_result_str)

    # Build summary
    summary = finalize_result.get("summary", {})
    by_severity = summary.get("by_severity", {})
    total = summary.get("total_findings", len(all_findings))

    report_lines = [
        f"# Security Scan Report (Batch Mode)\n",
        f"**Total findings: {total}**\n",
        f"| Severity | Count |",
        f"|----------|-------|",
    ]
    for sev in ["critical", "high", "medium", "low", "info"]:
        count = by_severity.get(sev, 0)
        if count:
            report_lines.append(f"| {sev.capitalize()} | {count} |")

    report_lines.append(f"\n**Files scanned:** {total_files}")
    report_lines.append(f"**Batches:** {len(all_batches)}")
    report_lines.append(f"**Scan profile:** {scan_profile}")

    # Include top findings
    if all_findings:
        report_lines.append("\n## Top Findings\n")
        sorted_findings = sorted(
            all_findings,
            key=lambda f: {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}.get(
                str(f.get("severity", "info")).lower(), 5
            ),
        )
        for finding in sorted_findings[:5]:
            sev = str(finding.get("severity", "info")).upper()
            title = finding.get("title", "Untitled")
            fpath = finding.get("file_path", "unknown")
            line = finding.get("line_number", "")
            loc = f"{fpath}:{line}" if line else fpath
            remediation = finding.get("remediation", "")
            report_lines.append(f"- **[{sev}]** {title} — `{loc}`")
            if remediation:
                report_lines.append(f"  - Fix: {remediation}")

    # Push to Maeris
    scan_id_final = finalize_result.get("scan_id") or scan_id
    if scan_id_final:
        try:
            push_result_str = await executor.execute("push_to_maeris", {
                "data_type": "vulnerabilities",
                "scan_id": scan_id_final,
            })
            push_result = json.loads(push_result_str)
            pushed_count = push_result.get("pushed", push_result.get("count", 0))
            report_lines.append(f"\n**Pushed {pushed_count} findings to Maeris.**")
        except Exception as push_err:
            report_lines.append(f"\n*Failed to push to Maeris: {push_err}*")

    report = "\n".join(report_lines)
    on_progress("Scan complete.")
    return True, report


async def execute_via_direct_api(
    client,
    cmd_id: str,
    user_message: str,
    cwd: str,
    system_prompt: str,
    on_progress,
    cancel_event: threading.Event,
) -> tuple[bool, str]:
    """Run an agentic loop via the Maeris AI proxy.

    Returns (success, result_text).
    """
    executor = LocalToolExecutor(cwd)
    tools = executor.get_tool_definitions()
    is_scan_request = _looks_like_security_scan_request(user_message)
    explicit_deep_scan = is_scan_request and _is_explicit_deep_scan_request(user_message)
    two_stage_scan = is_scan_request and not explicit_deep_scan

    # Try batch mode for security scans (rate-limit safe)
    if is_scan_request:
        try:
            if await client.check_batch_support():
                on_progress("Using batch API mode (rate-limit safe)...")
                return await execute_scan_via_batch(
                    client, cmd_id, user_message, cwd,
                    system_prompt, on_progress, cancel_event,
                )
        except Exception as e:
            click.echo(f"    Batch mode unavailable ({e}), falling back to sequential...")
            # Fall through to existing sequential agentic loop

    if is_scan_request:
        if two_stage_scan:
            on_progress("Quick pass running...")
        user_message = _build_security_scan_user_message(
            user_message,
            two_stage=two_stage_scan,
        )

    messages = [
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": user_message,
                    "cache_control": {"type": "ephemeral"},
                }
            ],
        }
    ]

    click.secho("  Using direct API (Claude Code not found)", fg="cyan")
    on_progress("Using direct API mode...")

    result_text = ""
    deep_scan_progress_emitted = False

    for turn in range(MAX_TURNS):
        if cancel_event.is_set():
            return False, "Cancelled by user."

        # Prune if context is growing too large
        if _estimate_tokens(messages) > PRUNE_THRESHOLD_TOKENS:
            messages = _prune_message_history(messages)
            on_progress("Compacting context to stay within limits...")

        # Compute dynamic max_tokens to avoid context window overflow
        est_input = _estimate_tokens(messages)
        dynamic_max = _compute_max_tokens(
            est_input,
            SCAN_MAX_TOKENS if is_scan_request else MAX_TOKENS,
        )

        # Call the backend AI proxy
        body = {
            "model": MODEL,
            "max_tokens": dynamic_max,
            "system": [
                {
                    "type": "text",
                    "text": system_prompt,
                    "cache_control": {"type": "ephemeral"},
                }
            ],
            "messages": messages,
            "tools": tools,
        }
        idempotency_key = f"{cmd_id}_{turn}"

        try:
            response = await _call_with_retry_ctx(
                client,
                body,
                idempotency_key,
                is_scan_workflow=is_scan_request,
                on_progress=on_progress,
            )
        except InsufficientCreditsError as e:
            msg = f"Insufficient AI credits: {e}"
            on_progress(msg)
            return False, msg
        except RateLimitError as e:
            retry_after = _extract_retry_after_seconds(str(e))
            msg = _make_structured_error(
                error_code="rate_limited",
                error_message="Security scan was rate limited by the AI service.",
                retry_after_seconds=retry_after,
                suggested_next_action=(
                    "Retry shortly. If this persists, run a quick scan first and then deep scan on demand."
                ),
            )
            on_progress("Rate limited by AI service. Please retry shortly.")
            return False, msg
        except Exception as e:
            msg = f"AI proxy error: {e}"
            on_progress(msg)
            return False, msg

        assistant_content = response.get("content", [])
        stop_reason = response.get("stop_reason", "end_turn")

        # Append assistant turn
        messages.append({"role": "assistant", "content": assistant_content})

        # Report text blocks as progress
        for block in assistant_content:
            if block.get("type") == "text" and block.get("text", "").strip():
                text = block["text"].strip()
                # Send first few lines as progress
                for line in text.split("\n")[:5]:
                    line = line.strip()
                    if line:
                        on_progress(line[:200])

        # If no tool_use, we're done
        if stop_reason != "tool_use":
            # Extract final text from the response
            text_parts = [
                b["text"] for b in assistant_content
                if b.get("type") == "text" and b.get("text", "").strip()
            ]
            result_text = "\n".join(text_parts) if text_parts else "Task completed."
            return True, result_text

        # Execute tool calls
        tool_results = []
        for block in assistant_content:
            if block.get("type") != "tool_use":
                continue

            if cancel_event.is_set():
                return False, "Cancelled by user."

            tool_name = block["name"]
            tool_id = block["id"]
            tool_input = block.get("input", {})

            # Report tool progress
            label = TOOL_LABELS.get(tool_name) or BUILTIN_LABELS.get(tool_name) or f"Running {tool_name}..."
            on_progress(label)
            click.echo(f"    Tool: {tool_name}")
            if (
                two_stage_scan
                and not deep_scan_progress_emitted
                and tool_name == "security_scan"
                and str(tool_input.get("scan_profile", "")).lower() == "deep_security"
            ):
                on_progress("Escalating to deep scan due to high/critical findings...")
                deep_scan_progress_emitted = True

            try:
                result = await executor.execute(tool_name, tool_input)
                # Truncate very large results to avoid exceeding token limits
                max_len = 30000 if is_scan_request else 50000
                if len(result) > max_len:
                    result = result[:max_len] + "\n... (truncated)"
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": tool_id,
                    "content": result,
                })
                if is_scan_request and tool_name == "security_scan":
                    await asyncio.sleep(SCAN_TOOL_DELAY)
                elif not is_scan_request:
                    await asyncio.sleep(DEFAULT_TOOL_DELAY)
            except Exception as e:
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": tool_id,
                    "content": f"Error executing {tool_name}: {e}",
                    "is_error": True,
                })

        messages.append({"role": "user", "content": tool_results})
        inter_turn = SCAN_INTER_TURN_DELAY if is_scan_request else DEFAULT_INTER_TURN_DELAY
        await asyncio.sleep(inter_turn)

    # Exhausted turns
    return True, result_text or "Reached maximum turns. Task may be incomplete."


async def _call_with_retry(client, body: dict, idempotency_key: str) -> dict:
    """Compatibility wrapper for callers that don't pass scan context."""
    return await _call_with_retry_ctx(
        client,
        body,
        idempotency_key,
        is_scan_workflow=False,
        on_progress=None,
    )


def _extract_retry_after_seconds(message: str) -> int | None:
    m = re.search(r"(\d+)\s*s(?:ec(?:ond)?s?)?", message, re.IGNORECASE)
    if not m:
        return None
    try:
        return int(m.group(1))
    except ValueError:
        return None


async def _call_with_retry_ctx(
    client,
    body: dict,
    idempotency_key: str,
    *,
    is_scan_workflow: bool,
    on_progress,
) -> dict:
    """Call proxy_anthropic_message with retry on rate limits."""
    retries = RATE_LIMIT_RETRIES + (2 if is_scan_workflow else 0)
    for attempt in range(retries):
        try:
            return await client.proxy_anthropic_message(
                body=body,
                idempotency_key=idempotency_key,
            )
        except RateLimitError as e:
            if attempt == retries - 1:
                raise
            suggested = _extract_retry_after_seconds(str(e))
            if suggested and suggested > 0:
                delay = float(suggested) + random.uniform(1.0, 3.0)
            else:
                delay = RATE_LIMIT_BASE_DELAY * (2 ** attempt)
                if is_scan_workflow:
                    delay += 3
                delay += random.uniform(0.0, 2.0)
            click.echo(f"    Rate limited, retrying in {delay:.1f}s...")
            if on_progress:
                on_progress(f"Rate limited, retrying in {int(delay)}s...")
            await asyncio.sleep(delay)
    raise RateLimitError(
        "Rate limit retries exhausted.",
        status_code=429,
        method="POST",
        url="/ai/messages",
        response_text="Rate limit retries exhausted",
    )
