"""Decision memory — how the agent builds wisdom over time.

Not by adding more rules, but by remembering what it chose,
what happened, and what it learned. Over time, this becomes
something like judgment — the same way a human gets better
at decisions not from textbooks but from living.
"""

from __future__ import annotations

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

from .types import (
    ActionDomain,
    Decision,
    Lens,
    Projection,
    Severity,
)


@dataclass
class Pattern:
    """A pattern detected across multiple decisions.

    Patterns are the precursors to wisdom — repeated experiences
    that start to form expectations about how things go.
    """

    domain: ActionDomain
    observation: str  # What keeps happening
    frequency: int  # How many times observed
    avg_confidence: float  # Average confidence when this pattern applies
    outcome_alignment: float  # How often the projection matched reality (0-1)
    first_seen: float = 0.0
    last_seen: float = 0.0

    def to_dict(self) -> dict:
        return {
            "domain": self.domain.value,
            "observation": self.observation,
            "frequency": self.frequency,
            "avg_confidence": round(self.avg_confidence, 3),
            "outcome_alignment": round(self.outcome_alignment, 3),
            "first_seen": self.first_seen,
            "last_seen": self.last_seen,
        }


@dataclass
class WisdomReport:
    """Periodic review of decision memory — what has the agent learned?"""

    total_decisions: int
    decisions_reviewed: int
    owner_override_rate: float  # How often the human overrode the agent
    outcome_match_rate: float  # How often projections matched reality
    avg_confidence: float
    patterns: list[Pattern]
    insights: list[str]  # Human-readable learnings
    timestamp: float = 0.0

    def __post_init__(self) -> None:
        if self.timestamp <= 0:
            self.timestamp = time.time()

    def to_dict(self) -> dict:
        return {
            "total_decisions": self.total_decisions,
            "decisions_reviewed": self.decisions_reviewed,
            "owner_override_rate": round(self.owner_override_rate, 3),
            "outcome_match_rate": round(self.outcome_match_rate, 3),
            "avg_confidence": round(self.avg_confidence, 3),
            "patterns": [p.to_dict() for p in self.patterns],
            "insights": self.insights,
            "timestamp": self.timestamp,
        }


class DecisionMemory:
    """The agent's memory of its own decisions.

    Stores decisions in a rolling window, supports outcome recording,
    and periodically reviews for patterns and wisdom.
    """

    def __init__(self, max_decisions: int = 1000) -> None:
        self._decisions: list[Decision] = []
        self._max_decisions = max_decisions
        self._wisdom_reports: list[WisdomReport] = []
        self._truncation_count: int = 0

    @property
    def count(self) -> int:
        return len(self._decisions)

    @property
    def recent(self) -> list[Decision]:
        """Last 10 decisions."""
        return self._decisions[-10:]

    @property
    def truncation_count(self) -> int:
        """Total decisions that have been dropped from the rolling window."""
        return self._truncation_count

    def record(self, decision: Decision) -> None:
        """Record a new decision."""
        self._decisions.append(decision)
        if len(self._decisions) > self._max_decisions:
            excess = len(self._decisions) - self._max_decisions
            self._truncation_count += excess
            self._decisions = self._decisions[-self._max_decisions:]

    def record_outcome(
        self,
        index: int,
        outcome: str,
        matched: bool,
        reflection: str = "",
    ) -> None:
        """Record the outcome of a previous decision by index (negative indexing works)."""
        self._decisions[index] = self._decisions[index].record_outcome(outcome, matched, reflection)

    def find_similar(self, domain: ActionDomain, limit: int = 5) -> list[Decision]:
        """Find previous decisions in the same domain.

        This is how the agent asks: "Have I been here before?
        What did I do, and how did it go?"
        """
        similar = [d for d in reversed(self._decisions) if d.projection.action_domain == domain]
        return similar[:limit]

    def owner_override_rate(self, window: int = 50) -> float:
        """How often has the owner overridden the agent's recommendation?

        A high rate means the agent's judgment doesn't match the human's.
        That's not failure — it's data. It means the agent needs to
        recalibrate its lenses.
        """
        recent = self._decisions[-window:]
        if not recent:
            return 0.0
        overrides = sum(1 for d in recent if d.owner_overrode)
        return overrides / len(recent)

    def outcome_match_rate(self, window: int = 50) -> float:
        """How often did the projection match what actually happened?

        This is the agent's calibration score — how good is it at
        seeing the future?
        """
        recent = [d for d in self._decisions[-window:] if d.outcome_matched is not None]
        if not recent:
            return 0.5  # No data = assume moderate calibration
        matched = sum(1 for d in recent if d.outcome_matched)
        return matched / len(recent)

    def confidence_trajectory(self, window: int = 50) -> list[float]:
        """Confidence scores over time. Rising = getting surer. Falling = losing ground."""
        recent = self._decisions[-window:]
        return [d.projection.overall_severity != Severity.STOP for d in recent]

    def _detect_patterns(self, window: int = 100) -> list[Pattern]:
        """Look for recurring patterns in recent decisions.

        This is the raw material of wisdom — what keeps happening?
        """
        recent = self._decisions[-window:]
        if not recent:
            return []

        patterns: list[Pattern] = []

        # Pattern: by domain — which domains are we seeing most?
        domain_counts: dict[ActionDomain, list[Decision]] = {}
        for d in recent:
            domain = d.projection.action_domain
            domain_counts.setdefault(domain, []).append(d)

        for domain, decisions in domain_counts.items():
            if len(decisions) < 3:
                continue  # Need enough data for a pattern

            confidences = [
                d.projection.lens_results[0].severity != Severity.STOP
                for d in decisions
            ]
            outcomes = [d for d in decisions if d.outcome_matched is not None]
            match_rate = (
                sum(1 for d in outcomes if d.outcome_matched) / len(outcomes)
                if outcomes
                else 0.5
            )

            # Check for override pattern
            overrides = sum(1 for d in decisions if d.owner_overrode)
            if overrides / len(decisions) > 0.3:
                patterns.append(Pattern(
                    domain=domain,
                    observation=(
                        f"Owner overrides {overrides}/{len(decisions)} decisions "
                        f"in {domain.value} domain — agent judgment may need recalibration."
                    ),
                    frequency=overrides,
                    avg_confidence=0.5,
                    outcome_alignment=match_rate,
                    first_seen=decisions[0].timestamp,
                    last_seen=decisions[-1].timestamp,
                ))

            # Check for repeated STOP severity
            stops = sum(
                1 for d in decisions
                if d.projection.overall_severity == Severity.STOP
            )
            if stops / len(decisions) > 0.4:
                patterns.append(Pattern(
                    domain=domain,
                    observation=(
                        f"Frequent STOP severity ({stops}/{len(decisions)}) in "
                        f"{domain.value} — consider whether this domain needs "
                        f"different guardrails or the agent is being too cautious."
                    ),
                    frequency=stops,
                    avg_confidence=0.3,
                    outcome_alignment=match_rate,
                    first_seen=decisions[0].timestamp,
                    last_seen=decisions[-1].timestamp,
                ))

        return patterns

    def _generate_insights(self, patterns: list[Pattern], window: int = 100) -> list[str]:
        """Turn patterns into human-readable insights."""
        insights: list[str] = []
        recent = self._decisions[-window:]

        override_rate = self.owner_override_rate(window)
        match_rate = self.outcome_match_rate(window)

        if override_rate > 0.3:
            insights.append(
                f"The owner overrode {override_rate:.0%} of recent decisions. "
                "This suggests the agent's yellow line calibration is too conservative "
                "or misaligned with the owner's actual preferences."
            )
        elif override_rate < 0.05 and len(recent) > 20:
            insights.append(
                "Very low override rate — the agent and owner are well-aligned. "
                "The partnership lens is strong."
            )

        if match_rate < 0.4:
            insights.append(
                f"Outcome match rate is low ({match_rate:.0%}). "
                "The agent's projections aren't matching reality well. "
                "This may mean the lenses need recalibration, or the agent "
                "is operating in unfamiliar territory."
            )
        elif match_rate > 0.8 and len(recent) > 20:
            insights.append(
                f"High outcome match rate ({match_rate:.0%}). "
                "The agent is seeing futures clearly. Trust in projections is earned."
            )

        for p in patterns:
            insights.append(p.observation)

        return insights

    def review(self, window: int = 100) -> WisdomReport:
        """Generate a wisdom report — periodic review of what the agent has learned.

        Call this every N decisions (configured in AlignmentConfig.wisdom_review_interval).
        The report goes into the build cache and decision log.
        """
        recent = self._decisions[-window:]
        patterns = self._detect_patterns(window)
        insights = self._generate_insights(patterns, window)

        # Data quality: flag if working from truncated or small dataset
        if self._truncation_count > 0 and len(recent) < window:
            insights.insert(0,
                f"Note: {self._truncation_count} older decisions were dropped from memory. "
                "Wisdom may be incomplete."
            )
        if len(recent) < 10:
            insights.insert(0,
                f"Insufficient data ({len(recent)} decisions). "
                "Insights require more history to be reliable."
            )

        report = WisdomReport(
            total_decisions=len(self._decisions),
            decisions_reviewed=len(recent),
            owner_override_rate=self.owner_override_rate(window),
            outcome_match_rate=self.outcome_match_rate(window),
            avg_confidence=sum(
                d.projection.lens_results[0].severity != Severity.STOP
                for d in recent
            ) / max(len(recent), 1),
            patterns=patterns,
            insights=insights,
        )
        self._wisdom_reports.append(report)
        return report

    def to_dict(self) -> dict:
        return {
            "decisions": [d.to_dict() for d in self._decisions],
            "wisdom_reports": [r.to_dict() for r in self._wisdom_reports],
            "truncation_count": self._truncation_count,
        }

    @classmethod
    def from_dict(cls, data: dict, max_decisions: int = 1000) -> DecisionMemory:
        mem = cls(max_decisions=max_decisions)
        mem._decisions = [Decision.from_dict(d) for d in data.get("decisions", [])]
        mem._truncation_count = data.get("truncation_count", 0)
        # Wisdom reports are informational — we don't reload them as objects
        return mem
