"""Approval Manager — human-in-the-loop for sensitive actions.

When the Permission Gateway returns REQUIRE_APPROVAL, the Approval Manager
creates an approval request that a human must explicitly grant or deny.

All approvals are audit-tracked: who approved, when, and why.
"""

from __future__ import annotations

import json
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Any

from cyberbrain26.data_plane.sqlite_store import SQLiteDatabase
from cyberbrain26.security_plane.identity import AgentIdentity, TrustLevel


class ApprovalStatus(str, Enum):
    """Status of an approval request."""

    PENDING = "pending"
    APPROVED = "approved"
    DENIED = "denied"
    EXPIRED = "expired"
    CANCELLED = "cancelled"


class ApprovalDecision(str, Enum):
    """Final decision on an approval request."""

    APPROVED = "approved"
    DENIED = "denied"


@dataclass
class ApprovalRequest:
    """A request for human approval of a sensitive action."""

    request_id: str
    agent: AgentIdentity
    action: str
    resource: str | None = None
    context: dict[str, Any] = field(default_factory=dict)
    risk_score: float = 0.0
    status: ApprovalStatus = ApprovalStatus.PENDING
    created_at: float = field(default_factory=lambda: time.time())
    reviewed_at: float | None = None
    reviewer: str | None = None
    reason: str = ""
    decision: ApprovalDecision | None = None
    metadata: dict[str, Any] = field(default_factory=dict)


@dataclass
class ApprovalRecord:
    """Audit record of an approval decision."""

    request_id: str
    agent_id: str
    action: str
    resource: str | None
    decision: ApprovalDecision
    reviewer: str
    timestamp: float
    reason: str = ""


class ApprovalManager:
    """Manages human approval workflow for sensitive actions.

    Usage:
        manager = ApprovalManager()
        request = manager.create_request(identity, "execute_shell", "/bin/rm")
        # Send to human for approval
        manager.approve(request.request_id, "user@example.com", "Approved")
        record = manager.get_record(request.request_id)
    """

    DEFAULT_TIMEOUT_SECONDS: float = 3600.0  # 1 hour

    def __init__(
        self,
        timeout_seconds: float | None = None,
        *,
        database: SQLiteDatabase | None = None,
    ) -> None:
        self.timeout_seconds = timeout_seconds or self.DEFAULT_TIMEOUT_SECONDS
        self.database = database
        self._requests: dict[str, ApprovalRequest] = {}
        self._records: list[ApprovalRecord] = []
        if self.database is not None:
            self._load_persisted()

    def create_request(
        self,
        agent: AgentIdentity,
        action: str,
        resource: str | None = None,
        context: dict[str, Any] | None = None,
        risk_score: float = 0.0,
        reason: str = "",
    ) -> ApprovalRequest:
        """Create a new approval request.

        Returns the request with a unique ID.
        """
        request_id = f"approval_{uuid.uuid4().hex[:12]}_{agent.agent_id}"
        request = ApprovalRequest(
            request_id=request_id,
            agent=agent,
            action=action,
            resource=resource,
            context=context or {},
            risk_score=risk_score,
            reason=reason,
        )
        if self.database is not None:
            with self.database.transaction() as connection:
                connection.execute(
                    """
                    INSERT INTO approval_requests(
                        request_id, agent_json, action, resource, context_json, risk_score,
                        status, created_at, reviewed_at, reviewer, reason, decision, metadata_json
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                    """,
                    (
                        request.request_id,
                        json.dumps(self._agent_to_json(request.agent), separators=(",", ":")),
                        request.action,
                        request.resource,
                        json.dumps(request.context, separators=(",", ":"), allow_nan=False),
                        request.risk_score,
                        request.status.value,
                        request.created_at,
                        request.reviewed_at,
                        request.reviewer,
                        request.reason,
                        request.decision.value if request.decision else None,
                        json.dumps(request.metadata, separators=(",", ":"), allow_nan=False),
                    ),
                )
        self._requests[request_id] = request
        return request

    def approve(
        self,
        request_id: str,
        reviewer: str,
        reason: str = "",
    ) -> ApprovalRequest | None:
        """Approve a pending request.

        Returns the updated request, or None if not found.
        """
        request = self.get_request(request_id)
        if request is None or request.status != ApprovalStatus.PENDING:
            return None

        return self._decide(request, ApprovalDecision.APPROVED, reviewer, reason)

    def deny(
        self,
        request_id: str,
        reviewer: str,
        reason: str = "",
    ) -> ApprovalRequest | None:
        """Deny a pending request.

        Returns the updated request, or None if not found.
        """
        request = self.get_request(request_id)
        if request is None or request.status != ApprovalStatus.PENDING:
            return None

        return self._decide(request, ApprovalDecision.DENIED, reviewer, reason)

    def get_request(self, request_id: str) -> ApprovalRequest | None:
        """Get a request by ID."""
        if self.database is not None:
            current = self._load_request(request_id)
            if current is None:
                self._requests.pop(request_id, None)
                return None
            self._requests[request_id] = current
            return current
        return self._requests.get(request_id)

    def get_record(self, request_id: str) -> ApprovalRecord | None:
        """Get the audit record for a request."""
        if self.database is not None:
            with self.database.connection() as connection:
                row = connection.execute(
                    "SELECT * FROM approval_records WHERE request_id = ?", (request_id,)
                ).fetchone()
            return self._row_to_record(row) if row is not None else None
        for record in self._records:
            if record.request_id == request_id:
                return record
        return None

    def list_pending(self) -> list[ApprovalRequest]:
        """List all pending approval requests."""
        if self.database is not None:
            with self.database.connection() as connection:
                rows = connection.execute(
                    "SELECT * FROM approval_requests WHERE status = ? ORDER BY created_at",
                    (ApprovalStatus.PENDING.value,),
                ).fetchall()
            pending = [self._row_to_request(row) for row in rows]
            self._requests.update({request.request_id: request for request in pending})
            return pending
        return [r for r in self._requests.values() if r.status == ApprovalStatus.PENDING]

    def list_records(self, agent_id: str | None = None) -> list[ApprovalRecord]:
        """List all approval records, optionally filtered by agent_id."""
        if self.database is not None:
            with self.database.connection() as connection:
                if agent_id is None:
                    rows = connection.execute(
                        "SELECT * FROM approval_records ORDER BY timestamp"
                    ).fetchall()
                else:
                    rows = connection.execute(
                        "SELECT * FROM approval_records WHERE agent_id = ? ORDER BY timestamp",
                        (agent_id,),
                    ).fetchall()
            return [self._row_to_record(row) for row in rows]
        if agent_id is None:
            return list(self._records)
        return [r for r in self._records if r.agent_id == agent_id]

    def cancel(self, request_id: str) -> ApprovalRequest | None:
        """Cancel a pending request."""
        request = self.get_request(request_id)
        if request is None or request.status != ApprovalStatus.PENDING:
            return None
        if self.database is not None:
            with self.database.transaction() as connection:
                updated = connection.execute(
                    "UPDATE approval_requests SET status = ? WHERE request_id = ? AND status = ?",
                    (
                        ApprovalStatus.CANCELLED.value,
                        request_id,
                        ApprovalStatus.PENDING.value,
                    ),
                ).rowcount
            if updated != 1:
                current = self._load_request(request_id)
                if current is not None:
                    self._requests[request_id] = current
                return None
        request.status = ApprovalStatus.CANCELLED
        return request

    def check_expired(self) -> list[ApprovalRequest]:
        """Check for and mark expired requests.

        Returns list of requests that were marked as expired.
        """
        now = time.time()
        expired: list[ApprovalRequest] = []
        if self.database is not None:
            self._load_persisted()
        for request in self._requests.values():
            if request.status == ApprovalStatus.PENDING and now - request.created_at > self.timeout_seconds:
                if self.database is not None:
                    with self.database.transaction() as connection:
                        updated = connection.execute(
                            "UPDATE approval_requests SET status = ? WHERE request_id = ? "
                            "AND status = ?",
                            (
                                ApprovalStatus.EXPIRED.value,
                                request.request_id,
                                ApprovalStatus.PENDING.value,
                            ),
                        ).rowcount
                    if updated != 1:
                        current = self._load_request(request.request_id)
                        if current is not None:
                            self._requests[request.request_id] = current
                        continue
                request.status = ApprovalStatus.EXPIRED
                expired.append(request)
        return expired

    def prune_expired(self, *, now: float | None = None) -> int:
        """Apply configured retention and reload the manager's persisted state."""
        if self.database is None:
            return 0
        deleted = self.database.prune_expired(now=now)["approvals"]
        self._requests = {}
        self._records = []
        self._load_persisted()
        return deleted

    def _decide(
        self,
        request: ApprovalRequest,
        decision: ApprovalDecision,
        reviewer: str,
        reason: str,
    ) -> ApprovalRequest | None:
        timestamp = time.time()
        status = (
            ApprovalStatus.APPROVED
            if decision is ApprovalDecision.APPROVED
            else ApprovalStatus.DENIED
        )
        record = ApprovalRecord(
            request_id=request.request_id,
            agent_id=request.agent.agent_id,
            action=request.action,
            resource=request.resource,
            decision=decision,
            reviewer=reviewer,
            timestamp=timestamp,
            reason=reason,
        )
        if self.database is not None:
            with self.database.transaction() as connection:
                updated = connection.execute(
                    "UPDATE approval_requests SET status = ?, reviewed_at = ?, reviewer = ?, "
                    "reason = CASE WHEN ? = '' THEN reason ELSE ? END, decision = ? "
                    "WHERE request_id = ? AND status = ?",
                    (
                        status.value,
                        timestamp,
                        reviewer,
                        reason,
                        reason,
                        decision.value,
                        request.request_id,
                        ApprovalStatus.PENDING.value,
                    ),
                ).rowcount
                if updated == 1:
                    connection.execute(
                        """
                        INSERT INTO approval_records(
                            request_id, agent_id, action, resource, decision,
                            reviewer, timestamp, reason
                        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                        """,
                        (
                            record.request_id,
                            record.agent_id,
                            record.action,
                            record.resource,
                            record.decision.value,
                            record.reviewer,
                            record.timestamp,
                            record.reason,
                        ),
                    )
            if updated != 1:
                current = self._load_request(request.request_id)
                if current is not None:
                    self._requests[request.request_id] = current
                return None
        request.status = status
        request.decision = decision
        request.reviewed_at = timestamp
        request.reviewer = reviewer
        if reason:
            request.reason = reason
        self._records.append(record)
        return request

    def _load_persisted(self) -> None:
        assert self.database is not None
        with self.database.connection() as connection:
            request_rows = connection.execute(
                "SELECT * FROM approval_requests ORDER BY created_at"
            ).fetchall()
            record_rows = connection.execute(
                "SELECT * FROM approval_records ORDER BY timestamp"
            ).fetchall()
        self._requests = {
            request.request_id: request
            for request in (self._row_to_request(row) for row in request_rows)
        }
        self._records = [self._row_to_record(row) for row in record_rows]

    def _load_request(self, request_id: str) -> ApprovalRequest | None:
        assert self.database is not None
        with self.database.connection() as connection:
            row = connection.execute(
                "SELECT * FROM approval_requests WHERE request_id = ?", (request_id,)
            ).fetchone()
        return self._row_to_request(row) if row is not None else None

    @staticmethod
    def _agent_to_json(agent: AgentIdentity) -> dict[str, Any]:
        trust_level = getattr(agent.trust_level, "value", agent.trust_level)
        scopes = [getattr(scope, "value", scope) for scope in agent.scopes]
        return {
            "agent_id": agent.agent_id,
            "role": agent.role,
            "trust_level": trust_level,
            "scopes": scopes,
            "owner": agent.owner,
            "metadata": agent.metadata,
        }

    @staticmethod
    def _row_to_request(row: Any) -> ApprovalRequest:
        agent_data = json.loads(row["agent_json"])
        agent = AgentIdentity(
            agent_id=agent_data["agent_id"],
            role=agent_data["role"],
            trust_level=TrustLevel(agent_data["trust_level"]),
            scopes=set(agent_data["scopes"]),
            owner=agent_data["owner"],
            metadata=agent_data["metadata"],
        )
        decision = ApprovalDecision(row["decision"]) if row["decision"] else None
        return ApprovalRequest(
            request_id=row["request_id"],
            agent=agent,
            action=row["action"],
            resource=row["resource"],
            context=json.loads(row["context_json"]),
            risk_score=row["risk_score"],
            status=ApprovalStatus(row["status"]),
            created_at=row["created_at"],
            reviewed_at=row["reviewed_at"],
            reviewer=row["reviewer"],
            reason=row["reason"],
            decision=decision,
            metadata=json.loads(row["metadata_json"]),
        )

    @staticmethod
    def _row_to_record(row: Any) -> ApprovalRecord:
        return ApprovalRecord(
            request_id=row["request_id"],
            agent_id=row["agent_id"],
            action=row["action"],
            resource=row["resource"],
            decision=ApprovalDecision(row["decision"]),
            reviewer=row["reviewer"],
            timestamp=row["timestamp"],
            reason=row["reason"],
        )


__all__ = [
    "ApprovalDecision",
    "ApprovalManager",
    "ApprovalRecord",
    "ApprovalRequest",
    "ApprovalStatus",
]
