"""Shared helpers for running Claude Code with MCP tools."""

import json
import os
import subprocess
import sys
import threading
import time

import click


# Friendly labels for MCP tools
TOOL_LABELS = {
    "generate_tests": "Analyzing your code and generating tests...",
    "generate_playwright_tests": "Analyzing your code and generating tests...",
    "run_and_fix_tests": "Running and auto-fixing tests...",
    "run_and_fix_playwright_tests": "Running and auto-fixing tests...",
    "push_tests_to_maeris": "Pushing tests to Maeris...",
    "push_to_maeris": "Pushing results to Maeris...",
    "security_scan": "Scanning for security issues...",
    "api_scan": "Scanning for APIs...",
}

# Friendly labels for Claude Code's built-in tools
BUILTIN_LABELS = {
    "Read": "Reading project files...",
    "Glob": "Scanning project structure...",
    "Grep": "Searching codebase...",
    "Write": "Writing files...",
    "Edit": "Updating files...",
    "Bash": "Running a command...",
}

# Heartbeat messages — rotate through these during long silences
HEARTBEAT_MESSAGES = [
    "⏳ Still working... analyzing code",
    "⏳ Still working... reading files",
    "⏳ Still working... this may take a minute",
    "⏳ Still working... almost there",
]


def ensure_mcp_json(target_dir: str) -> "tuple[bool, str | None]":
    """Write a .mcp.json in the target directory so Claude Code can discover
    the maeris MCP server.

    Always writes with current env vars (MAERIS_CONFIG_DIR, MAERIS_API_URL)
    to avoid stale credentials from a previous run.

    Returns (was_new, original_content) — was_new=True if no prior file existed;
    original_content holds the previous bytes so we can restore on cleanup.
    """
    mcp_path = os.path.join(target_dir, ".mcp.json")

    # Save original for restoration in cleanup
    original: str | None = None
    if os.path.exists(mcp_path):
        with open(mcp_path) as f:
            original = f.read()

    env_block: dict[str, str] = {}
    config_dir = os.environ.get("MAERIS_CONFIG_DIR")
    if config_dir:
        env_block["MAERIS_CONFIG_DIR"] = config_dir
    api_url = os.environ.get("MAERIS_API_URL")
    if api_url:
        env_block["MAERIS_API_URL"] = api_url

    config = {
        "mcpServers": {
            "maeris": {
                "command": "maeris",
                "args": ["serve"],
                **({"env": env_block} if env_block else {}),
            }
        }
    }
    with open(mcp_path, "w") as f:
        json.dump(config, f, indent=2)
    return (original is None, original)


def cleanup_mcp_json(target_dir: str, was_new: bool, original: "str | None"):
    """Restore or remove .mcp.json after Claude Code finishes."""
    mcp_path = os.path.join(target_dir, ".mcp.json")
    if was_new:
        try:
            os.remove(mcp_path)
        except OSError:
            pass
    elif original is not None:
        with open(mcp_path, "w") as f:
            f.write(original)



def run_claude_streaming(claude_bin: str, prompt: str, model: str, cwd: str,
                         system_prompt: str = "", on_progress=None,
                         cancel_event: threading.Event | None = None) -> "tuple[int, str]":
    """Run Claude Code in non-interactive stream-json mode.

    Args:
        claude_bin: Path to the claude binary
        prompt: The prompt to send
        model: Model name
        cwd: Working directory
        system_prompt: Optional system prompt to append
        on_progress: Optional callback(line: str) called for each progress line
        cancel_event: Optional threading.Event — when set, kills the subprocess

    Returns (exit_code, result_text) tuple.
    """
    cmd = [
        claude_bin, "-p", prompt,
        "--model", model,
        "--verbose",
        "--output-format", "stream-json",
        "--dangerously-skip-permissions",
    ]
    if system_prompt:
        cmd.extend(["--append-system-prompt", system_prompt])

    process = subprocess.Popen(
        cmd,
        stdin=subprocess.DEVNULL,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        bufsize=1,
        cwd=cwd,
    )

    # Track last output time for heartbeat thread
    last_output = [time.time()]
    done_event = threading.Event()

    def _echo(msg: str):
        """Echo to terminal + send to backend via on_progress callback."""
        click.echo(msg, err=True)
        last_output[0] = time.time()
        if on_progress:
            on_progress(msg)

    def _echo_local(msg: str):
        """Echo to terminal only — not sent to backend/UI."""
        click.echo(msg, err=True)
        last_output[0] = time.time()

    # Heartbeat thread — only prints locally, not sent to backend
    def _heartbeat():
        idx = 0
        while not done_event.is_set():
            done_event.wait(timeout=30)
            if done_event.is_set():
                break
            if time.time() - last_output[0] >= 30:
                hb = HEARTBEAT_MESSAGES[idx % len(HEARTBEAT_MESSAGES)]
                _echo_local(hb)
                idx += 1

    threading.Thread(target=_heartbeat, daemon=True).start()

    # Capture stderr — show errors, suppress noise
    def _drain_stderr():
        for line in process.stderr:
            stripped = line.rstrip()
            if stripped and ("error" in stripped.lower() or "warning" in stripped.lower()):
                _echo(stripped)

    threading.Thread(target=_drain_stderr, daemon=True).start()

    # Cancel watcher — kills the process when cancel_event is set
    if cancel_event:
        def _cancel_watcher():
            cancel_event.wait()
            if process.poll() is None:
                _echo_local("Cancellation requested — stopping Claude Code...")
                process.terminate()
                try:
                    process.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    process.kill()
        threading.Thread(target=_cancel_watcher, daemon=True).start()

    # Parse stream-json events
    result_text_parts: list = []

    for line in process.stdout:
        line = line.strip()
        if not line:
            continue
        try:
            event = json.loads(line)
        except (json.JSONDecodeError, ValueError):
            _echo(line)
            continue

        etype = event.get("type", "")

        if etype == "tool_use":
            tool = event.get("tool", "")
            label = TOOL_LABELS.get(tool) or BUILTIN_LABELS.get(tool)
            if label:
                _echo(label)

        elif etype == "tool_result":
            content = event.get("content", "")
            if not isinstance(content, str):
                continue
            text = content.strip()
            if not text:
                continue
            for result_line in text.split("\n"):
                rl = result_line.strip()
                if not rl:
                    continue
                rl_lower = rl.lower()
                if any(kw in rl_lower for kw in [
                    "found", "file", "test", "generat", "creat", "writ",
                    "pass", "fail", "error", "scan", "vulnerab", "api",
                    "batch", "analyz", "read", "process", "running",
                    "fixing", "pushing", "saved", "✓", "✗",
                ]):
                    _echo(rl[:200])
                    break

        elif etype == "result":
            text = event.get("result", "") or event.get("text", "") or ""
            if isinstance(text, str) and text.strip():
                result_text_parts.append(text.strip())
                for result_line in text.strip().split("\n"):
                    rl = result_line.strip()
                    if rl:
                        _echo(rl[:200])

        elif etype == "error":
            msg = event.get("error", {})
            if isinstance(msg, dict):
                _echo(f"Error: {msg.get('message', str(msg))}")
            else:
                _echo(f"Error: {msg}")

    done_event.set()
    result_text = "\n".join(result_text_parts)
    return process.wait(), result_text
