"""AlignmentEnclave — the yellow line made operational.

This is the main orchestrator. It sits between "the agent wants to do something"
and "the agent does it." Every action passes through here.

The enclave doesn't make the final decision — it illuminates it.
It shows what each lens sees, recommends whether to proceed,
and records the decision for future wisdom.

For most actions, this is instant and invisible — a quick scan
that says "all clear" and lets the agent move. For edge cases,
it slows down, thinks harder, and sometimes says "ask the human."

Like driving: most of the time you don't think about the yellow line.
But it's always there. And when it matters, you see it.
"""

from __future__ import annotations

import time
from dataclasses import dataclass
from typing import Optional

from .escalation import EscalationDecision, EscalationLevel, decide_escalation
from .lenses import ActionContext, evaluate_all_lenses
from .memory import DecisionMemory, WisdomReport
from .selfstate import SelfMonitor
from .storage import MemoryStorage, StorageBackend
from .types import (
    ActionDomain,
    AlignmentConfig,
    Decision,
    Projection,
    SelfState,
    SelfStateFlag,
    Severity,
    SEVERITY_ORDER,
)


class AlignmentEnclave:
    """The fifth pillar — future state projection and alignment.

    Usage:

        enclave = AlignmentEnclave.create(owner_npub="npub1...", owner_name="vergel")

        # Before any significant action:
        result = enclave.check(
            domain=ActionDomain.PAY,
            description="Pay 500 sats for relay hosting invoice",
            involves_money=True,
            money_amount_sats=500,
        )

        if result.should_proceed:
            # Do the thing
            enclave.record_proceeded()
        else:
            # Escalate or defer
            message = result.escalation.message_to_owner
            enclave.record_deferred()
    """

    def __init__(
        self,
        config: AlignmentConfig,
        monitor: SelfMonitor,
        memory: DecisionMemory,
        storage: StorageBackend,
    ) -> None:
        self._config = config
        self._monitor = monitor
        self._memory = memory
        self._storage = storage
        self._last_projection: Optional[Projection] = None
        self._last_escalation: Optional[EscalationDecision] = None
        self._pending_decision: bool = False

    @classmethod
    def create(
        cls,
        owner_npub: str = "",
        owner_name: str = "",
        storage: Optional[StorageBackend] = None,
        **config_overrides,
    ) -> AlignmentEnclave:
        """Create a new AlignmentEnclave.

        This is the moment the agent gets its compass.
        """
        if storage is None:
            storage = MemoryStorage()
        config = AlignmentConfig(
            owner_npub=owner_npub,
            owner_name=owner_name,
            **config_overrides,
        )
        monitor = SelfMonitor(config)
        memory = DecisionMemory(max_decisions=config.max_memory_decisions)

        # Record the owner's first interaction as "now"
        if owner_npub or owner_name:
            monitor.record_owner_interaction()

        return cls(config, monitor, memory, storage)

    @classmethod
    def load(cls, storage: StorageBackend) -> Optional[AlignmentEnclave]:
        """Load an existing enclave from storage."""
        data = storage.load()
        if data is None:
            return None
        config = AlignmentConfig.from_dict(data.get("config", {}))
        monitor = SelfMonitor(config)
        # Restore self-state
        if "self_state" in data:
            from .types import SelfState
            monitor._state = SelfState.from_dict(data["self_state"])
        memory = DecisionMemory.from_dict(
            data.get("memory", {}),
            max_decisions=config.max_memory_decisions,
        )
        return cls(config, monitor, memory, storage)

    def check(
        self,
        domain: ActionDomain,
        description: str,
        involves_secrets: bool = False,
        involves_money: bool = False,
        money_amount_sats: int = 0,
        involves_publication: bool = False,
        involves_communication: bool = False,
        recipient_trust_tier: Optional[str] = None,
        is_reversible: bool = True,
        is_novel: Optional[bool] = None,
        confidence: float = 0.5,
        request_origin: str = "self",
        resembles_known_attack: bool = False,
        crosses_trust_boundary: bool = False,
    ) -> CheckResult:
        """Run the yellow line check on a proposed action.

        This is the main API. Call this before any significant action.
        Returns a CheckResult telling you whether to proceed.
        """
        # Update self-state first — the agent needs to know its own condition
        self_state = self._monitor.check()

        # Detect novelty from memory if not specified
        if is_novel is None:
            similar = self._memory.find_similar(domain, limit=1)
            is_novel = len(similar) == 0

        # Build the action context
        ctx = ActionContext(
            domain=domain,
            description=description,
            self_state=self_state,
            involves_secrets=involves_secrets,
            involves_money=involves_money,
            money_amount_sats=money_amount_sats,
            involves_publication=involves_publication,
            involves_communication=involves_communication,
            recipient_trust_tier=recipient_trust_tier,
            is_reversible=is_reversible,
            is_novel=is_novel,
            owner_recently_active=self_state.hours_since_owner < self._config.owner_absent_hours,
            confidence=confidence,
            request_origin=request_origin,
            resembles_known_attack=resembles_known_attack,
            crosses_trust_boundary=crosses_trust_boundary,
        )

        # Run all five lenses
        lens_results = tuple(evaluate_all_lenses(ctx))

        # Determine overall severity — worst case across all lenses
        severities = [lr.severity for lr in lens_results]
        overall = max(severities, key=lambda s: SEVERITY_ORDER.index(s))

        # Should we proceed?
        should_proceed = overall in (Severity.CLEAR, Severity.CAUTION)
        should_escalate = overall in (Severity.YIELD, Severity.STOP)

        # Build the rationale
        if overall == Severity.CLEAR:
            rationale = "All five lenses clear. Proceed with confidence."
        elif overall == Severity.CAUTION:
            caution_lenses = [lr for lr in lens_results if lr.severity == Severity.CAUTION]
            names = [lr.lens.value for lr in caution_lenses]
            rationale = f"Caution from {', '.join(names)}. Proceed but note the concerns."
        elif overall == Severity.YIELD:
            yield_lenses = [lr for lr in lens_results if lr.is_blocking]
            names = [lr.lens.value for lr in yield_lenses]
            rationale = f"Yield — {', '.join(names)} raised concerns that need attention."
        else:
            stop_lenses = [lr for lr in lens_results if lr.severity == Severity.STOP]
            names = [lr.lens.value for lr in stop_lenses]
            rationale = f"STOP — {', '.join(names)} require human decision before proceeding."

        # Build the projection
        projection = Projection(
            action_domain=domain,
            action_description=description,
            lens_results=lens_results,
            overall_severity=overall,
            should_proceed=should_proceed,
            should_escalate=should_escalate,
            rationale=rationale,
        )

        # Run escalation analysis
        escalation = decide_escalation(projection, self_state, self._config)

        # Store for deferred decision recording
        self._last_projection = projection
        self._last_escalation = escalation
        self._pending_decision = True

        return CheckResult(
            projection=projection,
            escalation=escalation,
            should_proceed=should_proceed and escalation.level in (
                EscalationLevel.NONE,
                EscalationLevel.INFORM,
            ),
            should_escalate=escalation.level in (
                EscalationLevel.ASK,
                EscalationLevel.HALT,
            ),
            self_state_snapshot=self._monitor.snapshot(),
        )

    def record_proceeded(self, owner_overrode: bool = False, owner_feedback: str = "") -> Decision:
        """Record that the agent proceeded with the action."""
        if not self._pending_decision or self._last_projection is None:
            raise RuntimeError("No pending decision to record. Call check() first.")
        # Enforce the yellow line: STOP severity cannot proceed without owner override
        if self._last_projection.overall_severity == Severity.STOP and not owner_overrode:
            raise RuntimeError(
                "STOP severity cannot proceed without explicit owner override. "
                f"Action: {self._last_projection.action_description}. "
                "Call record_deferred() instead."
            )
        decision = Decision(
            projection=self._last_projection,
            chose_to_proceed=True,
            owner_overrode=owner_overrode,
            owner_feedback=owner_feedback or None,
        )
        self._memory.record(decision)
        self._monitor.record_decision(
            confidence=1.0 - SEVERITY_ORDER.index(self._last_projection.overall_severity) / 3,
        )
        self._pending_decision = False
        self._maybe_review()
        # Force immediate persistence
        if self._storage is not None:
            try:
                self.save()
            except Exception as e:
                self._monitor.force_flag(SelfStateFlag.MEMORY_PRESSURE)
                raise RuntimeError(
                    f"CRITICAL: Failed to persist decision. Error: {e}"
                ) from e
        return decision

    def record_deferred(self, owner_feedback: str = "") -> Decision:
        """Record that the agent deferred / did not proceed."""
        if not self._pending_decision or self._last_projection is None:
            raise RuntimeError("No pending decision to record. Call check() first.")
        decision = Decision(
            projection=self._last_projection,
            chose_to_proceed=False,
            owner_feedback=owner_feedback or None,
        )
        self._memory.record(decision)
        self._monitor.record_decision(confidence=0.3)
        self._pending_decision = False
        self._maybe_review()
        # Force immediate persistence
        if self._storage is not None:
            try:
                self.save()
            except Exception as e:
                self._monitor.force_flag(SelfStateFlag.MEMORY_PRESSURE)
                raise RuntimeError(
                    f"CRITICAL: Failed to persist decision. Error: {e}"
                ) from e
        return decision

    def record_owner_interaction(self) -> None:
        """Call this whenever the owner interacts with the agent."""
        self._monitor.record_owner_interaction()

    def report_tool_health(self, tool_name: str, is_working: bool) -> None:
        """Report health of an external tool."""
        self._monitor.report_tool_health(tool_name, is_working)

    def flag_manipulation(self) -> None:
        """Call this when prompt injection or manipulation is detected."""
        self._monitor.force_flag(SelfStateFlag.UNDER_INFLUENCE)

    def clear_manipulation(self) -> None:
        """Call this when the manipulation concern is resolved."""
        self._monitor.clear_flag(SelfStateFlag.UNDER_INFLUENCE)

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

    def _maybe_review(self) -> None:
        """Auto-review if we've hit the wisdom interval."""
        if self._memory.count % self._config.wisdom_review_interval == 0:
            self._memory.review()

    @property
    def self_state(self) -> SelfState:
        return self._monitor.state

    @property
    def decision_count(self) -> int:
        return self._memory.count

    @property
    def recent_decisions(self) -> list[Decision]:
        return self._memory.recent

    def save(self) -> None:
        """Persist alignment state to storage."""
        data = {
            "config": self._config.to_dict(),
            "self_state": self._monitor.state.to_dict(),
            "memory": self._memory.to_dict(),
            "version": "0.1.0",
        }
        self._storage.save(data)


@dataclass
class CheckResult:
    """The complete result of a yellow line check.

    This is what the agent gets back when it asks "should I do this?"
    """

    projection: Projection  # What the lenses see
    escalation: EscalationDecision  # What to do about it
    should_proceed: bool  # Bottom line: can I go?
    should_escalate: bool  # Bottom line: should I ask the human?
    self_state_snapshot: dict  # Agent health at time of check

    def summary(self) -> str:
        """One-line summary for quick logging."""
        status = "GO" if self.should_proceed else ("ASK" if self.should_escalate else "HOLD")
        return f"[{status}] {self.projection.summary()}"

