"""Decision framework: logging, confidence thresholds, rate limiting, escalation."""

from __future__ import annotations

import hashlib
import json
import logging
import os
import sqlite3
import threading
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any

logger = logging.getLogger("horizon.fund.decisions")


class OperatingMode(str, Enum):
    """Fund operating mode."""
    DRY_RUN = "dry_run"      # LLM proposes, human approves each action
    SUPERVISED = "supervised"  # LLM executes low-risk, escalates high-risk
    AUTONOMOUS = "autonomous"  # LLM executes all within guardrails


class ActionType(str, Enum):
    """Types of autonomous actions."""
    ADD_TO_UNIVERSE = "add_to_universe"
    DEPLOY_STAGING = "deploy_staging"
    PROMOTE_LIVE = "promote_live"
    SCALE_UP = "scale_up"
    SCALE_DOWN = "scale_down"
    RETIRE_STRATEGY = "retire_strategy"
    KILL_SWITCH = "kill_switch"
    REBALANCE = "rebalance"
    RESEARCH_SCAN = "research_scan"


@dataclass(frozen=True)
class ConfidenceThresholds:
    """Minimum confidence required for each action type."""
    add_to_universe: float = 0.3
    deploy_staging: float = 0.5
    promote_live: float = 0.7
    scale_up: float = 0.8
    scale_down: float = 0.5
    retire_strategy: float = 0.6
    kill_switch: float = 0.5
    rebalance: float = 0.6
    research_scan: float = 0.2

    def threshold_for(self, action: ActionType) -> float:
        return getattr(self, action.value, 0.5)


@dataclass
class RateLimits:
    """Rate limits to prevent runaway decision loops."""
    max_deploys_per_hour: int = 3
    max_capital_changes_per_day: int = 10
    max_kill_switches_per_day: int = 2
    cooldown_after_loss_secs: float = 300.0


@dataclass
class Decision:
    """A recorded autonomous decision with full reasoning chain."""
    decision_id: str
    timestamp: float
    action: ActionType
    parameters: dict[str, Any]
    reasoning: str
    confidence: float
    outcome: str  # "executed", "rejected", "escalated", "pending"
    error: str | None = None
    execution_time_ms: float = 0.0


class DecisionFramework:
    """Governs autonomous decision-making with guardrails.

    Every autonomous action must pass through this framework, which:
    1. Checks confidence against minimum thresholds
    2. Enforces rate limits
    3. Logs every decision with full reasoning (audit trail)
    4. Escalates to human when confidence is too low
    5. Supports dry-run mode (propose only)

    All decisions are persisted to SQLite for full audit.
    """

    _SCHEMA = """
    CREATE TABLE IF NOT EXISTS decisions (
        decision_id TEXT PRIMARY KEY,
        timestamp REAL NOT NULL,
        action TEXT NOT NULL,
        parameters_json TEXT NOT NULL,
        reasoning TEXT NOT NULL,
        confidence REAL NOT NULL,
        outcome TEXT NOT NULL,
        error TEXT,
        execution_time_ms REAL DEFAULT 0,
        prev_hash TEXT
    );
    CREATE INDEX IF NOT EXISTS idx_decisions_action ON decisions(action);
    CREATE INDEX IF NOT EXISTS idx_decisions_ts ON decisions(timestamp);
    CREATE INDEX IF NOT EXISTS idx_decisions_outcome ON decisions(outcome);
    """

    def __init__(
        self,
        mode: OperatingMode = OperatingMode.SUPERVISED,
        thresholds: ConfidenceThresholds | None = None,
        rate_limits: RateLimits | None = None,
        db_path: str | None = None,
    ) -> None:
        self._mode = mode
        self._thresholds = thresholds or ConfidenceThresholds()
        self._rate_limits = rate_limits or RateLimits()
        self._lock = threading.RLock()
        self._prev_hash = "0" * 64
        self._pending: list[Decision] = []
        self._action_counts: dict[str, list[float]] = {}

        # SQLite persistence
        self._db_path = db_path or os.path.join(
            os.path.expanduser("~"), ".horizon", "fund_decisions.db"
        )
        db_dir = os.path.dirname(self._db_path)
        if db_dir:
            os.makedirs(db_dir, exist_ok=True)
        self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
        self._conn.execute("PRAGMA journal_mode=WAL")
        self._conn.executescript(self._SCHEMA)
        self._conn.commit()
        if db_dir:
            os.chmod(self._db_path, 0o600)

    @property
    def mode(self) -> OperatingMode:
        return self._mode

    def set_mode(self, mode: OperatingMode) -> None:
        self._mode = mode
        logger.info("Operating mode changed to: %s", mode.value)

    def propose(
        self,
        action: ActionType,
        parameters: dict[str, Any],
        reasoning: str,
        confidence: float,
    ) -> Decision:
        """Propose an autonomous action. Evaluates guardrails and either
        executes, rejects, or queues for human approval.

        Returns:
            Decision with outcome status.
        """
        now = time.time()
        confidence = max(0.0, min(1.0, confidence))

        # Auto-expire stale pending decisions
        self.expire_pending()

        # Generate deterministic decision ID
        decision_id = self._generate_id(action, parameters, now)

        decision = Decision(
            decision_id=decision_id,
            timestamp=now,
            action=action,
            parameters=parameters,
            reasoning=reasoning,
            confidence=confidence,
            outcome="pending",
        )

        # Check confidence threshold
        threshold = self._thresholds.threshold_for(action)
        if confidence < threshold:
            decision.outcome = "escalated"
            decision.error = f"confidence {confidence:.2f} < threshold {threshold:.2f}"
            self._persist(decision)
            logger.info(
                "Decision ESCALATED: %s confidence=%.2f threshold=%.2f",
                action.value, confidence, threshold,
            )
            return decision

        # Check rate limits
        rate_check = self._check_rate_limits(action, now)
        if rate_check is not None:
            decision.outcome = "rejected"
            decision.error = rate_check
            self._persist(decision)
            logger.warning("Decision RATE-LIMITED: %s - %s", action.value, rate_check)
            return decision

        # Apply mode
        if self._mode == OperatingMode.DRY_RUN:
            decision.outcome = "pending"
            with self._lock:
                self._pending.append(decision)
            self._persist(decision)
            return decision

        if self._mode == OperatingMode.SUPERVISED:
            # Low-risk actions execute, high-risk escalate
            if self._is_high_risk(action):
                decision.outcome = "escalated"
                with self._lock:
                    self._pending.append(decision)
                self._persist(decision)
                return decision

        # Execute
        decision.outcome = "executed"
        self._record_action(action, now)
        self._persist(decision)
        logger.info(
            "Decision EXECUTED: %s confidence=%.2f params=%s",
            action.value, confidence, json.dumps(parameters, default=str)[:200],
        )
        return decision

    def approve(self, decision_id: str) -> bool:
        """Approve a pending decision (human-in-the-loop)."""
        with self._lock:
            for d in self._pending:
                if d.decision_id == decision_id:
                    d.outcome = "executed"
                    self._pending.remove(d)
                    self._update_outcome(decision_id, "executed")
                    logger.info("Decision APPROVED: %s", decision_id)
                    return True
        return False

    def reject(self, decision_id: str, reason: str = "") -> bool:
        """Reject a pending decision."""
        with self._lock:
            for d in self._pending:
                if d.decision_id == decision_id:
                    d.outcome = "rejected"
                    d.error = reason
                    self._pending.remove(d)
                    self._update_outcome(decision_id, "rejected", reason)
                    logger.info("Decision REJECTED: %s reason=%s", decision_id, reason)
                    return True
        return False

    def pending_decisions(self) -> list[Decision]:
        """Return all pending decisions awaiting approval."""
        with self._lock:
            return list(self._pending)

    def get_decision(self, decision_id: str) -> Decision | None:
        """Retrieve a single decision by ID from the audit log."""
        with self._lock:
            row = self._conn.execute(
                """SELECT decision_id, timestamp, action, parameters_json, reasoning,
                         confidence, outcome, error, execution_time_ms
                  FROM decisions WHERE decision_id = ?""",
                (decision_id,),
            ).fetchone()
        if row is None:
            return None
        return Decision(
            decision_id=row[0],
            timestamp=row[1],
            action=ActionType(row[2]),
            parameters=json.loads(row[3]) if row[3] else {},
            reasoning=row[4],
            confidence=row[5],
            outcome=row[6],
            error=row[7],
            execution_time_ms=row[8] or 0.0,
        )

    def recent_decisions(self, limit: int = 50, action: ActionType | None = None) -> list[Decision]:
        """Query recent decisions from the audit log."""
        conditions = []
        params: list[Any] = []

        if action is not None:
            conditions.append("action = ?")
            params.append(action.value)

        where = " AND ".join(conditions) if conditions else "1=1"
        sql = f"""SELECT decision_id, timestamp, action, parameters_json, reasoning,
                         confidence, outcome, error, execution_time_ms
                  FROM decisions WHERE {where}
                  ORDER BY timestamp DESC LIMIT ?"""
        params.append(limit)

        with self._lock:
            rows = self._conn.execute(sql, params).fetchall()

        return [
            Decision(
                decision_id=r[0],
                timestamp=r[1],
                action=ActionType(r[2]),
                parameters=json.loads(r[3]) if r[3] else {},
                reasoning=r[4],
                confidence=r[5],
                outcome=r[6],
                error=r[7],
                execution_time_ms=r[8] or 0.0,
            )
            for r in rows
        ]

    def stats(self) -> dict[str, Any]:
        """Return decision statistics."""
        with self._lock:
            rows = self._conn.execute(
                """SELECT outcome, COUNT(*) FROM decisions
                   GROUP BY outcome"""
            ).fetchall()
            action_rows = self._conn.execute(
                """SELECT action, COUNT(*) FROM decisions
                   WHERE outcome = 'executed'
                   GROUP BY action"""
            ).fetchall()
        return {
            "mode": self._mode.value,
            "pending": len(self._pending),
            "by_outcome": {r[0]: r[1] for r in rows},
            "executed_by_action": {r[0]: r[1] for r in action_rows},
        }

    def expire_pending(self, max_age_secs: float = 86400.0) -> int:
        """Expire pending decisions older than max_age_secs. Returns count expired."""
        now = time.time()
        expired = 0
        with self._lock:
            still_pending: list[Decision] = []
            for d in self._pending:
                if now - d.timestamp > max_age_secs:
                    d.outcome = "expired"
                    self._update_outcome(d.decision_id, "expired", "auto-expired after timeout")
                    expired += 1
                    logger.info("Decision EXPIRED: %s (age=%.0fs)", d.decision_id, now - d.timestamp)
                else:
                    still_pending.append(d)
            self._pending = still_pending
        return expired

    def set_thresholds(self, thresholds: ConfidenceThresholds) -> None:
        """Replace the current confidence thresholds."""
        self._thresholds = thresholds

    def close(self) -> None:
        with self._lock:
            self._conn.close()

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _check_rate_limits(self, action: ActionType, now: float) -> str | None:
        """Check rate limits. Returns error string if exceeded, None if OK."""
        hour_ago = now - 3600
        day_ago = now - 86400

        with self._lock:
            counts = self._action_counts.get(action.value, [])

            rl = self._rate_limits
            if action in (ActionType.DEPLOY_STAGING, ActionType.PROMOTE_LIVE):
                recent = [t for t in counts if t > hour_ago]
                if len(recent) >= rl.max_deploys_per_hour:
                    return f"deploy rate limit: {len(recent)}/{rl.max_deploys_per_hour} per hour"

            if action in (ActionType.SCALE_UP, ActionType.SCALE_DOWN, ActionType.REBALANCE):
                recent = [t for t in counts if t > day_ago]
                if len(recent) >= rl.max_capital_changes_per_day:
                    return f"capital change rate limit: {len(recent)}/{rl.max_capital_changes_per_day} per day"

            if action == ActionType.KILL_SWITCH:
                recent = [t for t in counts if t > day_ago]
                if len(recent) >= rl.max_kill_switches_per_day:
                    return f"kill switch rate limit: {len(recent)}/{rl.max_kill_switches_per_day} per day"

            # Prune timestamps older than 24h to prevent unbounded growth
            for key in list(self._action_counts):
                self._action_counts[key] = [
                    t for t in self._action_counts[key] if t > day_ago
                ]

        return None

    def _record_action(self, action: ActionType, timestamp: float) -> None:
        with self._lock:
            self._action_counts.setdefault(action.value, []).append(timestamp)

    @staticmethod
    def _is_high_risk(action: ActionType) -> bool:
        return action in (
            ActionType.PROMOTE_LIVE,
            ActionType.SCALE_UP,
            ActionType.KILL_SWITCH,
            ActionType.RETIRE_STRATEGY,
        )

    def _generate_id(self, action: ActionType, parameters: dict, timestamp: float) -> str:
        raw = f"{action.value}:{json.dumps(parameters, sort_keys=True, default=str)}:{timestamp}"
        return f"d-{hashlib.sha256(raw.encode()).hexdigest()[:12]}"

    def _persist(self, decision: Decision) -> None:
        params_json = json.dumps(decision.parameters, default=str)
        # Hash chain for tamper evidence
        record = f"{decision.decision_id}:{decision.action.value}:{params_json}:{self._prev_hash}"
        current_hash = hashlib.sha256(record.encode()).hexdigest()

        with self._lock:
            self._conn.execute(
                """INSERT OR REPLACE INTO decisions
                   (decision_id, timestamp, action, parameters_json, reasoning,
                    confidence, outcome, error, execution_time_ms, prev_hash)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
                (
                    decision.decision_id, decision.timestamp,
                    decision.action.value, params_json,
                    decision.reasoning, decision.confidence,
                    decision.outcome, decision.error,
                    decision.execution_time_ms, current_hash,
                ),
            )
            self._conn.commit()
            self._prev_hash = current_hash

    def _update_outcome(self, decision_id: str, outcome: str, error: str | None = None) -> None:
        with self._lock:
            self._conn.execute(
                "UPDATE decisions SET outcome = ?, error = ? WHERE decision_id = ?",
                (outcome, error, decision_id),
            )
            self._conn.commit()
