"""LLM fallback — real execution via proxy API when tool binary is unavailable.

When the actual tool binary (aider, goose, codex, …) is not installed, this
module calls litellm.acompletion to generate code, then **parses fenced code
blocks** from the response and writes them to disk.  This makes the fallback
a real code-generation path rather than a text-only advisor.
"""
from __future__ import annotations

import asyncio
import os
import re
from pathlib import Path
from typing import Any

import structlog

from proxym.tools.adapters._result import ToolResult

logger = structlog.get_logger()

# ── System prompts per tool ──────────────────────────────────────────────────
#
# The prompt instructs the LLM to emit **fenced code blocks** with the target
# file path on the opening fence line so ``_extract_file_blocks`` can parse
# them.  Format:  ```lang path/to/file.ext\n<content>\n```

_FILE_BLOCK_INSTRUCTION = (
    "\n\nIMPORTANT OUTPUT FORMAT: For every file you create or modify, emit a "
    "fenced code block whose opening line contains the **relative file path** "
    "immediately after the language tag. Example:\n"
    "```typescript src/store.ts\n"
    "// full file content …\n"
    "```\n"
    "Always output the COMPLETE file content, not a diff.  This is critical — "
    "the orchestrator extracts these blocks and writes them to disk."
)

_TOOL_SYSTEM_PROMPTS: dict[str, str] = {
    "aider": (
        "You are Aider, an AI pair programmer. Analyze the codebase and implement "
        "changes as requested. Output the COMPLETE content of every file you create "
        "or modify inside fenced code blocks with the file path on the fence line."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "claude-code": (
        "You are Claude Code, Anthropic's coding agent. Analyze the project, "
        "reason step by step about the problem, then provide a complete solution. "
        "Output complete file contents in fenced code blocks with file paths."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "codex": (
        "You are OpenAI Codex CLI, an agentive terminal coding assistant. "
        "Implement the requested changes and output complete file contents."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "goose": (
        "You are Goose, an autonomous coding assistant by Block. Analyze the "
        "project, understand the task, write files, and run commands as needed."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "open-interpreter": (
        "You are Open Interpreter, an agent that executes code and shell commands. "
        "Implement the changes and output complete file contents."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "plandex": (
        "You are Plandex, a multi-file planning and coding agent. Plan the "
        "implementation across files, then output complete file contents."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "llm-cli": (
        "You are a universal LLM coding assistant. Implement the requested "
        "changes and output complete file contents."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "windsurf": (
        "You are Windsurf Cascade, an AI coding agent. Analyze the project "
        "structure, understand the task, and provide complete file implementations."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "cursor": (
        "You are Cursor Composer, an AI coding agent. Analyze the codebase, "
        "understand the task requirements, and provide complete file implementations."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "roo-code": (
        "You are Roo Code, an AI coding assistant. Analyze the project, "
        "understand the task, and provide complete file implementations."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "cline": (
        "You are Cline, an autonomous coding agent. Analyze the project "
        "structure, plan your approach, then provide complete file implementations."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "browser": (
        "You are a coding assistant. Analyze the task and provide complete file "
        "implementations in fenced code blocks with file paths."
        + _FILE_BLOCK_INSTRUCTION
    ),
    "vscode": (
        "You are a VS Code coding assistant. Analyze the project structure, "
        "understand the task, and provide complete file implementations."
        + _FILE_BLOCK_INSTRUCTION
    ),
}

# ── Code block parser + file writer ──────────────────────────────────────────

# Matches:  ```lang  path/to/file.ext\n<content>\n```
_CODE_BLOCK_RE = re.compile(
    r"```[\w]*\s+([\w./_-]+(?:\.[\w]+)+)\s*\n(.*?)```",
    re.DOTALL,
)


def _extract_file_blocks(content: str) -> list[tuple[str, str]]:
    """Extract (relative_path, file_content) pairs from fenced code blocks.

    Recognises the pattern:  ```lang path/to/file.ext\\n<body>\\n```
    Only paths with a file extension are accepted to avoid false positives.
    """
    return [(m.group(1).strip(), m.group(2)) for m in _CODE_BLOCK_RE.finditer(content)]


def _write_file_blocks(
    project_dir: str, blocks: list[tuple[str, str]],
) -> list[str]:
    """Write extracted code blocks to disk. Returns list of written paths."""
    written: list[str] = []
    base = Path(project_dir)
    for rel_path, body in blocks:
        # Safety: reject absolute paths and directory traversal
        if rel_path.startswith("/") or ".." in rel_path:
            logger.warning("llm_fallback_skip_path", path=rel_path, reason="unsafe")
            continue
        target = base / rel_path
        try:
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_text(body, encoding="utf-8")
            written.append(rel_path)
            logger.info("llm_fallback_wrote_file", path=rel_path,
                        size=len(body))
        except OSError as exc:
            logger.warning("llm_fallback_write_failed", path=rel_path,
                           error=str(exc))
    return written


# ── Project context builder ──────────────────────────────────────────────────

def _build_project_context(project_dir: str) -> str:
    """Build a project context string including directory listing and key file contents."""
    project_path = Path(project_dir)
    if not project_path.is_dir():
        return ""
    parts: list[str] = []
    try:
        entries = sorted(project_path.iterdir())
        file_list = []
        for e in entries[:50]:
            if e.name.startswith(".") and e.name not in (".env.example", ".gitignore"):
                continue
            if e.is_dir():
                sub_items = list(e.iterdir())[:20]
                file_list.append(f"  {e.name}/")
                for s in sorted(sub_items):
                    file_list.append(f"    {e.name}/{s.name}")
            else:
                file_list.append(f"  {e.name}")
        if file_list:
            parts.append(f"\n\nProject directory: {project_dir}\nFiles:\n" + "\n".join(file_list[:50]))
    except OSError:
        pass

    # Include key source files so the LLM has full context
    _KEY_FILES = ["spec.md", "package.json", "tsconfig.json"]
    for fname in _KEY_FILES:
        fpath = project_path / fname
        if fpath.is_file():
            try:
                text = fpath.read_text(encoding="utf-8")[:4000]
                parts.append(f"\n\n### {fname}\n```\n{text}\n```")
            except OSError:
                pass

    # Include source stubs so the LLM can see what needs implementing
    src_dir = project_path / "src"
    if src_dir.is_dir():
        for src_file in sorted(src_dir.glob("*.ts"))[:10]:
            try:
                text = src_file.read_text(encoding="utf-8")[:6000]
                parts.append(f"\n\n### src/{src_file.name}\n```typescript\n{text}\n```")
            except OSError:
                pass
    tests_dir = project_path / "tests"
    if tests_dir.is_dir():
        for test_file in sorted(tests_dir.glob("*.ts"))[:5]:
            try:
                text = test_file.read_text(encoding="utf-8")[:4000]
                parts.append(f"\n\n### tests/{test_file.name}\n```typescript\n{text}\n```")
            except OSError:
                pass

    return "".join(parts)


# ── Model resolution ─────────────────────────────────────────────────────────

def _resolve_litellm_model(resolved_model: str, settings) -> tuple[str, str]:
    """Resolve to a concrete litellm model + provider. Returns (model, provider).

    Uses ``providers.resolve_model()`` — the single canonical resolver.
    """
    _OR_FALLBACK = "openrouter/google/gemini-2.5-flash"
    try:
        from proxym.providers import resolve_model as _resolve
        spec = _resolve(resolved_model)
        configured = settings.available_providers()
        if spec and spec.provider in configured:
            return spec.litellm_model, spec.provider
        if spec and "openrouter" in configured:
            return f"openrouter/{spec.litellm_model}", "openrouter"
        return _OR_FALLBACK, "openrouter"
    except Exception:
        return _OR_FALLBACK, "openrouter"


# ── Main fallback entrypoint ─────────────────────────────────────────────────

async def _llm_fallback_execute(
    tool_name: str,
    prompt: str,
    ticket_id: str,
    project_dir: str,
    mode: str = "",
    model_override: str = "",
    timeout: float = 120,
) -> ToolResult:
    """Execute a task via LLM API when the tool binary is not available.

    Calls litellm.acompletion through the proxy's own routing, using the
    tool's assigned provider and model.  **Parses fenced code blocks from
    the response and writes them to disk**, making this a real code-generation
    path rather than a text-only advisor.
    """
    import time
    import litellm
    from proxym.config import get_settings
    from proxym.api.completions import _inject_provider_key

    settings = get_settings()
    t0 = time.monotonic()

    # Resolve model: use override if given, otherwise settings.default_model
    from proxym.providers import resolve_model as _resolve
    model_str = model_override if model_override else settings.default_model
    spec = _resolve(model_str)
    resolved_model = spec.litellm_model if spec else settings.resolve_model_alias(model_str)

    system_prompt = _TOOL_SYSTEM_PROMPTS.get(tool_name, _TOOL_SYSTEM_PROMPTS["browser"])
    if mode:
        system_prompt += f"\n\nMode: {mode}. Adapt your approach accordingly."

    project_context = _build_project_context(project_dir)

    messages = [
        {"role": "system", "content": system_prompt + project_context},
        {"role": "user", "content": prompt},
    ]

    litellm_model, provider = _resolve_litellm_model(resolved_model, settings)

    kwargs: dict[str, Any] = {
        "model": litellm_model,
        "messages": messages,
        "max_tokens": 16384,
        "temperature": 0.3,
    }
    _inject_provider_key(kwargs, provider)

    logger.info(
        "llm_fallback_execute",
        tool=tool_name,
        ticket_id=ticket_id,
        model=litellm_model,
        provider=provider,
        project_dir=project_dir,
        prompt_length=len(prompt),
    )

    try:
        response = await asyncio.wait_for(
            litellm.acompletion(**kwargs),
            timeout=timeout,
        )
        duration = time.monotonic() - t0

        # Extract response content
        content = ""
        if hasattr(response, "choices") and response.choices:
            msg = response.choices[0].message
            content = getattr(msg, "content", "") or ""

        # Extract usage
        usage = getattr(response, "usage", None)
        prompt_tokens = getattr(usage, "prompt_tokens", 0) if usage else 0
        completion_tokens = getattr(usage, "completion_tokens", 0) if usage else 0
        actual_model = getattr(response, "model", litellm_model)

        # ── Parse code blocks and write files to disk ──
        file_blocks = _extract_file_blocks(content)
        files_written: list[str] = []
        if file_blocks and project_dir:
            files_written = _write_file_blocks(project_dir, file_blocks)

        # Build detailed stdout log
        stdout_parts = [
            f"═══ proxym LLM Execution Log ═══",
            f"Tool: {tool_name} (via LLM fallback)",
            f"Ticket: {ticket_id}",
            f"Model: {actual_model}",
            f"Provider: {provider}",
            f"Mode: {mode or 'code'}",
            f"Duration: {duration:.2f}s",
            f"Tokens: {prompt_tokens} in / {completion_tokens} out",
            f"Project: {project_dir}",
            f"Files written: {len(files_written)} ({', '.join(files_written) if files_written else 'none'})",
            f"═══════════════════════════════════",
            f"",
            content,
        ]
        stdout = "\n".join(stdout_parts)

        metadata = {
            "adapter": "llm_fallback",
            "model": actual_model,
            "provider": provider,
            "model_requested": litellm_model,
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "total_tokens": prompt_tokens + completion_tokens,
            "system_prompt_length": len(system_prompt),
            "user_prompt_length": len(prompt),
            "response_length": len(content),
            "files_written": files_written,
            "code_blocks_found": len(file_blocks),
        }

        logger.info(
            "llm_fallback_done",
            tool=tool_name,
            ticket_id=ticket_id,
            model=actual_model,
            tokens=f"{prompt_tokens}+{completion_tokens}",
            duration=f"{duration:.1f}s",
            response_len=len(content),
            files_written=len(files_written),
        )

        return ToolResult(
            tool=tool_name,
            ticket_id=ticket_id,
            exit_code=0,
            stdout=stdout,
            stderr="",
            duration_s=duration,
            files_changed=files_written,
            execution_metadata=metadata,
        )

    except asyncio.TimeoutError:
        duration = time.monotonic() - t0
        return ToolResult(
            tool=tool_name, ticket_id=ticket_id,
            exit_code=-1,
            stderr=f"LLM call timed out after {timeout}s (model: {litellm_model})",
            duration_s=duration,
            execution_metadata={"adapter": "llm_fallback", "model": litellm_model, "error": "timeout"},
        )
    except Exception as exc:
        duration = time.monotonic() - t0
        logger.error("llm_fallback_error", tool=tool_name, error=str(exc)[:300])
        return ToolResult(
            tool=tool_name, ticket_id=ticket_id,
            exit_code=1,
            stderr=f"LLM fallback error: {exc}",
            duration_s=duration,
            execution_metadata={"adapter": "llm_fallback", "model": litellm_model, "error": str(exc)[:200]},
        )
