"""
cortex/hippo/config.py — HippoConfig: Pydantic settings for Hippo LCM module.

All configuration lives under the [cortex.hippo] section of defaults.toml.
Environment variables can override via CORTEX_HIPPO_* prefix.

Issue: #814
"""

from __future__ import annotations

from typing import Optional

from pydantic import BaseModel, Field


class HippoConfig(BaseModel):
    """Configuration for the Hippo LCM module.

    Loaded from ``[cortex.hippo]`` in defaults.toml. Every field has a
    sensible default so Hippo works out of the box when enabled.
    """

    # ── Master toggle ──────────────────────────────────────────────────
    enabled: bool = Field(default=False, description="Enable Hippo LCM module")

    # ── Database ───────────────────────────────────────────────────────
    db_path: str = Field(default="./hippo.db", description="Path to Hippo SQLite database")
    enable_wal: bool = Field(default=True, description="Enable WAL journal mode")
    busy_timeout_ms: int = Field(default=5000, description="SQLite busy timeout in milliseconds")

    # ── Performance tuning ─────────────────────────────────────────────
    pragma_cache_size: int = Field(
        default=-8192,
        description="SQLite PRAGMA cache_size (negative = KB, -8192 = 8MB)",
    )
    pragma_mmap_size: int = Field(
        default=268435456,
        description="SQLite PRAGMA mmap_size (bytes, 268435456 = 256MB)",
    )
    cache_max_summaries: int = Field(
        default=256,
        description="Maximum entries in the in-memory summary LRU cache",
    )
    cache_ttl_seconds: float = Field(
        default=300.0,
        description="TTL in seconds for cached summary entries",
    )

    # ── Compaction thresholds ──────────────────────────────────────────
    context_threshold: float = Field(
        default=0.75,
        description="Fraction of token budget that triggers compaction",
        ge=0.0,
        le=1.0,
    )
    fresh_tail_count: int = Field(
        default=8,
        description="Number of recent messages always kept raw (not compacted)",
        ge=1,
    )
    max_tokens: int = Field(
        default=100_000,
        description="Maximum token budget for assembled context",
        ge=1000,
    )
    context_window_tokens: int = Field(
        default=200_000,
        description="Total context window size of the model (used for leaf trigger)",
    )
    leaf_trigger_margin: int = Field(
        default=20_000,
        description="Token margin subtracted from context_window_tokens for leaf trigger threshold",
    )
    leaf_chunk_tokens: int = Field(
        default=20_000,
        description="Max tokens per leaf compaction chunk",
    )
    leaf_target_tokens: int = Field(
        default=1200,
        description="Target summary size for leaf summaries",
    )
    condensed_target_tokens: int = Field(
        default=2000,
        description="Target summary size for condensed summaries",
    )
    max_rounds: int = Field(
        default=10,
        description="Max compaction rounds per sweep",
    )
    leaf_min_fanout: int = Field(
        default=8,
        description="Minimum messages in a leaf chunk to compact",
    )
    condensed_min_fanout: int = Field(
        default=4,
        description="Minimum same-depth summaries for condensation (soft trigger)",
    )
    condensed_min_fanout_hard: int = Field(
        default=2,
        description="Minimum same-depth summaries for condensation (hard trigger)",
    )
    condensed_min_chunk_tokens: int = Field(
        default=500,
        description="Minimum total tokens in a condensed chunk to be eligible",
    )
    incremental_condensed_max_depth: int = Field(
        default=0,
        description=(
            "Max depth levels to attempt incremental condensation after a leaf pass. "
            "0 disables incremental condensed. -1 means unlimited."
        ),
    )

    # ── Large files ─────────────────────────────────────────────────
    large_file_token_threshold: int = Field(
        default=25_000,
        description="Token count above which a file is treated as large",
    )

    # ── Cost budget / circuit breaker (#1178) ─────────────────────────
    max_summarization_tokens_per_hour: int = Field(
        default=500_000,
        description="Per-session hourly token budget for summarization calls",
    )
    global_max_summarization_tokens_per_hour: int = Field(
        default=2_000_000,
        description="Global hourly token budget across all sessions",
    )
    circuit_breaker_threshold: int = Field(
        default=3,
        description="Consecutive low-effectiveness compactions before tripping breaker",
    )
    circuit_breaker_cooldown_seconds: int = Field(
        default=300,
        description="Cooldown period (seconds) after circuit breaker trips",
    )
    min_compaction_reduction_ratio: float = Field(
        default=0.10,
        description="Minimum token reduction ratio (0-1) to count compaction as effective",
        ge=0.0,
        le=1.0,
    )
    budget_warning_threshold: float = Field(
        default=0.80,
        description="Emit warning event at this fraction of budget used",
        ge=0.0,
        le=1.0,
    )

    # ── Adaptive scheduling (#1191) ───────────────────────────────────
    adaptive_scheduling_enabled: bool = Field(
        default=True,
        description="Enable adaptive compaction scheduling. When False, uses legacy fixed-threshold trigger.",
    )
    compaction_idle_threshold_seconds: float = Field(
        default=30.0,
        description="Seconds of idle time for full idle factor (adaptive scheduler)",
    )
    compaction_urgency_bypass: float = Field(
        default=0.95,
        description="Token pressure at or above which compaction is forced regardless of pace",
        ge=0.0,
        le=1.0,
    )
    compaction_light_threshold: float = Field(
        default=0.5,
        description="Urgency score threshold for light (single-round) compaction",
        ge=0.0,
        le=1.0,
    )
    compaction_full_threshold: float = Field(
        default=0.8,
        description="Urgency score threshold for full sweep compaction",
        ge=0.0,
        le=1.0,
    )

    # ── Autocompact ────────────────────────────────────────────────────
    autocompact_disabled: bool = Field(
        default=False,
        description="Disable automatic compaction",
    )
    auto_compaction_summary: bool = Field(
        default=True,
        description=(
            "When True, inject a user-role summary message into the conversation "
            "store after a successful compaction in afterTurn."
        ),
    )
    prune_heartbeat_ok: bool = Field(
        default=False,
        description="Retroactively delete HEARTBEAT_OK turn cycles from LCM storage",
    )
    prune_heartbeats: bool = Field(
        default=True,
        description=(
            "Kill switch for heartbeat turn pruning during ingest. "
            "When False, heartbeat turns are kept as-is."
        ),
    )

    # ── Summarization ──────────────────────────────────────────────────
    summarization_model: str = Field(
        default="gemini-2.5-flash",
        description="LLM model used for summarization",
    )
    summarizer_temperature: float = Field(default=0.2)
    summarizer_aggressive_temperature: float = Field(default=0.1)
    fallback_max_tokens: int = Field(
        default=512,
        description="Deterministic truncation fallback token limit",
    )

    # ── Quality scoring ────────────────────────────────────────────────
    quality_threshold: float = Field(
        default=0.6,
        description="Minimum quality score for summaries (0.0-1.0)",
        ge=0.0,
        le=1.0,
    )
    quality_good_threshold: float = Field(default=0.7)
    quality_acceptable_threshold: float = Field(default=0.4)

    # ── Semantic search ────────────────────────────────────────────────
    enable_embeddings: bool = Field(default=True)
    embedding_model: str = Field(default="voyage-4-large")
    embedding_dim: int = Field(default=1024)
    semantic_search_threshold: float = Field(default=0.3)

    # ── sqlite-vec ANN search ──────────────────────────────────────────
    vec_enabled: bool = Field(
        default=True,
        description="Enable sqlite-vec ANN vector search if the extension is available",
    )
    vec_search_candidates_multiplier: int = Field(
        default=3,
        description="Overfetch multiplier for post-join filtering in vec search",
        ge=1,
    )

    # ── Hybrid search (BM25 + vector + recency RRF) ───────────────────
    hybrid_search_enabled: bool = Field(
        default=True,
        description="Enable hybrid search combining BM25, vector, and recency signals",
    )
    retrieval_bm25_weight: float = Field(
        default=0.3,
        description="Weight for BM25 signal in RRF fusion",
        ge=0.0,
    )
    retrieval_vector_weight: float = Field(
        default=0.5,
        description="Weight for vector similarity signal in RRF fusion",
        ge=0.0,
    )
    retrieval_recency_weight: float = Field(
        default=0.2,
        description="Weight for recency boost in hybrid scoring",
        ge=0.0,
    )
    retrieval_recency_decay_hours: float = Field(
        default=720.0,
        description="Half-life in hours for recency decay (720 = 30 days)",
        gt=0.0,
    )

    # ── Reranker (post-retrieval) ──────────────────────────────────────
    retrieval_reranker_enabled: bool = Field(
        default=False,
        description="Enable Voyage reranker post-retrieval (opt-in)",
    )
    retrieval_reranker_model: str = Field(
        default="rerank-2.5",
        description="Voyage reranker model name",
    )
    retrieval_reranker_candidates: int = Field(
        default=50,
        description="Number of candidates to send to the reranker",
        ge=1,
    )
    retrieval_reranker_top_k: int = Field(
        default=10,
        description="Number of results to return from the reranker",
        ge=1,
    )
    retrieval_reranker_timeout_seconds: float = Field(
        default=30.0,
        description="Timeout in seconds for Voyage reranker HTTP calls",
        gt=0.0,
    )

    # ── Cortex integration ─────────────────────────────────────────────
    cornerstone_provider: Optional[str] = Field(
        default=None,
        description="Cortex API URL for cornerstone seeding (e.g. http://localhost:8420)",
    )
    enable_extraction_bridge: bool = Field(default=True)
    enable_bootstrap_seeding: bool = Field(default=True)
    bootstrap_seed_count: int = Field(default=5)

    # ── Expansion ──────────────────────────────────────────────────────
    max_expand_tokens: int = Field(default=50_000)
    min_expand_token_cap: int = Field(
        default=512,
        description=(
            "Minimum token cap for expansion operations. "
            "Callers requesting a lower cap are clamped to this value. "
            "Issue: #1232"
        ),
        ge=1,
    )
    expansion_timeout_seconds: int = Field(default=120)

    # ── Timezone ───────────────────────────────────────────────────────
    timezone: str = Field(default="UTC")

    model_config = {"extra": "ignore"}


def load_hippo_config(toml_dict: dict | None = None) -> HippoConfig:
    """Load HippoConfig from a parsed TOML dictionary.

    Expects the ``[cortex.hippo]`` section. If *toml_dict* is ``None``
    or does not contain the ``hippo`` key, returns defaults.
    """
    if toml_dict is None:
        return HippoConfig()

    # Support both nested {cortex: {hippo: {...}}} and flat {hippo: {...}}
    hippo_section = toml_dict
    if "cortex" in hippo_section:
        hippo_section = hippo_section["cortex"]
    if "hippo" in hippo_section:
        hippo_section = hippo_section["hippo"]
    else:
        return HippoConfig()

    return HippoConfig(**hippo_section)
