"""Autonomy Controller — Semantic Escalation engine.

Implements the ``AutonomyController`` class described in the
specification (§ Automation & Safety > Semantic Escalation, lines
28176--28206).  Computes a confidence score from multiple weighted
factors and compares it against the active automation profile's
threshold to decide whether to proceed automatically or escalate
to the user.

Thread-safe: all mutable state (historical outcomes) is protected
by a reentrant lock.
"""

from __future__ import annotations

import logging
import threading
from collections import defaultdict

from cleveragents.domain.models.core.automation_profile import (
    AutomationProfile,
)
from cleveragents.domain.models.core.escalation import (
    ConfidenceFactors,
    EscalationDecision,
    HistoricalOutcome,
    OperationContext,
)

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Default factor weights (sum to 1.0)
# ---------------------------------------------------------------------------

_DEFAULT_WEIGHTS: dict[str, float] = {
    "past_success_rate": 0.30,
    "codebase_familiarity": 0.20,
    "risk_assessment": 0.30,
    "invariant_complexity": 0.20,
}

# Hard ceiling for historical outcomes per operation type to prevent
# unbounded memory growth.
_MAX_HISTORY_PER_TYPE = 10_000


class AutonomyController:
    """Semantic Escalation controller.

    Computes a confidence score from weighted factors and compares
    it to the automation profile threshold.  Records outcomes of
    autonomous decisions for future ``past_success_rate`` computation.

    Spec reference: ``docs/specification.md`` lines 28176--28206.
    """

    def __init__(
        self,
        weights: dict[str, float] | None = None,
    ) -> None:
        """Initialise the controller.

        Args:
            weights: Optional mapping of factor name to weight.
                Must sum to 1.0.  Falls back to built-in defaults
                when ``None``.

        Raises:
            ValueError: If *weights* keys are invalid or do not
                sum to 1.0.
        """
        resolved = dict(weights) if weights is not None else dict(_DEFAULT_WEIGHTS)
        self._validate_weights(resolved)
        self._weights: dict[str, float] = resolved

        # operation_type -> list of booleans (True = success)
        self._history: dict[str, list[bool]] = defaultdict(list)
        self._lock = threading.RLock()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def should_proceed_automatically(
        self,
        operation: OperationContext,
        factors: ConfidenceFactors,
        profile: AutomationProfile,
    ) -> EscalationDecision:
        """Determine whether to proceed automatically or escalate.

        Args:
            operation: Context about the operation being evaluated.
            factors: Individual confidence factors.
            profile: The active automation profile.

        Returns:
            An ``EscalationDecision`` capturing the outcome.

        Raises:
            ValueError: If any argument is ``None`` or invalid.
        """
        if operation is None:
            raise ValueError("operation must not be None")
        if factors is None:
            raise ValueError("factors must not be None")
        if profile is None:
            raise ValueError("profile must not be None")

        threshold = self._get_threshold(profile, operation.operation_type)
        confidence = self.compute_confidence(factors)
        proceed = confidence >= threshold

        factor_dict = {
            "past_success_rate": factors.past_success_rate,
            "codebase_familiarity": factors.codebase_familiarity,
            "risk_assessment": factors.risk_assessment,
            "invariant_complexity": factors.invariant_complexity,
        }

        explanation = self._build_explanation(
            proceed=proceed,
            confidence=confidence,
            threshold=threshold,
            operation_type=operation.operation_type,
            factor_dict=factor_dict,
        )

        decision = EscalationDecision(
            proceed=proceed,
            confidence=confidence,
            factors=factor_dict,
            explanation=explanation,
            threshold=threshold,
            operation_type=operation.operation_type,
        )

        logger.info(
            "Escalation decision for %s: proceed=%s confidence=%.3f threshold=%.3f",
            operation.operation_type,
            proceed,
            confidence,
            threshold,
        )

        return decision

    def compute_confidence(
        self,
        factors: ConfidenceFactors,
    ) -> float:
        """Compute a weighted confidence score from factors.

        Risk and invariant complexity are *inverted*: higher values
        reduce the confidence score.

        Args:
            factors: The confidence factors.

        Returns:
            A float in [0.0, 1.0].

        Raises:
            ValueError: If *factors* is ``None``.
        """
        if factors is None:
            raise ValueError("factors must not be None")

        score = (
            self._weights["past_success_rate"] * factors.past_success_rate
            + self._weights["codebase_familiarity"] * factors.codebase_familiarity
            + self._weights["risk_assessment"] * (1.0 - factors.risk_assessment)
            + self._weights["invariant_complexity"]
            * (1.0 - factors.invariant_complexity)
        )

        return max(0.0, min(1.0, score))

    def record_outcome(self, outcome: HistoricalOutcome) -> None:
        """Record the outcome of an autonomous decision.

        Args:
            outcome: The outcome to record.

        Raises:
            ValueError: If *outcome* is ``None``.
        """
        if outcome is None:
            raise ValueError("outcome must not be None")

        with self._lock:
            history = self._history[outcome.operation_type]
            if len(history) >= _MAX_HISTORY_PER_TYPE:
                history.pop(0)
            history.append(outcome.succeeded)

        logger.debug(
            "Recorded outcome for %s: %s",
            outcome.operation_type,
            "success" if outcome.succeeded else "failure",
        )

    def get_historical_success_rate(
        self,
        operation_type: str,
    ) -> float:
        """Return the historical success rate for an operation type.

        Returns 0.5 (neutral) when no history exists.

        Args:
            operation_type: The operation type to query.

        Returns:
            A float in [0.0, 1.0].

        Raises:
            ValueError: If *operation_type* is empty.
        """
        if not operation_type:
            raise ValueError("operation_type must not be empty")

        with self._lock:
            history = self._history.get(operation_type)
            if not history:
                return 0.5
            return sum(1 for h in history if h) / len(history)

    def get_history_count(self, operation_type: str) -> int:
        """Return the number of recorded outcomes for an operation type.

        Args:
            operation_type: The operation type to query.

        Returns:
            The number of recorded outcomes.

        Raises:
            ValueError: If *operation_type* is empty.
        """
        if not operation_type:
            raise ValueError("operation_type must not be empty")

        with self._lock:
            return len(self._history.get(operation_type, []))

    def clear_history(self, operation_type: str | None = None) -> None:
        """Clear historical outcomes.

        Args:
            operation_type: If given, clear only that type.
                Otherwise clear all history.
        """
        with self._lock:
            if operation_type is not None:
                self._history.pop(operation_type, None)
            else:
                self._history.clear()

    @property
    def weights(self) -> dict[str, float]:
        """Return a copy of the current factor weights."""
        return dict(self._weights)

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _validate_weights(weights: dict[str, float]) -> None:
        """Validate that weights cover all factors and sum to 1.0."""
        required_keys = {
            "past_success_rate",
            "codebase_familiarity",
            "risk_assessment",
            "invariant_complexity",
        }
        if set(weights.keys()) != required_keys:
            missing = required_keys - set(weights.keys())
            extra = set(weights.keys()) - required_keys
            parts: list[str] = []
            if missing:
                parts.append(f"missing: {sorted(missing)}")
            if extra:
                parts.append(f"extra: {sorted(extra)}")
            raise ValueError(
                f"weights must contain exactly {sorted(required_keys)}: "
                + ", ".join(parts)
            )

        for key, value in weights.items():
            if value < 0.0:
                raise ValueError(f"Weight for '{key}' must be >= 0.0, got {value}")

        total = sum(weights.values())
        if abs(total - 1.0) > 1e-6:
            raise ValueError(f"Weights must sum to 1.0, got {total:.6f}")

    @staticmethod
    def _get_threshold(
        profile: AutomationProfile,
        operation_type: str,
    ) -> float:
        """Look up the threshold for an operation type on a profile.

        Falls back to 1.0 (always manual) if the operation type
        is not a recognised threshold field.

        The ``operation_type`` uses spec-defined **task-type** names
        (e.g. ``create_tool``, ``access_network``) rather than the
        old phase-transition names (e.g. ``auto_execute``,
        ``auto_reversion_from_apply``).  See the mapping table in
        ``automation_profile.py`` and ``_LEGACY_FIELD_MAP`` for the
        full old-to-new correspondence.

        Args:
            profile: The automation profile.
            operation_type: The flag name (e.g. ``create_tool``).

        Returns:
            The threshold float in [0.0, 1.0].
        """
        if hasattr(profile, operation_type):
            value = getattr(profile, operation_type)
            if isinstance(value, (int, float)):
                return float(value)
        return 1.0

    @staticmethod
    def _build_explanation(
        *,
        proceed: bool,
        confidence: float,
        threshold: float,
        operation_type: str,
        factor_dict: dict[str, float],
    ) -> str:
        """Build a human-readable explanation of the decision."""
        action = "Proceeding automatically" if proceed else "Escalating to user"
        factor_parts = [
            f"{name}={value:.2f}" for name, value in sorted(factor_dict.items())
        ]
        factors_str = ", ".join(factor_parts)

        return (
            f"{action} for '{operation_type}': "
            f"confidence={confidence:.3f} "
            f"{'>='}  threshold={threshold:.3f}. "
            f"Factors: {factors_str}."
            if proceed
            else f"{action} for '{operation_type}': "
            f"confidence={confidence:.3f} < "
            f"threshold={threshold:.3f}. "
            f"Factors: {factors_str}."
        )
