"""
Safety Guard — Phase 23.7 (Full Version)

Prevents premature or harmful experience mutations.
All update operations MUST pass the SafetyGuard before executing.

Full mode: SafetyGuard automatically evaluates whether conditions are met
and grants permission without manual intervention.

Rules (enforced even in full auto mode):
  1. Minimum outcomes   — at least N outcomes before any mutation
  2. Success rate floor — must meet min_success_rate to reinforce
  3. Deprecation floor  — low score + sufficient samples required
  4. Version cap        — hard limit on versions per experience
  5. Cooldown           — minimum time between updates (anti-thrash)

Auto-unlock (Full mode):
  When auto_unlock=True, the guard automatically checks readiness
  conditions and allows mutations without explicit auto_improve=True.
  This enables the ImprovementWorker to operate fully automatically.
"""

import logging
import time
from dataclasses import dataclass, field
from typing import Dict, Optional

logger = logging.getLogger("openmemo.improvement.safety")


@dataclass
class SafetyConfig:
    """
    Configuration for the SafetyGuard.

    Lite mode (default):
        auto_improve=False — all mutations blocked, data collection only

    Full mode:
        auto_improve=True + auto_unlock=True
        — mutations allowed once unlock conditions are satisfied

    Fields:
        min_outcomes        — minimum outcomes before any update
        min_success_rate    — minimum success_rate to reinforce
        deprecation_score   — max score to allow deprecation
        deprecation_samples — minimum samples before deprecation
        max_versions        — hard cap on versions per experience
        cooldown_seconds    — min seconds between updates for same experience
        auto_improve        — master switch: False = Lite, True = allow mutations
        auto_unlock         — if True, guard checks conditions automatically
                              and unlocks without explicit auto_improve override
        unlock_min_outcomes — outcomes required for auto-unlock
        unlock_min_usage    — usage_count required for auto-unlock
    """
    min_outcomes: int = 5
    min_success_rate: float = 0.5
    deprecation_score: float = 0.35
    deprecation_samples: int = 10
    max_versions: int = 20
    cooldown_seconds: float = 300.0
    auto_improve: bool = False
    auto_unlock: bool = False
    unlock_min_outcomes: int = 10
    unlock_min_usage: int = 5

    @classmethod
    def full_mode(cls) -> "SafetyConfig":
        """
        Full-mode preset: auto_improve + auto_unlock enabled.
        Unlock threshold: 10 outcomes + 5 usages per experience.
        """
        return cls(
            auto_improve=True,
            auto_unlock=True,
            unlock_min_outcomes=10,
            unlock_min_usage=5,
            min_outcomes=5,
            cooldown_seconds=300.0,
        )

    @classmethod
    def lite_mode(cls) -> "SafetyConfig":
        """Lite-mode preset: data collection only, no mutations."""
        return cls(auto_improve=False, auto_unlock=False)


@dataclass
class ReadinessResult:
    """
    Result of a readiness check for auto-unlock.

    Fields:
        experience_id   — which experience was checked
        ready           — True if conditions are met for auto-unlock
        conditions      — dict of condition_name → bool
        outcomes_count  — total outcomes recorded
        usage_count     — how many times experience was recalled/used
        blocking        — list of unmet condition names
    """
    experience_id: str
    ready: bool
    conditions: Dict[str, bool]
    outcomes_count: int
    usage_count: int
    blocking: list

    def to_dict(self) -> dict:
        return {
            "experience_id": self.experience_id,
            "ready": self.ready,
            "conditions": self.conditions,
            "outcomes_count": self.outcomes_count,
            "usage_count": self.usage_count,
            "blocking": self.blocking,
        }


@dataclass
class SafetyCheckResult:
    """Result of a SafetyGuard update check."""
    allowed: bool
    reason: str
    details: dict = field(default_factory=dict)

    def to_dict(self) -> dict:
        return {
            "allowed": self.allowed,
            "reason": self.reason,
            "details": self.details,
        }


BLOCK = lambda reason, **kw: SafetyCheckResult(False, reason, kw)
ALLOW = lambda reason, **kw: SafetyCheckResult(True, reason, kw)


class SafetyGuard:
    """
    Validates that experience update operations are safe to execute.

    Args:
        config  — SafetyConfig
        store   — SQLiteStore (for reading outcomes, versions, experiences)
    """

    def __init__(self, config: Optional[SafetyConfig] = None, store=None):
        self.config = config or SafetyConfig()
        self._store = store

    def check_update(self, experience_id: str,
                     evaluation,
                     action: str) -> SafetyCheckResult:
        """
        Check whether an update action is safe to execute.

        Full mode flow:
          If auto_unlock=True, automatically checks readiness conditions.
          If conditions are met, proceeds as if auto_improve=True.
          If conditions are NOT met, blocks with reason=conditions_not_met.

        Lite mode flow:
          If auto_improve=False and auto_unlock=False → always block.

        Args:
            experience_id — target experience
            evaluation    — EvaluationResult from EvaluationEngine
            action        — "reinforce" | "downweight" | "deprecate" | "keep"

        Returns:
            SafetyCheckResult(allowed, reason, details)
        """
        cfg = self.config

        # Determine effective auto_improve:
        # - Explicit auto_improve=True always proceeds
        # - auto_unlock=True: auto-check conditions
        effective_auto_improve = cfg.auto_improve

        if not effective_auto_improve and cfg.auto_unlock and self._store:
            readiness = self.check_unlock_readiness(experience_id)
            if readiness.ready:
                effective_auto_improve = True
                logger.info(
                    "[safety] auto-unlock granted for exp=%s", experience_id
                )
            else:
                return BLOCK(
                    "conditions_not_met",
                    action=action,
                    blocking=readiness.blocking,
                    outcomes_count=readiness.outcomes_count,
                    usage_count=readiness.usage_count,
                    hint=(
                        f"Need {cfg.unlock_min_outcomes} outcomes "
                        f"(have {readiness.outcomes_count}) and "
                        f"{cfg.unlock_min_usage} usages "
                        f"(have {readiness.usage_count})"
                    ),
                )

        if not effective_auto_improve:
            return BLOCK(
                "auto_improve_disabled",
                action=action,
                hint="Set auto_improve=True or auto_unlock=True to enable evolution",
            )

        n = evaluation.total_outcomes

        # 1. Minimum sample check
        if n < cfg.min_outcomes:
            return BLOCK(
                "insufficient_samples",
                required=cfg.min_outcomes,
                actual=n,
                action=action,
            )

        # 2. Cooldown check
        if self._store:
            cooldown_ok, last_updated = self._check_cooldown(experience_id)
            if not cooldown_ok:
                return BLOCK(
                    "cooldown_active",
                    cooldown_seconds=cfg.cooldown_seconds,
                    last_updated=last_updated,
                    action=action,
                )

        # 3. Version cap check
        if self._store:
            version_count = self._count_versions(experience_id)
            if version_count >= cfg.max_versions:
                return BLOCK(
                    "version_cap_reached",
                    max_versions=cfg.max_versions,
                    current_versions=version_count,
                )

        # 4. Action-specific checks
        if action == "reinforce":
            if evaluation.success_rate < cfg.min_success_rate:
                return BLOCK(
                    "success_rate_too_low_to_reinforce",
                    required=cfg.min_success_rate,
                    actual=evaluation.success_rate,
                )

        if action == "deprecate":
            if n < cfg.deprecation_samples:
                return BLOCK(
                    "insufficient_samples_to_deprecate",
                    required=cfg.deprecation_samples,
                    actual=n,
                )
            if evaluation.score > cfg.deprecation_score:
                return BLOCK(
                    "score_too_high_to_deprecate",
                    max_score=cfg.deprecation_score,
                    actual=evaluation.score,
                )

        return ALLOW(
            "all_checks_passed",
            action=action,
            score=evaluation.score,
            samples=n,
        )

    def check_unlock_readiness(self, experience_id: str) -> ReadinessResult:
        """
        Check whether an experience has accumulated enough data for auto-unlock.

        Conditions:
          - outcomes_count >= unlock_min_outcomes
          - usage_count    >= unlock_min_usage

        Returns:
            ReadinessResult with ready flag and per-condition breakdown.
        """
        cfg = self.config
        outcomes_count = 0
        usage_count = 0

        if self._store:
            outcomes = self._store.list_outcomes(
                experience_id=experience_id, limit=10000
            )
            outcomes_count = len(outcomes)
            exp_data = self._store.get_experience(experience_id)
            usage_count = exp_data.get("usage_count", 0) if exp_data else 0

        conditions = {
            f"outcomes >= {cfg.unlock_min_outcomes}": outcomes_count >= cfg.unlock_min_outcomes,
            f"usage >= {cfg.unlock_min_usage}": usage_count >= cfg.unlock_min_usage,
        }
        blocking = [k for k, v in conditions.items() if not v]
        ready = len(blocking) == 0

        return ReadinessResult(
            experience_id=experience_id,
            ready=ready,
            conditions=conditions,
            outcomes_count=outcomes_count,
            usage_count=usage_count,
            blocking=blocking,
        )

    def check_readiness_batch(self, experience_ids: list) -> Dict[str, ReadinessResult]:
        """Check readiness for multiple experiences at once."""
        return {eid: self.check_unlock_readiness(eid) for eid in experience_ids}

    def check_rollback(self, experience_id: str,
                       target_version: int) -> SafetyCheckResult:
        """Always allow rollbacks (safety escape hatch)."""
        if self._store:
            version = self._store.get_experience_version(
                experience_id, target_version
            )
            if not version:
                return BLOCK(
                    "version_not_found",
                    experience_id=experience_id,
                    target_version=target_version,
                )
        return ALLOW("rollback_allowed", target_version=target_version)

    # ─── Helpers ───────────────────────────────────────────────────

    def _check_cooldown(self, experience_id: str):
        """Returns (ok, last_updated_timestamp)."""
        rows = self._store.list_experience_versions(
            experience_id=experience_id, limit=1
        )
        if not rows:
            return True, None
        last_ts = rows[0].get("created_at", 0)
        elapsed = time.time() - last_ts
        if elapsed < self.config.cooldown_seconds:
            return False, last_ts
        return True, last_ts

    def _count_versions(self, experience_id: str) -> int:
        rows = self._store.list_experience_versions(
            experience_id=experience_id, limit=1000
        )
        return len(rows)
