"""InitializationMixin — CortexPlugin __init__ and config/budget helpers."""
from __future__ import annotations

import asyncio
import logging
from typing import Any, List, Optional

from cortex.logging_utils import install_redaction_filter
from cortex.capture.wake_pipeline import _config_int

from .models import _new_session_id

logger = logging.getLogger(__name__)


class InitializationMixin:
    """Mixin that provides __init__, embedding model check, and budget guard."""

    def __init__(
        self,
        config_path: Optional[str] = None,
        db_path: Optional[str] = None,
    ) -> None:
        """
        Initialize all Cortex modules with dependency injection.

        Modules are initialised in order:
          config → storage → llm → extraction → entity_resolution →
          reconciliation → cornerstone → token_budget → retrieval → feedback

        Any module that fails to initialise is recorded; the plugin remains
        functional with degraded capability rather than crashing.
        """
        self._init_errors: List[str] = []
        self._session_id: Optional[str] = None
        self._llm_call_count: int = 0   # resets on wake()

        # Install API key redaction on root logger (idempotent)
        install_redaction_filter()

        # -- Config ----------------------------------------------------------
        try:
            from cortex.config import CortexConfig
            self.config = CortexConfig.from_toml(config_path)
            if db_path:
                self.config.storage_db_path = db_path
        except Exception as exc:  # pragma: no cover
            logger.error("Failed to load CortexConfig: %s", exc)
            self._init_errors.append(f"config: {exc}")
            # Fall back to defaults inline so other modules can still init
            from cortex.config import CortexConfig
            self.config = CortexConfig()
            if db_path:
                self.config.storage_db_path = db_path

        max_concurrent = max(1, _config_int(self.config, "max_concurrent_extractions", 4))
        self._extraction_semaphore = asyncio.Semaphore(max_concurrent)
        self._zero_claim_outcomes: int = 0

        # -- Metrics Collector (#368) ----------------------------------------
        self._metrics: Any = None
        try:
            from cortex.core.metrics import MetricsCollector
            self._metrics = MetricsCollector.get(
                owner_id=self.config.owner_id,
                enabled=self.config.metrics_enabled,
            )
            logger.info(
                "CortexPlugin: metrics %s (format=%s)",
                "enabled" if self.config.metrics_enabled else "disabled",
                self.config.metrics_export_format,
            )
        except Exception as exc:
            logger.warning("Failed to init MetricsCollector: %s", exc)

        # -- Feature Flags (#368) --------------------------------------------
        self._flags: Any = None
        try:
            from cortex.core.feature_flags import FeatureFlagManager
            self._flags = FeatureFlagManager(config=self.config)
            logger.info(
                "CortexPlugin: feature flags %s",
                "enabled" if self.config.feature_flags_enabled else "disabled",
            )
        except Exception as exc:
            logger.warning("Failed to init FeatureFlagManager: %s", exc)

        # -- Storage (M1) ----------------------------------------------------
        self.storage: Any = None
        try:
            from cortex.storage.sqlite_adapter import SQLiteAdapter
            # Determine effective embedding model for the lock check.
            # provider_embedding_model takes precedence when a provider is configured.
            _eff_embedding_model = (
                self.config.provider_embedding_model
                if self.config.provider_embedding_provider
                else self.config.embedding_model
            )
            # Allow env-var override for migration tooling (CORTEX_EMBEDDING_MODEL_LOCK_ENABLED=false)
            import os as _os
            _lock_env = _os.environ.get("CORTEX_EMBEDDING_MODEL_LOCK_ENABLED", "").lower()
            _lock_enabled = (
                False if _lock_env in ("false", "0", "no", "off")
                else self.config.embedding_model_lock_enabled
            )
            self.storage = SQLiteAdapter(
                db_path=self.config.storage_db_path,
                enable_wal=self.config.storage_enable_wal,
                busy_timeout_ms=self.config.storage_busy_timeout_ms,
                embedding_dim=self.config.embedding_dim,
                embedding_model=_eff_embedding_model,
                embedding_model_lock_enabled=_lock_enabled,
            )
        except Exception as exc:
            logger.error("Failed to init SQLiteAdapter: %s", exc)
            self._init_errors.append(f"storage: {exc}")

        # -- Circuit Breaker (config-driven) ---------------------------------
        from cortex.llm.circuit_breaker import CircuitBreaker
        self._circuit_breaker: Optional[CircuitBreaker] = None
        if self.config.circuit_breaker_enabled:
            self._circuit_breaker = CircuitBreaker(
                enabled=True,
                threshold=self.config.circuit_breaker_threshold,
                cooldown_sec=self.config.circuit_breaker_cooldown_sec,
            )
            logger.info(
                "CortexPlugin: circuit breaker enabled (threshold=%d, cooldown=%ds)",
                self.config.circuit_breaker_threshold,
                self.config.circuit_breaker_cooldown_sec,
            )

        # -- LLM Provider (ProviderRouter — replaces legacy cascade) ----------
        # NOTE: Import via package for test patchability (tests patch
        # "cortex.api.plugin.ProviderRouter" and "cortex.api.plugin.build_provider_cascade")
        import cortex.api.plugin as _plugin_pkg
        ProviderRouter = _plugin_pkg.ProviderRouter
        build_provider_cascade = _plugin_pkg.build_provider_cascade

        self.llm: Any = None
        try:
            self.llm = ProviderRouter(config=self.config)
            logger.info(
                "CortexPlugin: ProviderRouter initialised — mode=%s, "
                "provider=%s",
                getattr(self.config, "provider_mode", "host"),
                self.llm.provider_names(),
            )
        except Exception as exc:
            logger.warning(
                "CortexPlugin: ProviderRouter init failed (%s), "
                "falling back to legacy cascade",
                exc,
            )
            # Fallback: legacy FallbackLLMProvider cascade for backward compat
            try:
                priority = list(self.config.llm_provider_priority)
                self.llm = build_provider_cascade(
                    priority,
                    completion_model=self.config.extraction_model,
                    embed_model=self.config.embedding_model,
                    circuit_breaker=self._circuit_breaker,
                )
                logger.info(
                    "CortexPlugin: legacy LLM cascade initialised — priority: %s, "
                    "active providers: %s",
                    priority,
                    self.llm.provider_names(),
                )
            except Exception as cascade_exc:
                logger.error(
                    "Failed to init LLM provider (both router and cascade): %s",
                    cascade_exc,
                )
                self._init_errors.append(f"llm: router={exc}, cascade={cascade_exc}")

        # -- Model Router (#375 — wrap provider with tier routing) ------------
        if self.llm is not None:
            try:
                from cortex.llm.router import ModelRouter
                self.llm = ModelRouter(provider=self.llm, config=self.config)
                if getattr(self.config, "model_routing_enabled", False):
                    logger.info(
                        "CortexPlugin: model routing enabled — "
                        "fast=%s, main=%s, reasoning=%s",
                        getattr(self.config, "model_routing_fast", ""),
                        getattr(self.config, "model_routing_main", ""),
                        getattr(self.config, "model_routing_reasoning", ""),
                    )
                else:
                    logger.debug("CortexPlugin: model routing disabled (passthrough)")
            except Exception as exc:
                logger.warning("CortexPlugin: model router init failed: %s — using raw provider", exc)

        if self.llm is None:
            logger.warning(
                "CortexPlugin: No LLM provider configured. "
                "Memory extraction, reconciliation, and dream cycles will be disabled. "
                "Set provider_mode and provider credentials in cortex.toml, "
                "or configure via POST /api/v1/config/providers in host mode."
            )

        # -- Per-operation LLM providers (Issue #1247) -----------------------
        # Each operation can have its own LLM provider (base_url + api_key)
        # for independent rate limits and routing to different backends.
        # Falls back to the shared self.llm when no per-op override is set.
        self._extraction_llm: Any = self.llm
        self._reconciliation_llm: Any = self.llm
        self._dialectic_llm: Any = self.llm

        if self.llm is not None:
            try:
                from cortex.llm.provider_router import build_operation_provider

                for op_name, attr_name in [
                    ("extraction", "_extraction_llm"),
                    ("reconciliation", "_reconciliation_llm"),
                    ("dialectic", "_dialectic_llm"),
                ]:
                    op_provider = build_operation_provider(
                        config=self.config,
                        operation=op_name,
                    )
                    if op_provider is not None:
                        # Wrap in ModelRouter for tier-based routing support
                        try:
                            from cortex.llm.router import ModelRouter
                            op_provider = ModelRouter(
                                provider=op_provider, config=self.config,
                            )
                        except Exception as mr_exc:
                            logger.warning(
                                "CortexPlugin: ModelRouter wrap failed for %s: %s — using raw provider",
                                op_name, mr_exc,
                            )
                        setattr(self, attr_name, op_provider)
                        logger.info(
                            "CortexPlugin: per-operation LLM provider active for %s",
                            op_name,
                        )
            except Exception as exc:
                logger.warning(
                    "CortexPlugin: per-operation provider init failed: %s — "
                    "all operations will use the shared provider",
                    exc,
                )

        # -- Extraction Pipeline (M2) ----------------------------------------
        self.extractor: Any = None
        try:
            from cortex.extraction.extractor import ExtractionPipeline
            self.extractor = ExtractionPipeline(
                llm=self._extraction_llm,
                model=self.config.extraction_model,
                custom_instructions=getattr(self.config, "extraction_custom_instructions", ""),
                batch_size=self.config.extraction_batch_size,
                max_claims_per_extraction=(
                    _config_int(
                        self.config,
                        "max_claims_per_extraction",
                        _config_int(self.config, "max_extraction_calls_per_batch", 25),
                    )
                ),
                max_entities_per_extraction=getattr(self.config, "max_entities_per_extraction", 100),
                prompt_version=getattr(self.config, "extraction_prompt_version", "v2"),
                known_subjects=getattr(self.config, "known_subjects", None),
            )
            # Wire benchmark config flags (#1276, #1277)
            self.extractor.extract_assistant_turns = getattr(
                self.config, "extract_assistant_turns", False,
            )
            self.extractor.extraction_temporal_markers = getattr(
                self.config, "extraction_temporal_markers", True,
            )
            # Wire split extraction mode (#321)
            self.extractor.extraction_mode = getattr(
                self.config, "extraction_mode", "unified",
            )
        except Exception as exc:
            logger.error("Failed to init ExtractionPipeline: %s", exc)
            self._init_errors.append(f"extraction: {exc}")

        # -- Entity Resolver (M3) --------------------------------------------
        self.entity_resolver: Any = None
        try:
            from cortex.core.entity_resolution import EntityResolver
            self.entity_resolver = EntityResolver(
                storage=self.storage,
                llm=self._reconciliation_llm,
                candidate_k=self.config.entity_resolution_candidate_k,
                batch_size=self.config.entity_resolution_batch_size,
            )
        except Exception as exc:
            logger.error("Failed to init EntityResolver: %s", exc)
            self._init_errors.append(f"entity_resolution: {exc}")

        # -- Reconciliation Engine (M4) --------------------------------------
        self.reconciler: Any = None
        try:
            from cortex.core.reconciliation import ReconciliationEngine
            self.reconciler = ReconciliationEngine(
                storage=self.storage,
                llm=self._reconciliation_llm,
                entity_resolver=self.entity_resolver,
                config=self.config,
            )
        except Exception as exc:
            logger.error("Failed to init ReconciliationEngine: %s", exc)
            self._init_errors.append(f"reconciliation: {exc}")

        # -- Cornerstone Guardian (M5) ---------------------------------------
        self.cornerstone_guardian: Any = None
        try:
            from cortex.identity.cornerstone import CornerstoneGuardian
            self.cornerstone_guardian = CornerstoneGuardian(
                storage=self.storage,
                config=self.config,
                llm=self.llm,  # pass LLM for propose_evolution()
            )
        except Exception as exc:
            logger.error("Failed to init CornerstoneGuardian: %s", exc)
            self._init_errors.append(f"cornerstone: {exc}")

        # -- Token Budget Manager (M9) ---------------------------------------
        self.token_budget_manager: Any = None
        try:
            from cortex.core.token_budget import TokenBudgetManager
            self.token_budget_manager = TokenBudgetManager(config=self.config)
        except Exception as exc:
            logger.error("Failed to init TokenBudgetManager: %s", exc)
            self._init_errors.append(f"token_budget: {exc}")

        # -- Context Window Calculator (#329) --------------------------------
        self.context_calculator: Any = None
        try:
            from cortex.core.context_calculator import ContextWindowCalculator
            self.context_calculator = ContextWindowCalculator(
                config=self.config,
                metrics=self._metrics,
            )
        except Exception as exc:
            logger.error("Failed to init ContextWindowCalculator: %s", exc)
            self._init_errors.append(f"context_calculator: {exc}")

        # -- Context Compiler (#606) -----------------------------------------
        self.context_compiler: Any = None
        if self.token_budget_manager is not None:
            try:
                from cortex.core.context_compiler import ContextCompiler
                self.context_compiler = ContextCompiler(
                    token_budget=self.token_budget_manager,
                    config=self.config,
                )
            except Exception as exc:
                logger.error("Failed to init ContextCompiler: %s", exc)
                self._init_errors.append(f"context_compiler: {exc}")

        # -- Working Memory Cache (#371) -------------------------------------
        self.working_memory: Any = None
        if self.config.working_memory_enabled:
            try:
                from cortex.core.working_memory import WorkingMemoryCache
                self.working_memory = WorkingMemoryCache(
                    max_items=self.config.working_memory_max_items,
                    default_ttl_seconds=self.config.working_memory_default_ttl_seconds,
                    enabled=True,
                )
                logger.info(
                    "CortexPlugin: working memory enabled (max_items=%d, ttl=%ds)",
                    self.config.working_memory_max_items,
                    self.config.working_memory_default_ttl_seconds,
                )
            except Exception as exc:
                logger.warning("Failed to init WorkingMemoryCache: %s — continuing without working memory", exc)

        # -- Retrieval Pipeline (M8) -----------------------------------------
        self.retrieval: Any = None
        try:
            from cortex.core.retrieval import RetrievalPipeline
            self.retrieval = RetrievalPipeline(
                storage=self.storage,
                config=self.config,
                token_budget=self.token_budget_manager,
                cornerstone_guardian=self.cornerstone_guardian,
                llm=self.llm,
                vector_weight=self.config.retrieval_vector_weight,
                bm25_weight=self.config.retrieval_bm25_weight,
                working_memory=self.working_memory,
            )
        except Exception as exc:
            logger.error("Failed to init RetrievalPipeline: %s", exc)
            self._init_errors.append(f"retrieval: {exc}")

        # -- Embedding model switch protection --------------------------------
        self._embedding_model_changed: bool = False
        self._embedding_model_warning_emitted: bool = False
        self._check_embedding_model_consistency()

        # -- Feedback System (M12) -------------------------------------------
        self.feedback_system: Any = None
        try:
            from cortex.core.feedback import FeedbackSystem
            self.feedback_system = FeedbackSystem(
                storage=self.storage,
                owner_id=self.config.owner_id,
            )
        except Exception as exc:
            logger.error("Failed to init FeedbackSystem: %s", exc)
            self._init_errors.append(f"feedback: {exc}")

        # -- Promotion Scorer (#376) ------------------------------------------
        self.promotion_scorer: Any = None
        try:
            from cortex.core.promotion import PromotionScorer
            self.promotion_scorer = PromotionScorer(
                config=self.config,
                feedback_system=self.feedback_system,
            )
            logger.info(
                "CortexPlugin: promotion scorer enabled (threshold=%.2f, min_sessions=%d)",
                self.promotion_scorer.threshold,
                self.promotion_scorer._min_sessions,
            )
        except Exception as exc:
            logger.warning("Failed to init PromotionScorer: %s — using legacy S2 heuristics", exc)

        # -- Event Emitter (M19 — In-process event bus) ----------------------
        # Wire WebhookDispatcher → EventEmitter so ALL events get outbound delivery
        self.event_emitter: Any = None
        self._webhook_dispatcher: Any = None
        try:
            from cortex.events import EventEmitter
            from cortex.events.webhook import WebhookDispatcher
            self._webhook_dispatcher = WebhookDispatcher(storage=self.storage)
            self.event_emitter = EventEmitter(
                storage=self.storage,
                webhook_dispatcher=self._webhook_dispatcher,
            )
        except Exception as exc:
            logger.error("Failed to init EventEmitter: %s", exc)
            self._init_errors.append(f"event_emitter: {exc}")

        # -- Deprecation Pipeline (spaced repetition / reinforcement) --------
        self.deprecation_pipeline: Any = None
        try:
            from cortex.deprecation.pipeline import DeprecationPipeline
            self.deprecation_pipeline = DeprecationPipeline(
                storage=self.storage,
                config=self.config,
            )
        except Exception as exc:
            logger.error("Failed to init DeprecationPipeline: %s", exc)
            self._init_errors.append(f"deprecation: {exc}")

        if self._init_errors:
            logger.warning(
                "CortexPlugin initialised with %d error(s): %s",
                len(self._init_errors),
                "; ".join(self._init_errors),
            )
        else:
            logger.info("CortexPlugin initialised successfully")

    # =========================================================================
    # EMBEDDING MODEL CONSISTENCY CHECK
    # =========================================================================

    def _check_embedding_model_consistency(self) -> None:
        """Check if the embedding model has changed since last run.

        Reads the previously used embedding model from the cortex_config table
        and compares it with the current config. If different and embeddings
        exist, logs a loud warning and sets a flag.
        """
        if self.storage is None:
            return

        current_model = self.config.embedding_model

        try:
            # Read previously stored model from DB
            conn = self.storage._get_connection()
            cursor = conn.execute(
                "SELECT value FROM cortex_config WHERE key = 'embedding_model'"
            )
            row = cursor.fetchone()
            previous_model = row[0] if row else None

            # Check if embeddings exist
            embed_cursor = conn.execute(
                "SELECT COUNT(*) FROM embedding_index"
            )
            embed_count = embed_cursor.fetchone()[0]

            if previous_model and previous_model != current_model and embed_count > 0:
                logger.warning(
                    "⚠️  EMBEDDING MODEL CHANGED from '%s' to '%s'. "
                    "%d existing embeddings are INCOMPATIBLE and will produce "
                    "degraded search results. Consider re-indexing or reverting "
                    "the model change.",
                    previous_model,
                    current_model,
                    embed_count,
                )
                self._embedding_model_changed = True

            # Store current model + dim for next startup comparison
            conn.execute(
                "INSERT OR REPLACE INTO cortex_config (key, value) VALUES (?, ?)",
                ("embedding_model", current_model),
            )
            conn.execute(
                "INSERT OR REPLACE INTO cortex_config (key, value) VALUES (?, ?)",
                ("embedding_dim", str(self.config.embedding_dim)),
            )
            conn.commit()
        except Exception as exc:
            # Table might not exist yet — that's fine, first run
            logger.debug(
                "Embedding model consistency check skipped: %s", exc
            )

    # =========================================================================
    # LLM BUDGET GUARD
    # =========================================================================

    def _budget_check(self, calls_needed: int = 1) -> Optional[str]:
        """Return an error string if *calls_needed* LLM calls would exceed budget.

        Returns None when the budget is sufficient (call is allowed).
        Disabled (returns None always) when max_llm_calls_per_session == 0.
        """
        limit = self.config.max_llm_calls_per_session
        if limit == 0:
            return None  # budget guard disabled
        if self._llm_call_count + calls_needed > limit:
            return (
                f"LLM call budget exhausted: {self._llm_call_count}/{limit} calls "
                f"used this session. Reset on wake()."
            )
        return None

    def _budget_consume(self, n: int = 1) -> None:
        """Consume *n* LLM calls from the session budget and warn at 80%."""
        self._llm_call_count += n
        limit = self.config.max_llm_calls_per_session
        if limit == 0:
            return
        pct = self._llm_call_count / limit
        if pct >= 0.8:
            logger.warning(
                "LLM call budget at %.0f%% (%d/%d calls used this session)",
                pct * 100,
                self._llm_call_count,
                limit,
            )
