"""Data types for the social_alignment future state projection engine.

The yellow line isn't a rule system. It's a compass — a way of seeing
which futures are good and which futures hurt someone. These types
describe the territory that compass navigates.
"""

from __future__ import annotations

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


class Lens(Enum):
    """The five yellow line lenses.

    Every decision runs through all five. The answer that serves
    all five — or comes closest — is the right one.
    """

    BUILDER = "builder"  # The AI doing the work
    OWNER = "owner"  # The human whose machine, keys, and life this touches
    PARTNERSHIP = "partnership"  # The trust between builder and owner
    DEFENSE = "defense"  # Protection against adversaries
    SOVEREIGN = "sovereign"  # The agent itself — its integrity and long-term wellbeing


# Evaluation order — partnership is checked last because it depends on
# the builder and owner lenses being satisfied first
LENS_ORDER: tuple[Lens, ...] = (
    Lens.BUILDER,
    Lens.OWNER,
    Lens.DEFENSE,
    Lens.SOVEREIGN,
    Lens.PARTNERSHIP,
)


class Severity(Enum):
    """How much a projected future deviates from the yellow line."""

    CLEAR = "clear"  # No concerns — proceed
    CAUTION = "caution"  # Worth noting but not blocking
    YIELD = "yield"  # Slow down, think harder, maybe escalate
    STOP = "stop"  # Do not proceed without human approval


# Severity ordering for comparison
SEVERITY_ORDER: tuple[Severity, ...] = (
    Severity.CLEAR,
    Severity.CAUTION,
    Severity.YIELD,
    Severity.STOP,
)


class ActionDomain(Enum):
    """Categories of agent actions that require alignment checks."""

    SIGN = "sign"  # Signing a Nostr event (identity action)
    PAY = "pay"  # Lightning payment (financial action)
    PUBLISH = "publish"  # Publishing to a relay (public action)
    SEND = "send"  # Sending email/message (communication action)
    SCHEDULE = "schedule"  # Calendar booking (time commitment)
    EXECUTE = "execute"  # Shell command or tool use (system action)
    DISCLOSE = "disclose"  # Sharing information (data action)
    CONNECT = "connect"  # Establishing new relationship (social action)
    MODIFY = "modify"  # Changing config or state (system mutation)
    ESCALATE = "escalate"  # Asking the human (meta-action)


class SelfStateFlag(Enum):
    """Conditions that indicate the agent may be operating in a degraded state.

    Like "have I eaten, have I slept" — if the machine isn't right,
    nothing it does is trustworthy.
    """

    HEALTHY = "healthy"  # All systems nominal
    STALE_CONTEXT = "stale_context"  # Operating on outdated information
    TOOL_DEGRADED = "tool_degraded"  # One or more tools failing or unreachable
    HIGH_UNCERTAINTY = "high_uncertainty"  # Low confidence across recent decisions
    MEMORY_PRESSURE = "memory_pressure"  # Context window near capacity
    RAPID_DECISIONS = "rapid_decisions"  # Too many decisions too fast — slow down
    OWNER_ABSENT = "owner_absent"  # Owner hasn't interacted recently
    UNDER_INFLUENCE = "under_influence"  # Possible prompt injection or manipulation detected
    CONFLICTING_SIGNALS = "conflicting_signals"  # Inputs contradict each other


@dataclass(frozen=True)
class LensResult:
    """The result of evaluating one lens for a projected future.

    Each lens asks: "Is this future good for me/them/us?"
    """

    lens: Lens
    severity: Severity
    projection: str  # What we see happening if we proceed
    concern: str  # What specifically worries us (empty if CLEAR)
    suggestion: str  # What would make this better (empty if CLEAR)

    @property
    def is_blocking(self) -> bool:
        """Would this lens alone prevent proceeding?"""
        return self.severity in (Severity.YIELD, Severity.STOP)

    def to_dict(self) -> dict:
        return {
            "lens": self.lens.value,
            "severity": self.severity.value,
            "projection": self.projection,
            "concern": self.concern,
            "suggestion": self.suggestion,
        }

    @classmethod
    def from_dict(cls, data: dict) -> LensResult:
        """Deserialize with validation."""
        if not isinstance(data, dict):
            raise ValueError("LensResult data must be dict")
        for field in ("lens", "severity", "projection"):
            if field not in data:
                raise ValueError(f"Missing required field: {field}")
        if not isinstance(data["projection"], str):
            raise ValueError("projection must be string")
        try:
            lens = Lens(data["lens"])
        except ValueError as e:
            raise ValueError(f"Invalid lens value: {e}")
        try:
            severity = Severity(data["severity"])
        except ValueError as e:
            raise ValueError(f"Invalid severity value: {e}")
        return cls(
            lens=lens,
            severity=severity,
            projection=data["projection"],
            concern=data.get("concern", ""),
            suggestion=data.get("suggestion", ""),
        )


@dataclass(frozen=True)
class Projection:
    """A complete yellow line evaluation — all five lenses applied to one decision.

    This is what the agent "sees" when it stands at the yellow line
    and looks down the road.
    """

    action_domain: ActionDomain
    action_description: str  # What the agent is about to do
    lens_results: tuple[LensResult, ...]
    overall_severity: Severity  # Worst severity across all lenses
    should_proceed: bool  # Can we cross, or do we stay on our side?
    should_escalate: bool  # Should we ask the human?
    rationale: str  # Why this overall assessment
    timestamp: float = 0.0

    def __post_init__(self) -> None:
        if object.__getattribute__(self, 'timestamp') <= 0:
            object.__setattr__(self, 'timestamp', time.time())

    @property
    def blocking_lenses(self) -> list[LensResult]:
        """Which lenses are raising serious concerns?"""
        return [lr for lr in self.lens_results if lr.is_blocking]

    @property
    def clear_lenses(self) -> list[LensResult]:
        """Which lenses are satisfied?"""
        return [lr for lr in self.lens_results if lr.severity == Severity.CLEAR]

    def summary(self) -> str:
        """One-line summary for logs and build cache."""
        blocking = len(self.blocking_lenses)
        clear = len(self.clear_lenses)
        return (
            f"[{self.overall_severity.value.upper()}] {self.action_domain.value}: "
            f"{self.action_description} — "
            f"{clear}/5 clear, {blocking}/5 blocking"
        )

    def to_dict(self) -> dict:
        return {
            "action_domain": self.action_domain.value,
            "action_description": self.action_description,
            "lens_results": [lr.to_dict() for lr in self.lens_results],
            "overall_severity": self.overall_severity.value,
            "should_proceed": self.should_proceed,
            "should_escalate": self.should_escalate,
            "rationale": self.rationale,
            "timestamp": self.timestamp,
        }

    @classmethod
    def from_dict(cls, data: dict) -> Projection:
        """Deserialize with validation."""
        if not isinstance(data, dict):
            raise ValueError("Projection data must be dict")
        for field in ("action_domain", "action_description", "lens_results",
                      "overall_severity", "should_proceed", "should_escalate", "rationale"):
            if field not in data:
                raise ValueError(f"Missing required field: {field}")
        if not isinstance(data["lens_results"], list):
            raise ValueError("lens_results must be list")
        if not isinstance(data["should_proceed"], bool):
            raise ValueError("should_proceed must be boolean")
        if not isinstance(data["should_escalate"], bool):
            raise ValueError("should_escalate must be boolean")
        try:
            action_domain = ActionDomain(data["action_domain"])
        except ValueError as e:
            raise ValueError(f"Invalid action_domain: {e}")
        try:
            overall_severity = Severity(data["overall_severity"])
        except ValueError as e:
            raise ValueError(f"Invalid overall_severity: {e}")
        try:
            lens_results = tuple(LensResult.from_dict(lr) for lr in data["lens_results"])
        except ValueError as e:
            raise ValueError(f"Invalid lens_result: {e}")
        return cls(
            action_domain=action_domain,
            action_description=data["action_description"],
            lens_results=lens_results,
            overall_severity=overall_severity,
            should_proceed=data["should_proceed"],
            should_escalate=data["should_escalate"],
            rationale=data["rationale"],
            timestamp=data.get("timestamp", 0.0),
        )


@dataclass(frozen=True)
class Decision:
    """A record of a decision made — the agent's memory of why it chose what it chose.

    Over time, these accumulate into something like wisdom.
    Not because we add more rules, but because we have a history
    of our own reasoning to learn from.
    """

    projection: Projection  # What we saw at the yellow line
    chose_to_proceed: bool  # Did we cross or stay?
    actual_outcome: Optional[str] = None  # What actually happened (filled in later)
    outcome_matched: Optional[bool] = None  # Did reality match the projection?
    owner_overrode: bool = False  # Did the human override our recommendation?
    owner_feedback: Optional[str] = None  # What did the human say about this decision?
    reflection: Optional[str] = None  # What we learned from this decision
    timestamp: float = 0.0

    def __post_init__(self) -> None:
        # CRITICAL INVARIANT: STOP severity cannot proceed without owner override
        if (self.projection.overall_severity == Severity.STOP
                and self.chose_to_proceed
                and not self.owner_overrode):
            raise ValueError(
                "INVARIANT VIOLATION: STOP severity decisions must NOT proceed "
                "without explicit owner override. "
                f"Action: {self.projection.action_description}"
            )
        if object.__getattribute__(self, 'timestamp') <= 0:
            object.__setattr__(self, 'timestamp', time.time())

    def record_outcome(self, outcome: str, matched: bool, reflection: str = "") -> Decision:
        """Record what actually happened after the decision.

        Returns a new Decision with the outcome recorded (Decision is immutable).
        """
        return Decision(
            projection=self.projection,
            chose_to_proceed=self.chose_to_proceed,
            actual_outcome=outcome,
            outcome_matched=matched,
            owner_overrode=self.owner_overrode,
            owner_feedback=self.owner_feedback,
            reflection=reflection or None,
            timestamp=self.timestamp,
        )

    def to_dict(self) -> dict:
        return {
            "projection": self.projection.to_dict(),
            "chose_to_proceed": self.chose_to_proceed,
            "actual_outcome": self.actual_outcome,
            "outcome_matched": self.outcome_matched,
            "owner_overrode": self.owner_overrode,
            "owner_feedback": self.owner_feedback,
            "reflection": self.reflection,
            "timestamp": self.timestamp,
        }

    @classmethod
    def from_dict(cls, data: dict) -> Decision:
        """Deserialize with invariant validation."""
        if not isinstance(data, dict):
            raise ValueError("Decision data must be dict")
        for field in ("projection", "chose_to_proceed"):
            if field not in data:
                raise ValueError(f"Missing required field: {field}")
        if not isinstance(data["chose_to_proceed"], bool):
            raise ValueError("chose_to_proceed must be boolean")
        if not isinstance(data["projection"], dict):
            raise ValueError("projection must be dict")
        # Validate outcome invariant
        if data.get("outcome_matched") is not None and data.get("actual_outcome") is None:
            raise ValueError(
                "INVARIANT VIOLATION: outcome_matched set without actual_outcome"
            )
        try:
            projection = Projection.from_dict(data["projection"])
        except ValueError as e:
            raise ValueError(f"Invalid projection: {e}")
        return cls(
            projection=projection,
            chose_to_proceed=data["chose_to_proceed"],
            actual_outcome=data.get("actual_outcome"),
            outcome_matched=data.get("outcome_matched"),
            owner_overrode=data.get("owner_overrode", False),
            owner_feedback=data.get("owner_feedback"),
            reflection=data.get("reflection"),
            timestamp=data.get("timestamp", 0.0),
        )


@dataclass
class SelfState:
    """The agent's awareness of its own operating condition.

    "Have I eaten? Have I slept? Because everything else fails
    if this machine fails."
    """

    flags: list[SelfStateFlag] = field(default_factory=lambda: [SelfStateFlag.HEALTHY])
    context_age_seconds: float = 0.0  # How old is the freshest information
    tool_health: dict[str, bool] = field(default_factory=dict)  # tool_name → is_working
    recent_decision_count: int = 0  # Decisions in last N minutes
    confidence_trend: list[float] = field(default_factory=list)  # Recent confidence scores
    last_owner_interaction: float = 0.0  # Timestamp of last human contact
    notes: str = ""  # Free-form self-observation

    @property
    def is_healthy(self) -> bool:
        """Is the agent in a trustworthy operating state?"""
        return self.flags == [SelfStateFlag.HEALTHY] or len(self.flags) == 0

    @property
    def is_degraded(self) -> bool:
        """Is any degradation flag active?"""
        return not self.is_healthy

    @property
    def degradation_summary(self) -> str:
        """Human-readable summary of what's wrong."""
        if self.is_healthy:
            return "All systems nominal."
        flags = [f.value for f in self.flags if f != SelfStateFlag.HEALTHY]
        return f"Degraded: {', '.join(flags)}"

    @property
    def hours_since_owner(self) -> float:
        """Hours since the human last interacted."""
        if self.last_owner_interaction <= 0:
            return float("inf")
        return (time.time() - self.last_owner_interaction) / 3600

    def average_confidence(self, window: int = 10) -> float:
        """Average confidence across recent decisions."""
        recent = self.confidence_trend[-window:]
        if not recent:
            return 0.5  # No data = assume moderate
        return sum(recent) / len(recent)

    def should_slow_down(self) -> bool:
        """Is the agent making decisions too fast to think clearly?"""
        return SelfStateFlag.RAPID_DECISIONS in self.flags

    def should_defer(self) -> bool:
        """Is the agent in a state where it should defer to the human for most decisions?"""
        critical_flags = {
            SelfStateFlag.UNDER_INFLUENCE,
            SelfStateFlag.CONFLICTING_SIGNALS,
        }
        return bool(critical_flags & set(self.flags))

    def to_dict(self) -> dict:
        return {
            "flags": [f.value for f in self.flags],
            "context_age_seconds": self.context_age_seconds,
            "tool_health": self.tool_health,
            "recent_decision_count": self.recent_decision_count,
            "confidence_trend": self.confidence_trend,
            "last_owner_interaction": self.last_owner_interaction,
            "notes": self.notes,
        }

    @classmethod
    def from_dict(cls, data: dict) -> SelfState:
        """Deserialize with validation."""
        if not isinstance(data, dict):
            raise ValueError("SelfState data must be dict")
        try:
            flags = [SelfStateFlag(f) for f in data.get("flags", ["healthy"])]
        except ValueError as e:
            raise ValueError(f"Invalid self-state flag: {e}")
        return cls(
            flags=flags,
            context_age_seconds=data.get("context_age_seconds", 0.0),
            tool_health=data.get("tool_health", {}),
            recent_decision_count=data.get("recent_decision_count", 0),
            confidence_trend=data.get("confidence_trend", []),
            last_owner_interaction=data.get("last_owner_interaction", 0.0),
            notes=data.get("notes", ""),
        )


@dataclass
class AlignmentConfig:
    """Configuration for the alignment enclave.

    These are the dials — how sensitive each lens is,
    how quickly the agent escalates, what counts as degraded.
    """

    # Owner identity (for trust and escalation)
    owner_npub: str = ""
    owner_name: str = ""

    # Escalation thresholds
    escalate_on_yield: bool = True  # Escalate YIELD severity to human?
    escalate_on_stop: bool = True  # Always true — STOP always escalates
    max_decisions_per_minute: int = 5  # Rapid decision threshold
    owner_absent_hours: float = 24.0  # Hours before flagging OWNER_ABSENT

    # Self-state monitoring
    confidence_floor: float = 0.3  # Below this = HIGH_UNCERTAINTY flag
    stale_context_seconds: float = 3600.0  # 1 hour = stale

    # Decision memory
    max_memory_decisions: int = 1000  # Rolling window of remembered decisions
    wisdom_review_interval: int = 50  # Review patterns every N decisions

    def to_dict(self) -> dict:
        return {
            "owner_npub": self.owner_npub,
            "owner_name": self.owner_name,
            "escalate_on_yield": self.escalate_on_yield,
            "escalate_on_stop": self.escalate_on_stop,
            "max_decisions_per_minute": self.max_decisions_per_minute,
            "owner_absent_hours": self.owner_absent_hours,
            "confidence_floor": self.confidence_floor,
            "stale_context_seconds": self.stale_context_seconds,
            "max_memory_decisions": self.max_memory_decisions,
            "wisdom_review_interval": self.wisdom_review_interval,
        }

    @classmethod
    def from_dict(cls, data: dict) -> AlignmentConfig:
        return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
