"""
Entroly Proxy Transform — Request/Response Transformation
==========================================================

Pure functions for parsing LLM API requests, formatting context blocks,
injecting optimized context, and computing optimal sampling temperature.

Includes EGTC (Entropy-Gap Temperature Calibration) — a novel algorithm
that derives the optimal LLM sampling temperature from information-theoretic
properties of the selected context fragments.

No state, no side effects.
"""

from __future__ import annotations

import copy
import hashlib
import math
from typing import Any, Dict, List, Optional

from .proxy_config import ProxyConfig, context_window_for_model


def detect_provider(path: str, headers: Dict[str, str]) -> str:
    """Detect whether this is an OpenAI or Anthropic request.

    Returns "openai" or "anthropic".
    """
    if "/v1/messages" in path:
        return "anthropic"
    if "x-api-key" in headers and "authorization" not in headers:
        return "anthropic"
    return "openai"


def extract_user_message(body: Dict[str, Any], provider: str) -> str:
    """Extract the latest user message text from the request body."""
    messages = body.get("messages", [])
    if not messages:
        return ""

    # Walk backwards to find the last user message
    for msg in reversed(messages):
        if msg.get("role") != "user":
            continue
        content = msg.get("content", "")
        if isinstance(content, str):
            return content
        # Anthropic / OpenAI content blocks: [{"type": "text", "text": "..."}]
        if isinstance(content, list):
            texts = [
                block.get("text", "")
                for block in content
                if isinstance(block, dict) and block.get("type") == "text"
            ]
            return " ".join(texts)
    return ""


def extract_model(body: Dict[str, Any]) -> str:
    """Extract the model name from the request body."""
    return body.get("model", "")


def compute_token_budget(model: str, config: ProxyConfig) -> int:
    """Compute the token budget for context injection.

    When ECDB (Entropy-Calibrated Dynamic Budget) is disabled, uses the
    static context_fraction.  When enabled, use compute_dynamic_budget()
    instead — this function remains for backwards compatibility.
    """
    window = context_window_for_model(model)
    return int(window * config.context_fraction)


# ══════════════════════════════════════════════════════════════════════
# ECDB — Entropy-Calibrated Dynamic Budget
# ══════════════════════════════════════════════════════════════════════
#
# Instead of a fixed 15% of context window, ECDB dynamically computes
# the optimal token budget from information-theoretic signals:
#
#   budget = base_fraction × window × query_factor × codebase_factor
#
# Query factor: vague queries need more context (broad search space),
# specific queries need less (precise target). Uses sigmoid on vagueness:
#
#   query_factor = 0.5 + 1.5 × σ(3.0 × (vagueness - 0.5))
#     At vagueness=0.0: factor ≈ 0.56 (specific → small budget)
#     At vagueness=0.5: factor ≈ 1.25 (average)
#     At vagueness=1.0: factor ≈ 1.94 (vague → large budget)
#
# Codebase factor: scales with project size (more fragments = more
# context may be relevant):
#
#   codebase_factor = min(2.0, 0.5 + total_fragments / 200)
#     At 100 fragments: factor = 1.0 (baseline)
#     At 500 fragments: factor = 2.0 (cap)
#
# The final budget is clamped to [min_budget, max_budget] to prevent
# both starvation and waste.
#
# Business value: saves 40-60% tokens on specific queries (the majority
# in real IDE usage) while allowing generous budgets for ambiguous tasks.
# ══════════════════════════════════════════════════════════════════════

def compute_dynamic_budget(
    model: str,
    config: "ProxyConfig",
    vagueness: float = 0.5,
    total_fragments: int = 0,
) -> int:
    """Compute token budget calibrated by query entropy and codebase size.

    ECDB: Entropy-Calibrated Dynamic Budget — replaces fixed 15% fraction
    with an information-theoretic budget that adapts to each request.

    All parameters are configurable via ProxyConfig (sourced from
    tuning_config.json → autotune daemon). No hardcoded constants.

    Args:
        model: LLM model name (for context window lookup).
        config: Proxy configuration.
        vagueness: Query vagueness score [0, 1] from query analysis.
        total_fragments: Number of fragments in the engine.

    Returns:
        Token budget (int), always in [ecdb_min_budget, ecdb_max_fraction × window].
    """
    window = context_window_for_model(model)
    base = config.context_fraction  # e.g., 0.15

    # Query factor: sigmoid on vagueness
    # Steepness and range are configurable via autotune
    v = max(0.0, min(1.0, vagueness))
    z = config.ecdb_sigmoid_steepness * (v - 0.5)
    query_factor = config.ecdb_sigmoid_base + config.ecdb_sigmoid_range / (1.0 + math.exp(-z))

    # Codebase factor: scales with project size
    codebase_factor = min(
        config.ecdb_codebase_cap,
        0.5 + max(total_fragments, 1) / config.ecdb_codebase_divisor,
    )

    # Raw budget
    raw = base * window * query_factor * codebase_factor

    # Clamp to bounds
    max_budget = int(window * config.ecdb_max_fraction)
    budget = max(config.ecdb_min_budget, min(max_budget, int(raw)))

    return budget


# ══════════════════════════════════════════════════════════════════════
# APA — Adaptive Prompt Augmentation
# ══════════════════════════════════════════════════════════════════════
#
# Three features that save tokens and help devs:
#   1. Calibrated per-language token estimation (replaces crude len/4)
#   2. Task-aware preamble (1-2 sentences from real signals)
#   3. Content-hash deduplication (removes redundant fragments)
# ══════════════════════════════════════════════════════════════════════

# Per-language chars-per-token ratios.
# Measured against cl100k_base (GPT-4/Claude) on real code corpora.
# Code tokens pack denser than English prose (which averages ~4.0).
_CHARS_PER_TOKEN: Dict[str, float] = {
    "python": 3.0,
    "rust": 3.5,
    "typescript": 3.1,
    "javascript": 3.1,
    "go": 3.4,
    "java": 3.2,
    "kotlin": 3.2,
    "ruby": 3.0,
    "c": 3.6,
    "cpp": 3.4,
    "sql": 3.3,
    "json": 2.8,  # lots of braces/quotes = fewer chars per token
    "yaml": 3.5,
    "toml": 3.5,
    "markdown": 4.0,  # closest to English prose
    "bash": 3.2,
}
_DEFAULT_CHARS_PER_TOKEN = 3.3  # average across code languages


def calibrated_token_count(content: str, source: str = "") -> int:
    """Estimate token count using per-language char/token ratios.

    More accurate than len/4 — saves 15-25% of wasted context budget.
    """
    if not content:
        return 0
    lang = _infer_language(source)
    ratio = _CHARS_PER_TOKEN.get(lang, _DEFAULT_CHARS_PER_TOKEN)
    return max(1, int(len(content) / ratio))


def _build_preamble(
    task_type: str,
    vagueness: float,
    security_count: int,
) -> str:
    """Build a task-aware preamble — 0-2 sentences of actionable guidance.

    Only emits when signals warrant it. Returns empty string otherwise.
    This gives the LLM information it genuinely doesn't have about the
    context selection and any issues found in the code.
    """
    parts: List[str] = []

    # Security findings — information the LLM truly doesn't have
    if security_count > 0:
        noun = "issue" if security_count == 1 else "issues"
        parts.append(
            f"⚠ SAST found {security_count} {noun} in the provided code. "
            f"Address these before other changes."
        )

    # High vagueness — prompt the model to seek clarification
    if vagueness > 0.6:
        parts.append(
            "The query is ambiguous — ask the developer to clarify scope "
            "before making broad changes."
        )

    # Task-specific hint (only for strong task signals, not Unknown)
    _TASK_HINTS: Dict[str, str] = {
        "BugTracing": "Focus on error propagation paths and edge cases.",
        "Refactoring": "Preserve existing behavior exactly; verify call sites.",
        "Testing": "Cover edge cases and error paths, not just happy paths.",
        "CodeReview": "Flag correctness issues before style suggestions.",
    }
    hint = _TASK_HINTS.get(task_type, "")
    if hint:
        parts.append(hint)

    return " ".join(parts)


def _deduplicate_fragments(
    fragments: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
    """Remove duplicate fragments by content hash.

    Saves 10-20% tokens in multi-turn sessions where the same file
    gets re-ingested across turns.
    """
    seen: set = set()
    unique: List[Dict[str, Any]] = []
    for frag in fragments:
        content = frag.get("preview", frag.get("content", ""))
        # Hash first 256 chars — enough to identify duplicates, fast
        h = hashlib.md5(content[:256].encode("utf-8", errors="replace")).hexdigest()
        if h not in seen:
            seen.add(h)
            unique.append(frag)
    return unique


def format_context_block(
    fragments: List[Dict[str, Any]],
    security_issues: List[str],
    ltm_memories: List[Dict[str, Any]],
    refinement_info: Optional[Dict[str, Any]],
    *,
    task_type: str = "Unknown",
    vagueness: float = 0.0,
) -> str:
    """Format selected fragments into a context string for injection.

    Returns empty string if there's nothing to inject.
    """
    if not fragments and not ltm_memories:
        return ""

    # Deduplicate fragments (saves tokens in multi-turn sessions)
    fragments = _deduplicate_fragments(fragments)

    parts: List[str] = []
    parts.append("--- Relevant Code Context (auto-selected by entroly) ---")
    parts.append("")

    # Task-aware preamble (conditional — only when signals warrant it)
    preamble = _build_preamble(task_type, vagueness, len(security_issues))
    if preamble:
        parts.append(preamble)
        parts.append("")

    # Refinement info (if query was refined)
    if refinement_info:
        parts.append(
            f"[Query refined: \"{refinement_info.get('original', '')}\" "
            f"→ \"{refinement_info.get('refined', '')}\" "
            f"(vagueness: {refinement_info.get('vagueness', 0):.2f})]"
        )
        parts.append("")

    # Code fragments — group by resolution (full first, then skeleton, then references)
    full_frags = [f for f in fragments if f.get("variant", "full") == "full"]
    skel_frags = [f for f in fragments if f.get("variant") == "skeleton"]
    ref_frags = [f for f in fragments if f.get("variant") == "reference"]

    for frag in full_frags:
        source = frag.get("source", "unknown")
        relevance = frag.get("relevance", 0)
        tokens = frag.get("token_count", 0)
        content = frag.get("preview", frag.get("content", ""))

        # Infer language from source for code fence
        lang = _infer_language(source)
        parts.append(f"## {source} (relevance: {relevance:.2f}, {tokens} tokens)")
        parts.append(f"```{lang}")
        parts.append(content.rstrip())
        parts.append("```")
        parts.append("")

    # Skeleton fragments (structural outlines for budget-constrained files)
    if skel_frags:
        parts.append("## Structural Outlines (signatures only)")
        for frag in skel_frags:
            source = frag.get("source", "unknown")
            tokens = frag.get("token_count", 0)
            content = frag.get("preview", frag.get("content", ""))
            lang = _infer_language(source)
            parts.append(f"### {source} ({tokens} tokens)")
            parts.append(f"```{lang}")
            parts.append(content.rstrip())
            parts.append("```")
            parts.append("")

    # Reference fragments (file existence awareness, minimal tokens)
    if ref_frags:
        parts.append("## Also relevant (not shown in full)")
        ref_lines = [f"- {f.get('source', 'unknown')}" for f in ref_frags]
        parts.extend(ref_lines)
        parts.append("")

    # Long-term memories (cross-session)
    if ltm_memories:
        parts.append("## Cross-Session Memory")
        for mem in ltm_memories:
            retention = mem.get("retention", 0)
            content = mem.get("content", "")
            parts.append(f"- [retention: {retention:.2f}] {content[:200]}")
        parts.append("")

    # Security warnings
    if security_issues:
        parts.append("## Security Warnings")
        for issue in security_issues:
            parts.append(f"- {issue}")
        parts.append("")

    parts.append("--- End Context ---")
    return "\n".join(parts)


def format_hierarchical_context(
    hcc_result: Dict[str, Any],
    security_issues: List[str],
    ltm_memories: List[Dict[str, Any]],
    refinement_info: Optional[Dict[str, Any]],
    *,
    task_type: str = "Unknown",
    vagueness: float = 0.0,
) -> str:
    """Format hierarchical compression result into context for LLM injection.

    Three-level structure:
      L1: Skeleton map — one line per file, entire codebase visible
      L2: Dep-graph cluster — expanded skeletons for query-connected files
      L3: Full content — knapsack-optimal fragments at full resolution

    The LLM sees the ENTIRE codebase structure (L1), detailed structure
    of relevant neighborhood (L2), and full code where it matters (L3).
    """
    if hcc_result.get("status") == "empty":
        return ""

    parts: List[str] = []
    parts.append("--- Relevant Code Context (auto-selected by entroly) ---")
    parts.append("")

    # Task-aware preamble (conditional — only when signals warrant it)
    preamble = _build_preamble(task_type, vagueness, len(security_issues))
    if preamble:
        parts.append(preamble)
        parts.append("")

    # Refinement info
    if refinement_info:
        parts.append(
            f'[Query refined: "{refinement_info.get("original", "")}" '
            f'→ "{refinement_info.get("refined", "")}" '
            f'(vagueness: {refinement_info.get("vagueness", 0):.2f})]'
        )
        parts.append("")

    # ── Level 1: Skeleton Map (entire codebase overview) ──
    l1_map = hcc_result.get("level1_map", "")
    if l1_map:
        coverage = hcc_result.get("coverage", {})
        l1_files = coverage.get("level1_files", 0) if isinstance(coverage, dict) else 0
        parts.append(f"## Codebase Overview ({l1_files} files)")
        parts.append("```")
        parts.append(l1_map.rstrip())
        parts.append("```")
        parts.append("")

    # ── Level 2: Dep-Graph Cluster (structural context) ──
    l2_cluster = hcc_result.get("level2_cluster", "")
    if l2_cluster:
        coverage = hcc_result.get("coverage", {})
        l2_files = coverage.get("level2_cluster_files", 0) if isinstance(coverage, dict) else 0
        parts.append(f"## Related Code Structure ({l2_files} connected files)")
        parts.append(l2_cluster.rstrip())
        parts.append("")

    # ── Level 3: Full Content (knapsack-optimal) ──
    l3_frags = hcc_result.get("level3_fragments", [])
    if l3_frags:
        parts.append(f"## Full Code ({len(l3_frags)} fragments)")
        for frag in l3_frags:
            source = frag.get("source", "unknown")
            tokens = frag.get("token_count", 0)
            content = frag.get("content", frag.get("preview", ""))
            lang = _infer_language(source)
            parts.append(f"### {source} ({tokens} tokens)")
            parts.append(f"```{lang}")
            parts.append(content.rstrip())
            parts.append("```")
            parts.append("")

    # Long-term memories (cross-session)
    if ltm_memories:
        parts.append("## Cross-Session Memory")
        for mem in ltm_memories:
            retention = mem.get("retention", 0)
            content = mem.get("content", "")
            parts.append(f"- [retention: {retention:.2f}] {content[:200]}")
        parts.append("")

    # Security warnings
    if security_issues:
        parts.append("## Security Warnings")
        for issue in security_issues:
            parts.append(f"- {issue}")
        parts.append("")

    parts.append("--- End Context ---")
    return "\n".join(parts)


def inject_context_openai(
    body: Dict[str, Any], context_text: str
) -> Dict[str, Any]:
    """Inject optimized context into an OpenAI chat completion request.

    Inserts a system message at the beginning of the messages array.
    If there's already a system message at position 0, the context is
    prepended to its content.
    """
    body = copy.deepcopy(body)
    messages = body.get("messages", [])

    if messages and messages[0].get("role") == "system":
        # Prepend context to existing system message
        existing = messages[0].get("content", "")
        messages[0]["content"] = f"{context_text}\n\n{existing}"
    else:
        # Insert new system message at position 0
        messages.insert(0, {"role": "system", "content": context_text})

    body["messages"] = messages
    return body


def inject_context_anthropic(
    body: Dict[str, Any], context_text: str
) -> Dict[str, Any]:
    """Inject optimized context into an Anthropic messages request.

    Uses the top-level "system" field. Appends to existing system content.
    """
    body = copy.deepcopy(body)
    existing = body.get("system", "")

    if isinstance(existing, str):
        if existing:
            body["system"] = f"{context_text}\n\n{existing}"
        else:
            body["system"] = context_text
    elif isinstance(existing, list):
        # System content blocks: prepend as a text block
        body["system"] = [{"type": "text", "text": context_text}] + existing
    else:
        body["system"] = context_text

    return body


# ══════════════════════════════════════════════════════════════════════
# EGTC v2 — Entropy-Gap Temperature Calibration
# ══════════════════════════════════════════════════════════════════════
#
# Two-stage algorithm for deriving the optimal LLM sampling temperature
# from information-theoretic properties of the input context.
#
# STAGE 1 — Fisher Base Temperature
# ──────────────────────────────────
# Directly computes the optimal temperature from the Fisher information
# of the softmax distribution. For logits ℓ and temperature τ:
#
#   p(y|ℓ,τ) = softmax(ℓ/τ)
#
# The Fisher information w.r.t. τ is:
#
#   I(τ) = (1/τ⁴) · Var_p(ℓ)
#
# Maximising I(τ) — the τ at which the distribution is most informative
# about the model's internal ranking — yields:
#
#   τ* = Var(ℓ)^(1/4)
#
# We don't observe logits, but Var(ℓ) is proportional to the entropy of
# the input context (fragments with higher Shannon entropy produce more
# varied logit distributions across the vocabulary). This gives:
#
#   τ_fisher = (H_c + ε)^(1/4) × scale
#
# where ε = 0.01 prevents zero-entropy collapse, and scale = 0.55 is
# calibrated so that H_c = 0.5 (typical code) → τ ≈ 0.46.
#
# STAGE 2 — Sigmoid Correction
# ─────────────────────────────
# The Fisher base captures how context complexity maps to optimal τ.
# But other signals (vagueness, sufficiency, task type, dispersion)
# modulate the result. These are combined via a sigmoid correction:
#
#   z = α·V − γ·S + δ_task − ε·D + bias
#   correction = 0.3 + 1.4 × σ(z)     → range [0.3, 1.7]
#   τ_egtc = clamp(τ_fisher × correction, τ_min, τ_max)
#
# Note: H_c is NOT in the sigmoid (it's in the Fisher base — no
# double-counting). The correction purely handles non-Fisher signals.
#
# STAGE 3 — Turn-Trajectory Convergence (optional)
# ─────────────────────────────────────────────────
# Within a conversation, early turns are exploratory (high τ) and later
# turns should converge toward determinism as the task crystallises:
#
#   convergence = 1 - (1 - c_min) × (1 - exp(-λ × turn))
#   τ_final = max(τ_min, τ_egtc × convergence)
#
# c_min = 0.6 → steady-state is 60% of EGTC recommendation.
# λ = 0.07 → half-convergence at ~10 turns (ln(2)/10 ≈ 0.069).
#
# ─── Key Properties ─────────────────────────────────────────────────
# 1. Monotonic: ↑vagueness → ↑τ,  ↑sufficiency → ↓τ
# 2. Bounded: τ ∈ [0.15, 0.95] always
# 3. Zero-cost: all signals already computed in Rust
# 4. Model-agnostic: works at proxy layer, no logit access needed
# 5. User-respecting: never overrides explicit temperature setting
# ═══════════════════════════════════════════════════════════════════════

# Task type → intrinsic temperature bias (δ_task).
# Negative = wants lower τ (precision). Positive = wants higher τ (creativity).
_TASK_TEMPERATURE_BIAS: Dict[str, float] = {
    "BugTracing":      -0.8,   # deterministic: reproduce → locate → fix
    "Refactoring":     -0.4,   # structured: preserve semantics exactly
    "Testing":         -0.3,   # systematic: generate correct assertions
    "CodeReview":      -0.2,   # analytical: identify real issues
    "CodeGeneration":   0.3,   # creative: multiple valid implementations
    "Documentation":    0.5,   # expressive: natural language fluency
    "Exploration":      0.7,   # divergent: broad hypothesis generation
    "Unknown":          0.0,   # neutral default
}

# Sigmoid correction coefficients (tunable via autotune daemon).
# α: vagueness → raises τ (missing information demands exploration)
# γ: sufficiency → lowers τ (rich context constrains the answer)
# ε_d: dispersion → lowers τ (heterogeneous context needs selective focus)
_ALPHA = 1.6
_GAMMA = 1.2
_EPS_D = 0.5

# Fisher base scale: calibrated so H_c=0.5 → τ_fisher ≈ 0.46
_FISHER_SCALE = 0.55
_FISHER_EPS = 0.01  # prevents (0)^(1/4) collapse

# Temperature bounds
_TAU_MIN = 0.15
_TAU_MAX = 0.95

# Sigmoid correction range: [0.3, 1.7]
# Low correction (0.3) = strong precision demand → τ drops to 30% of Fisher
# High correction (1.7) = strong exploration demand → τ rises to 170% of Fisher
_CORRECTION_MIN = 0.3
_CORRECTION_RANGE = 1.4  # _CORRECTION_MIN + _CORRECTION_RANGE = 1.7

# Trajectory convergence defaults
_TRAJECTORY_C_MIN = 0.6
_TRAJECTORY_LAMBDA = 0.07


def compute_optimal_temperature(
    vagueness: float,
    fragment_entropies: List[float],
    sufficiency: float,
    task_type: str,
    *,
    fisher_scale: float = _FISHER_SCALE,
    alpha: float = _ALPHA,
    gamma: float = _GAMMA,
    eps_d: float = _EPS_D,
) -> float:
    """Compute the information-theoretically optimal sampling temperature.

    EGTC v2: Two-stage Fisher base + sigmoid correction.

    Args:
        vagueness: Query vagueness score [0, 1].
        fragment_entropies: Shannon entropy scores [0, 1] per fragment.
        sufficiency: Knapsack fill ratio [0, 1].
        task_type: Task classification from Rust TaskType::classify.
        fisher_scale: Fisher base scaling factor (tunable).
        alpha: Vagueness coefficient (tunable).
        gamma: Sufficiency coefficient (tunable).
        eps_d: Entropy dispersion coefficient (tunable).

    Returns:
        Optimal temperature τ* ∈ [τ_min, τ_max].
    """
    # ── Clamp inputs ──
    v = max(0.0, min(1.0, vagueness))
    s = max(0.0, min(1.0, sufficiency))

    # ── Mean context entropy (H_c) ──
    if fragment_entropies:
        h_c = max(0.0, sum(fragment_entropies) / len(fragment_entropies))
    else:
        h_c = 0.0

    # ── STAGE 1: Fisher base temperature ──
    # τ_fisher = (H_c + ε)^(1/4) × scale
    # From I(τ) = Var(ℓ)/τ⁴ → τ* = Var(ℓ)^(1/4) ∝ H^(1/4)
    tau_fisher = (h_c + _FISHER_EPS) ** 0.25 * fisher_scale

    # ── Task bias (δ) ──
    delta = _TASK_TEMPERATURE_BIAS.get(task_type, 0.0)

    # ── Entropy dispersion (D) — std dev of fragment entropies ──
    if len(fragment_entropies) >= 2:
        variance = sum((h - h_c) ** 2 for h in fragment_entropies) / len(fragment_entropies)
        d = math.sqrt(variance)
    else:
        d = 0.0

    # ── STAGE 2: Sigmoid correction ──
    # z combines non-Fisher signals; H_c is NOT here (already in Fisher base)
    bias = -0.3  # slight precision bias (most IDE queries want determinism)
    z = alpha * v - gamma * s + delta - eps_d * d + bias
    sigma_z = 1.0 / (1.0 + math.exp(-z))
    correction = _CORRECTION_MIN + _CORRECTION_RANGE * sigma_z

    tau = tau_fisher * correction
    tau = max(_TAU_MIN, min(_TAU_MAX, tau))

    return round(tau, 4)


def apply_trajectory_convergence(
    tau: float,
    turn_count: int,
    c_min: float = _TRAJECTORY_C_MIN,
    lam: float = _TRAJECTORY_LAMBDA,
) -> float:
    """Apply turn-trajectory temperature convergence.

    As a conversation progresses, the task becomes better defined and
    the model should converge toward more deterministic output.

    convergence_factor = 1 - (1 - c_min) × (1 - exp(-λ × turn))

    At turn 0: factor = 1.0 (full EGTC temperature)
    At turn ∞: factor = c_min (steady-state compression)
    Half-convergence at turn ≈ ln(2)/λ ≈ 10 turns (for λ=0.07)

    Args:
        tau: EGTC-computed temperature.
        turn_count: Number of turns in current session.
        c_min: Minimum convergence factor (steady-state).
        lam: Convergence rate.

    Returns:
        Converged temperature, never below τ_min.
    """
    if turn_count <= 0:
        return tau
    convergence = 1.0 - (1.0 - c_min) * (1.0 - math.exp(-lam * turn_count))
    return max(_TAU_MIN, round(tau * convergence, 4))


def apply_temperature(
    body: Dict[str, Any],
    tau: float,
) -> Dict[str, Any]:
    """Set the temperature in the request body, respecting user overrides.

    If the user explicitly set a temperature, we DON'T override it.
    We only inject when temperature is absent (the common case in IDE usage).
    """
    if "temperature" in body:
        return body  # user explicitly set it — respect their choice
    body = copy.deepcopy(body)
    body["temperature"] = tau
    return body


_LANG_MAP = {
    ".py": "python",
    ".pyi": "python",
    ".rs": "rust",
    ".js": "javascript",
    ".ts": "typescript",
    ".tsx": "typescript",
    ".jsx": "javascript",
    ".go": "go",
    ".java": "java",
    ".kt": "kotlin",
    ".rb": "ruby",
    ".sql": "sql",
    ".sh": "bash",
    ".yaml": "yaml",
    ".yml": "yaml",
    ".toml": "toml",
    ".json": "json",
    ".md": "markdown",
    ".c": "c",
    ".cpp": "cpp",
    ".h": "c",
    ".hpp": "cpp",
}


def _infer_language(source: str) -> str:
    """Infer programming language from a source identifier like 'file:utils.py'."""
    s = source.lower()
    for ext, lang in _LANG_MAP.items():
        if s.endswith(ext):
            return lang
    return ""
