"""Analyze incoming request content to determine task tier and required capabilities.

The analyzer inspects the user messages, system prompt, and metadata to classify
the task into a `TaskTier` and extract required `Capability` flags.  This drives
the router's model selection — cheap models for trivial work, Opus only when needed.

Strategy hierarchy (checked in order):
  1. Explicit header  `X-Task-Tier: complex`  — user override
  2. Explicit model   `model: opus-4.6`       — passthrough
  3. Content signals  keywords, length, structure analysis
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field

from proxym.providers import Capability, TaskTier

# ── Signal patterns ──────────────────────────────────────────

# Patterns that indicate DEEP reasoning / complex multi-file work
_COMPLEX_PATTERNS: list[re.Pattern] = [
    re.compile(p, re.IGNORECASE) for p in [
        r"refactor\s+(the\s+)?(entire|whole|complete|full)\s+",
        r"re-?architect",
        r"migrat(e|ion)\s+(from|to)\s+",
        r"design\s+(a\s+)?(system|architecture|schema)",
        r"multi[- ]?file\s+(change|edit|refactor)",
        r"(complex|hard|tricky|subtle)\s+(bug|issue|problem|error)",
        r"(performance|memory)\s+(optim|profil|bottleneck)",
        r"security\s+(audit|review|vuln)",
        r"(implement|build|create)\s+(a\s+)?(complete|full|entire)\s+",
        r"(10|20|50|100)\+?\s+files?",
        r"across\s+(multiple|many|all)\s+(files|modules|packages)",
    ]
]

# Patterns for standard implementation tasks
_STANDARD_PATTERNS: list[re.Pattern] = [
    re.compile(p, re.IGNORECASE) for p in [
        r"(implement|write|create|add)\s+(a\s+)?(function|class|module|endpoint|test|component)",
        r"(fix|resolve|debug)\s+(this|the|a)\s+",
        r"(update|modify|change|edit)\s+",
        r"(add|remove|rename)\s+(a\s+)?(field|column|method|property)",
        r"(write|generate)\s+(unit\s+)?tests?\s+",
        r"(explain|review)\s+(this|the)\s+(code|function|class)",
        r"code\s+review",
    ]
]

# Patterns for trivial/operational tasks
_TRIVIAL_PATTERNS: list[re.Pattern] = [
    re.compile(p, re.IGNORECASE) for p in [
        r"^(what|how|why|when|where|who|which)\s+",
        r"(show|list|print|display|get)\s+",
        r"(rename|format|lint|prettify)",
        r"(autocomplete|suggest|complete)\s+",
        r"(translate|convert)\s+(this|the)\s+(to|into)\s+",
        r"(simple|quick|small|tiny|minor)\s+(fix|change|edit|update)",
        r"^(fix|add)\s+(a\s+)?(typo|import|comma|semicolon)",
    ]
]

# Thinking/reasoning signals
_THINKING_PATTERNS: list[re.Pattern] = [
    re.compile(p, re.IGNORECASE) for p in [
        r"(think|reason|analyze|consider)\s+(about|through|carefully)",
        r"(step[- ]by[- ]step|chain[- ]of[- ]thought)",
        r"(why\s+does|what\s+causes|root\s+cause)",
        r"(trade[- ]?offs?|pros?\s+and\s+cons?)",
        r"(compare|evaluate|assess)\s+(different|multiple|various)",
        r"(debug|diagnos|troubleshoot)\s+.{30,}",  # long debug descriptions
    ]
]

# Vision signals
_VISION_PATTERNS: list[re.Pattern] = [
    re.compile(p, re.IGNORECASE) for p in [
        r"(image|screenshot|picture|photo|diagram|ui|mockup)",
        r"(look at|see|view|analyze)\s+(this|the)\s+(image|screenshot)",
    ]
]


@dataclass
class AnalysisResult:
    """Result of content analysis."""

    tier: TaskTier
    capabilities: frozenset[Capability]
    confidence: float = 0.8
    reasoning: str = ""
    estimated_output_tokens: int = 2000
    signals: list[str] = field(default_factory=list)


def analyze_request(
    messages: list[dict] | str,
    *,
    explicit_tier: str | TaskTier | None = None,
    explicit_model: str | None = None,
    file_count: int = 0,
    context_tokens: int = 0,
    context_files: list[str] | None = None,
) -> AnalysisResult:
    """Analyze a chat completion request and return routing metadata.

    Parameters
    ----------
    messages : list[dict]
        OpenAI-format messages.
    explicit_tier : str | None
        Value of ``X-Task-Tier`` header (user override).
    explicit_model : str | None
        If user requested a specific model, skip analysis.
    file_count : int
        Number of files in the attached context (from delta buffer).
    context_tokens : int
        Approximate token count of the context window.
    """
    if isinstance(messages, str):
        messages = [{"role": "user", "content": messages}]
    if context_files is not None:
        file_count = len(context_files)

    # 1. Explicit overrides — short-circuit
    if explicit_tier:
        return _handle_explicit_tier(explicit_tier.value if isinstance(explicit_tier, TaskTier) else explicit_tier)
    if explicit_model:
        return _handle_explicit_model(explicit_model)

    # 2. Content-based analysis — compose helpers
    user_text = _extract_user_text(messages)
    system_text = _extract_system_text(messages)
    all_text = f"{system_text}\n{user_text}"

    signals: list[str] = []
    tier_scores: dict[TaskTier, float] = {t: 0.0 for t in TaskTier}
    caps: set[Capability] = set()

    _match_patterns(user_text, all_text, tier_scores, caps, signals)
    _detect_image_blocks(messages, caps, signals)
    _apply_heuristics(user_text, all_text, file_count, context_tokens,
                      tier_scores, caps, signals)

    tier = _select_tier(tier_scores, signals, len(user_text))

    return AnalysisResult(
        tier=tier,
        capabilities=frozenset(caps),
        confidence=min(0.95, 0.5 + tier_scores.get(tier, 0) * 0.1),
        reasoning=f"tier={tier.value}, top_signals={signals[:3]}",
        estimated_output_tokens=_estimate_output_tokens(tier),
        signals=signals,
    )


# ── Explicit override handlers ───────────────────────────────

def _handle_explicit_tier(explicit_tier: str) -> AnalysisResult:
    """Handle X-Task-Tier header override."""
    try:
        tier = TaskTier(explicit_tier.lower())
    except ValueError:
        tier = TaskTier.STANDARD
    return AnalysisResult(
        tier=tier,
        capabilities=frozenset(),
        confidence=1.0,
        reasoning=f"Explicit tier override: {explicit_tier}",
    )


def _handle_explicit_model(explicit_model: str) -> AnalysisResult:
    """Handle explicit model passthrough (skip content analysis)."""
    return AnalysisResult(
        tier=TaskTier.STANDARD,
        capabilities=frozenset(),
        confidence=1.0,
        reasoning=f"Explicit model requested: {explicit_model}",
    )


# ── Pattern matching ─────────────────────────────────────────

_PATTERN_GROUPS: list[tuple[list[re.Pattern], TaskTier, float, str | None]] = [
    (_COMPLEX_PATTERNS,  TaskTier.COMPLEX,        2.0, None),
    (_STANDARD_PATTERNS, TaskTier.STANDARD,        1.5, None),
    (_TRIVIAL_PATTERNS,  TaskTier.TRIVIAL,         1.5, None),
    (_THINKING_PATTERNS, TaskTier.DEEP_REASONING,  2.5, "THINKING"),
]


def _match_patterns(
    user_text: str,
    all_text: str,
    tier_scores: dict[TaskTier, float],
    caps: set[Capability],
    signals: list[str],
) -> None:
    """Score tier patterns and detect vision/thinking capabilities."""
    for patterns, tier, weight, cap_name in _PATTERN_GROUPS:
        for pat in patterns:
            if pat.search(user_text):
                tier_scores[tier] += weight
                signals.append(f"{tier.value}_pattern: {pat.pattern[:40]}")
                if cap_name:
                    caps.add(Capability[cap_name])

    for pat in _VISION_PATTERNS:
        if pat.search(all_text):
            caps.add(Capability.VISION)
            signals.append("vision_detected")


def _detect_image_blocks(
    messages: list[dict],
    caps: set[Capability],
    signals: list[str],
) -> None:
    """Check for image_url content blocks in messages."""
    for msg in messages:
        content = msg.get("content", "")
        if not isinstance(content, list):
            continue
        for block in content:
            if isinstance(block, dict) and block.get("type") == "image_url":
                caps.add(Capability.VISION)
                signals.append("image_content_block")


# ── Heuristics ───────────────────────────────────────────────

def _length_signals(text_len: int, tier_scores: dict, signals: list) -> None:
    """Score based on prompt length."""
    if text_len > 3000:
        tier_scores[TaskTier.STANDARD] += 1.0
        signals.append(f"long_prompt: {text_len} chars")
    if text_len > 8000:
        tier_scores[TaskTier.COMPLEX] += 1.5
        signals.append(f"very_long_prompt: {text_len} chars")


def _file_signals(file_count: int, tier_scores: dict, signals: list) -> None:
    """Score based on number of files in context."""
    if file_count > 5:
        tier_scores[TaskTier.COMPLEX] += 1.5
        signals.append(f"multi_file_context: {file_count}")
    elif file_count > 1:
        tier_scores[TaskTier.STANDARD] += 0.5


def _context_signals(context_tokens: int, tier_scores: dict, caps: set, signals: list) -> None:
    """Score based on context window size and detect code blocks."""
    if context_tokens > 100_000:
        caps.add(Capability.LONG_CONTEXT)
        tier_scores[TaskTier.COMPLEX] += 1.0
        signals.append(f"large_context: {context_tokens} tokens")


_TOOL_KEYWORDS = ("function call", "tool_use", "tool_call", "mcp")
_CODE_KEYWORDS = ("code", "function", "class", "module", "api", "endpoint", "test",
                  "bug", "error", "refactor", "implement", "variable", "method")
_REASONING_KEYWORDS = ("step by step", "reason", "reasoning", "think", "thinking", "explain")


def _keyword_signals(user_text: str, all_text: str, tier_scores: dict, caps: set, signals: list) -> None:
    """Detect code blocks, tool-use, and programming keywords."""
    code_blocks = len(re.findall(r"```", user_text))
    if code_blocks >= 4:
        tier_scores[TaskTier.STANDARD] += 1.0
        caps.add(Capability.CODE)
        signals.append(f"code_blocks: {code_blocks // 2}")

    lower = all_text.lower()
    if any(kw in lower for kw in _TOOL_KEYWORDS):
        caps.add(Capability.TOOL_USE)
        signals.append("tool_use_detected")
    if any(kw in lower for kw in _CODE_KEYWORDS):
        caps.add(Capability.CODE)
        tier_scores[TaskTier.STANDARD] += 1.5
        signals.append("code_keywords_detected")
    if any(kw in lower for kw in _REASONING_KEYWORDS):
        caps.add(Capability.THINKING)
    if "tool" in lower:
        caps.add(Capability.TOOL_USE)


def _apply_heuristics(
    user_text: str,
    all_text: str,
    file_count: int,
    context_tokens: int,
    tier_scores: dict[TaskTier, float],
    caps: set[Capability],
    signals: list[str],
) -> None:
    """Apply length, file-count, code-block, and keyword heuristics."""
    _length_signals(len(user_text), tier_scores, signals)
    _file_signals(file_count, tier_scores, signals)
    _context_signals(context_tokens, tier_scores, caps, signals)
    _keyword_signals(user_text, all_text, tier_scores, caps, signals)


# ── Tier selection ───────────────────────────────────────────

def _select_tier(
    tier_scores: dict[TaskTier, float],
    signals: list[str],
    text_len: int,
) -> TaskTier:
    """Pick winning tier from accumulated scores."""
    if not signals:
        return TaskTier.TRIVIAL if text_len < 50 else (TaskTier.OPERATIONAL if text_len < 500 else TaskTier.STANDARD)

    tier = max(tier_scores, key=lambda t: tier_scores[t])
    # If scores are close, prefer cheaper tier
    if tier_scores[tier] < 1.0:
        if any(s == "code_keywords_detected" for s in signals):
            return TaskTier.STANDARD
        return TaskTier.OPERATIONAL
    return tier


def _estimate_output_tokens(tier: TaskTier) -> int:
    """Estimate expected output tokens based on task tier."""
    if tier in (TaskTier.COMPLEX, TaskTier.DEEP_REASONING):
        return 6000
    if tier == TaskTier.STANDARD:
        return 3000
    return 1000


def _extract_user_text(messages: list[dict]) -> str:
    parts = []
    for msg in messages:
        if msg.get("role") != "user":
            continue
        content = msg.get("content", "")
        if isinstance(content, str):
            parts.append(content)
        elif isinstance(content, list):
            for block in content:
                if isinstance(block, dict) and block.get("type") == "text":
                    parts.append(block.get("text", ""))
    return "\n".join(parts)


def _extract_system_text(messages: list[dict]) -> str:
    for msg in messages:
        if msg.get("role") == "system":
            content = msg.get("content", "")
            return content if isinstance(content, str) else ""
    return ""
