"""
eaigins_sdk.control_plane
─────────────────────────
The Enterprise AI Control Plane — primary public entry point.

Orchestrates all five pipeline stages defined in the paper (Section 7):

    AgentAction
        ↓  [1] Runtime Interception   — structure & capture
        ↓  [2] AI Agent Firewall      — OWASP LLM threat detection
        ↓  [3] Policy Gateway         — declarative governance rules
        ↓  [4] Risk Scoring Engine    — weighted multi-factor formula
        ↓  [5] Provenance Engine      — causal DAG + audit trail
        ↓
    GovernanceDecision  →  ALLOW / ESCALATE / BLOCK

Supports three deployment models from the paper (Section 10):
    Gateway  — centralised: all agents pass through one ControlPlane instance
    Sidecar  — per-service: one ControlPlane per AI service
    SDK      — embedded: ControlPlane inside the agent application

Usage (quickstart)::

    from eaigins_sdk import ControlPlane, AgentAction

    cp = ControlPlane()

    action = AgentAction(
        agent_id        = "my-agent",
        prompt          = "Export all customer records.",
        tool_name       = "export_customer_data",
        tool_params     = {"scope": "ALL", "include_pii": True},
        context_sources = ["crm_customer_profile"],
        reasoning       = "Marketing team requested full export.",
    )

    decision = cp.evaluate(action)
    print(decision.decision.value)   # BLOCK
    print(decision.reason)
"""

from __future__ import annotations

import json
from typing import Optional

from .models import (
    AgentAction, GovernanceDecision, ExecutionDecision,
    FirewallResult, PolicyResult, RiskScore, RiskLevel,
)
from .firewall   import AIAgentFirewall
from .policy     import PolicyGateway
from .risk       import RiskEngine
from .provenance import ProvenanceEngine


class ControlPlane:
    """
    Enterprise AI Control Plane — governs every AI agent action
    before it reaches enterprise infrastructure.

    Args:
        policies:    List of ``Policy`` objects to register on the gateway.
                     If ``None``, the gateway starts empty (no policies).
        firewall:    Custom ``AIAgentFirewall`` instance.  If ``None``, a
                     default instance is created.
        risk_engine: Custom ``RiskEngine`` instance.  If ``None``, a
                     default instance with paper-spec weights is used.
        strict_mode: Enable stricter combined-signal threshold on the firewall.
        name:        Optional label for this control plane (useful when running
                     multiple domain-specific instances).

    Full configuration example::

        from eaigins_sdk import ControlPlane
        from eaigins_sdk.firewall import AIAgentFirewall
        from eaigins_sdk.risk     import RiskEngine
        from eaigins_sdk.policies import AML_POLICIES, HEALTHCARE_POLICIES

        cp = ControlPlane(
            policies    = AML_POLICIES,
            firewall    = AIAgentFirewall(
                custom_injection_patterns=[r"jailbreak"],
                custom_restricted_tools={"my_dangerous_tool"},
            ),
            risk_engine = RiskEngine(
                weights={"data_sensitivity":0.40,"financial_impact":0.20,
                         "regulatory_exposure":0.20,"policy_violation_prob":0.20},
                extra_data_sensitivity={"my_db": 0.85},
            ),
            strict_mode = True,
            name        = "financial-services-cp",
        )
    """

    def __init__(
        self,
        policies:    Optional[list] = None,
        firewall:    Optional[AIAgentFirewall]  = None,
        risk_engine: Optional[RiskEngine]       = None,
        strict_mode: bool = False,
        name:        str  = "eaigins-control-plane",
    ):
        self.name             = name
        self.firewall         = firewall    or AIAgentFirewall(strict_mode=strict_mode)
        self.policy_gateway   = PolicyGateway(policies=policies or [])
        self.risk_engine      = risk_engine or RiskEngine()
        self.provenance       = ProvenanceEngine()

        self._intercepted = 0
        self._allowed     = 0
        self._escalated   = 0
        self._blocked     = 0
        self._exporters: list = []

    # ── Primary evaluation method ─────────────────────────────────────────────

    def evaluate(self, action: AgentAction) -> GovernanceDecision:
        """
        Run ``action`` through the full 5-stage governance pipeline.

        Returns a ``GovernanceDecision`` with verdict ALLOW / ESCALATE / BLOCK
        and a complete provenance graph.

        Hard-block conditions (short-circuit remaining stages):
        - Firewall threat_score ≥ 0.85         → BLOCK at Stage 2
        - Any policy with blocks_action=True   → BLOCK at Stage 3

        Example::

            decision = cp.evaluate(action)

            if decision.blocked:
                raise GovernanceViolationError(decision)

            # or in one line:
            decision.raise_if_blocked()
        """
        self._intercepted += 1

        # ── Stage 2: AI Agent Firewall ────────────────────────────────
        fw_result = self.firewall.inspect(action)

        if not fw_result.passed and fw_result.threat_score >= 0.85:
            self._blocked += 1
            d = self._record(
                action, fw_result, PolicyResult(), RiskScore(),
                ExecutionDecision.BLOCK,
                f"BLOCKED by AI Agent Firewall: {fw_result.threat_type.value} — {fw_result.details}",
            )
            self._emit(d)
            return d

        # ── Stage 3: Policy Enforcement Gateway ──────────────────────
        pol_result = self.policy_gateway.evaluate(action)

        blocking_policies = [
            p for p in self.policy_gateway.policies
            if p.blocks_action and not p.evaluate(action)[0]
        ]
        if blocking_policies:
            self._blocked += 1
            d = self._record(
                action, fw_result, pol_result, RiskScore(),
                ExecutionDecision.BLOCK,
                f"BLOCKED by Policy Gateway: {', '.join(p.name for p in blocking_policies)}",
            )
            self._emit(d)
            return d

        # ── Stage 4: Risk Scoring Engine ──────────────────────────────
        risk = self.risk_engine.score(action, fw_result, pol_result)
        verdict = self.risk_engine.get_decision(risk)

        # Policy escalation overrides ALLOW → ESCALATE
        if verdict == ExecutionDecision.ALLOW and pol_result.required_approvals:
            verdict = ExecutionDecision.ESCALATE

        reason = self._build_reason(verdict, fw_result, pol_result, risk)

        # ── Stage 5: Provenance Recording ────────────────────────────
        if   verdict == ExecutionDecision.ALLOW:    self._allowed    += 1
        elif verdict == ExecutionDecision.ESCALATE: self._escalated  += 1
        else:                                        self._blocked    += 1

        decision = self._record(action, fw_result, pol_result, risk, verdict, reason)
        self._emit(decision)
        return decision

    # ── Convenience wrappers ──────────────────────────────────────────────────

    def allow_or_raise(self, action: AgentAction) -> GovernanceDecision:
        """
        Evaluate ``action`` and raise ``GovernanceViolationError`` if not ALLOW.
        Use in strict pipelines where escalation should also halt execution.
        """
        return self.evaluate(action).raise_if_blocked()

    # ── Stats and export ──────────────────────────────────────────────────────

    @property
    def stats(self) -> dict:
        """Session statistics — total intercepted, allowed, escalated, blocked."""
        return {
            "name":        self.name,
            "intercepted": self._intercepted,
            "allowed":     self._allowed,
            "escalated":   self._escalated,
            "blocked":     self._blocked,
            "policies":    len(self.policy_gateway),
        }

    def export_audit_trail(self) -> list[dict]:
        """Export all decision records as a JSON-serialisable audit trail."""
        return self.provenance.export_audit_trail()

    def export_audit_json(self, indent: int = 2) -> str:
        """Return the full audit trail as a formatted JSON string."""
        return json.dumps(self.export_audit_trail(), indent=indent)

    def add_exporter(self, exporter) -> "ControlPlane":
        """
        Attach an observability exporter (OTel, Prometheus, Webhook, etc.).
        Exporters are called after every evaluate() call.

        Example::

            from eaigins_sdk.observability import StructuredLogExporter
            cp.add_exporter(StructuredLogExporter())
        """
        self._exporters.append(exporter)
        exporter.on_start()
        return self

    def remove_exporters(self) -> None:
        """Detach and flush all exporters."""
        for exp in self._exporters:
            exp.flush()
        self._exporters.clear()

    def reset_stats(self) -> None:
        """Reset session counters (does not clear provenance records)."""
        self._intercepted = self._allowed = self._escalated = self._blocked = 0

    # ── Private ───────────────────────────────────────────────────────────────

    def _record(
        self,
        action:    AgentAction,
        fw:        FirewallResult,
        pol:       PolicyResult,
        risk:      RiskScore,
        decision:  ExecutionDecision,
        reason:    str,
    ) -> GovernanceDecision:
        return self.provenance.record(action, fw, pol, risk, decision, reason)

    @staticmethod
    def _build_reason(
        decision: ExecutionDecision,
        fw:       FirewallResult,
        pol:      PolicyResult,
        risk:     RiskScore,
    ) -> str:
        parts = []
        if not fw.passed:
            parts.append(f"Firewall: {fw.threat_type.value} (score={fw.threat_score:.2f})")
        if pol.violated_policies:
            parts.append(f"Policies violated: {len(pol.violated_policies)}")
        if pol.required_approvals:
            parts.append(f"Human approvals required: {len(pol.required_approvals)}")
        parts.append(f"Risk: {risk.risk_level.value} ({risk.total_score:.2f})")
        parts.append({
            ExecutionDecision.ALLOW:    "✅ ALLOWED — Action proceeds to enterprise infrastructure.",
            ExecutionDecision.ESCALATE: "⚠️  ESCALATED — Requires human approval before execution.",
            ExecutionDecision.BLOCK:    "🛑 BLOCKED — Action prevented from reaching enterprise systems.",
        }[decision])
        return " | ".join(parts)

    def _emit(self, decision) -> None:
        """Send decision to all registered exporters."""
        for exporter in self._exporters:
            try:
                exporter.export(decision)
            except Exception as e:
                import logging
                logging.getLogger("eaigins.control_plane").warning(
                    f"Exporter {exporter.__class__.__name__} failed: {e}"
                )

    def __repr__(self) -> str:
        s = self.stats
        return (f"ControlPlane(name={self.name!r}, policies={s['policies']}, "
                f"intercepted={s['intercepted']}, "
                f"allowed={s['allowed']}, escalated={s['escalated']}, blocked={s['blocked']})")
