"""Adaptive parameter selection for dual retrieval requests."""

from __future__ import annotations

from dataclasses import dataclass
import math
import re
from typing import List, Optional

from hieromem_core.memory import Memory


@dataclass(slots=True)
class ParameterDecision:
    """Container describing the final retrieval weights and rationale."""

    alpha_dense: float
    beta_symbolic: float
    lambda_decay: float
    insights: List[str]
    mode: str


class AdaptiveParameterAdvisor:
    """Heuristic controller that selects α/β/λ based on context."""

    QUESTION_PREFIXES = (
        "who",
        "what",
        "when",
        "where",
        "why",
        "how",
        "which",
        "did",
        "does",
        "is",
        "are",
        "can",
        "should",
    )
    CONCEPTUAL_KEYWORDS = (
        "reflect",
        "feeling",
        "insight",
        "theme",
        "story",
        "summar",
        "idea",
        "concept",
        "relationship",
        "reason",
    )
    RECENCY_CUES = (
        "today",
        "yesterday",
        "earlier",
        "recent",
        "last time",
        "remember when",
    )

    def __init__(self, memory: Memory):
        self.memory = memory

    # ------------------------------------------------------------------ #
    def recommend(
        self,
        *,
        query: Optional[str],
        alpha_override: Optional[float] = None,
        beta_override: Optional[float] = None,
        lambda_override: Optional[float] = None,
        lam_baseline: Optional[float] = None,
    ) -> ParameterDecision:
        stats = self.memory.stats()
        alpha = 0.6
        beta = 0.4
        lam_source = float(
            self.memory.lam) if lam_baseline is None else float(lam_baseline)
        lam = lam_source
        insights: List[str] = []

        alpha_free = alpha_override is None
        beta_free = beta_override is None

        if alpha_override is not None:
            alpha = float(alpha_override)
            insights.append(f"α manually set to {alpha:.2f} by user override.")
        if beta_override is not None:
            beta = float(beta_override)
            insights.append(f"β manually set to {beta:.2f} by user override.")
        if lambda_override is not None:
            lam = float(lambda_override)
            insights.append(f"λ manually set to {lam:.2f} by user override.")

        query_lower = (query or "").strip().lower()
        if query_lower:
            if self._looks_like_question(query_lower):
                adjusted = False
                if beta_free:
                    beta += 0.12
                    adjusted = True
                if alpha_free:
                    alpha -= 0.08
                    adjusted = True
                if adjusted:
                    insights.append(
                        "Detected factoid/question — increasing symbolic weight β.")
            if len(query_lower) > 160:
                adjusted = False
                if alpha_free:
                    alpha += 0.08
                    adjusted = True
                if beta_free:
                    beta -= 0.05
                    adjusted = True
                if adjusted:
                    insights.append(
                        "Long-form prompt — emphasising dense embeddings α.")
            if self._contains_any(query_lower, self.CONCEPTUAL_KEYWORDS):
                adjusted = False
                if alpha_free:
                    alpha += 0.07
                    adjusted = True
                if beta_free:
                    beta -= 0.05
                    adjusted = True
                if adjusted:
                    insights.append(
                        "Conceptual language detected — leaning on semantic drift (α).")
            if self._looks_like_question(query_lower) and beta_free:
                beta += 0.12
                alpha -= 0.08
                insights.append(
                    "Detected factoid/question — increasing symbolic weight β.")
            if len(query_lower) > 160 and alpha_free:
                alpha += 0.08
                beta -= 0.05
                insights.append(
                    "Long-form prompt — emphasising dense embeddings α.")
            if self._contains_any(query_lower,
                                  self.CONCEPTUAL_KEYWORDS) and alpha_free:
                alpha += 0.07
                beta -= 0.05
                insights.append(
                    "Conceptual language detected — leaning on semantic drift (α).")
            if self._contains_any(
                    query_lower,
                    self.RECENCY_CUES) and beta_free:
                beta += 0.05
                insights.append(
                    "Recency cue detected — trusting structured graph (β).")

        avg_trust = stats.get("avg_trust")
        if isinstance(avg_trust, (int, float)
                      ) and avg_trust < 0.35 and alpha_free:
            alpha += 0.05
            insights.append(
                "Average trust is low — boosting dense similarity for generalisation.")

        utilisation = stats.get("memory_utilisation")
        if isinstance(utilisation, (int, float)):
            if utilisation > 0.85:
                if lambda_override is None:
                    lam = max(0.85, lam - 0.04)
                    insights.append(
                        "Memory is crowded — accelerating trust decay λ.")
            elif utilisation < 0.45 and lambda_override is None:
                lam = min(0.99, lam + 0.02)
                insights.append(
                    "Plenty of free capacity — relaxing decay slightly (λ).")

        alpha = self._clamp(alpha)
        beta = self._clamp(beta)
        if alpha_free and beta_free:
            total = alpha + beta
            if total > 1e-6:
                alpha /= total
                beta /= total
        elif not alpha_free and beta_free:
            beta = self._clamp(beta, upper=max(0.0, 1.0 - alpha))
        elif not beta_free and alpha_free:
            alpha = self._clamp(alpha, upper=max(0.0, 1.0 - beta))

        mode = (
            "manual overrides"
            if (
                alpha_override is not None
                or beta_override is not None
                or lambda_override is not None
            )
            else "adaptive"
        )
        insights.insert(
            0, f"Using α={alpha:.2f} / β={beta:.2f} with {mode} controller.")
        insights.append(f"Trust decay λ currently {lam:.2f}.")

        return ParameterDecision(alpha, beta, lam, insights, mode)

    # ------------------------------------------------------------------ #
    def apply_lambda(self, new_lambda: float) -> None:
        new_lambda = self._clamp(new_lambda)
        if math.isclose(
                new_lambda,
                float(
                    self.memory.lam),
                rel_tol=1e-3,
                abs_tol=1e-3):
            return
        self.memory.lam = new_lambda
        if hasattr(self.memory, "gsr"):
            self.memory.gsr.update_params(lam=new_lambda, validate_now=False)

    # ------------------------------------------------------------------ #
    @staticmethod
    def _clamp(
            value: float,
            *,
            lower: float = 0.0,
            upper: float = 1.0) -> float:
        return float(max(lower, min(upper, value)))

    @classmethod
    def _looks_like_question(cls, text: str) -> bool:
        if text.endswith("?"):
            return True
        return any(text.startswith(prefix) for prefix in cls.QUESTION_PREFIXES)

    @staticmethod
    def _contains_any(text: str, candidates: tuple[str, ...]) -> bool:
        return any(re.search(re.escape(keyword), text)
                   for keyword in candidates)
