"""One event-loop-owned, in-memory approval registry shared by every adapter/UI."""
from __future__ import annotations

import asyncio
import logging
from collections import OrderedDict
from dataclasses import dataclass
from datetime import datetime, timezone
from uuid import uuid4

from .models import Approval, ControlError

logger = logging.getLogger(__name__)


@dataclass
class Pending:
    approval: Approval
    future: asyncio.Future


class ApprovalRegistry:
    def __init__(self, timeout=None, on_change=None):
        # Legacy timeout argument is accepted but never grants decision authority.
        self.timeout_policy = "wait"
        self.on_change = on_change
        self.entries: OrderedDict[str, Pending] = OrderedDict()
        # A presentation belongs to an authenticated WebSocket incarnation, not
        # just a reusable device ID. Only its currently offered ID is actionable.
        self.presentations: dict[str, str] = {}

    def changed(self):
        if self.on_change:
            self.on_change()

    def create(self, *, agent, session_id, project_id, kind, title, detail, owner, request_identity):
        if len(self.list_pending()) >= 128:
            raise ControlError("too many pending approvals", 429)
        if any(e.approval.owner == owner and e.approval.request_identity == request_identity
               for e in self.entries.values()):
            raise ControlError("duplicate approval request identity", 409)
        loop = asyncio.get_running_loop()
        now = datetime.now(timezone.utc)
        approval = Approval(approval_id=uuid4().hex, agent=agent, session_id=session_id,
            project_id=project_id, kind=kind, title=title[:160], detail=detail[:4000],
            owner=owner, request_identity=request_identity, created_at=now,
            expires_at=None)
        self.entries[approval.approval_id] = Pending(approval, loop.create_future())
        # Retain only a small terminal tail; pending futures are never evicted.
        for key in list(self.entries):
            if len(self.entries) <= 384:
                break
            if self.entries[key].approval.state != "PENDING":
                del self.entries[key]
        self.changed()
        return approval

    def get(self, approval_id):
        entry = self.entries.get(approval_id) if isinstance(approval_id, str) else None
        return entry.approval if entry else None

    def list_pending(self, agent=None, session_id=None):
        return [e.approval for e in self.entries.values() if e.approval.state == "PENDING"
                and (agent is None or e.approval.agent == agent)
                and (session_id is None or e.approval.session_id == session_id)]

    def finish(self, entry, state):
        if entry.approval.state != "PENDING":
            return False
        entry.approval.state = state
        if not entry.future.done():
            entry.future.set_result(state)
        self.changed()
        return True

    def expire(self):
        """Compatibility seam: 0.4.0 only supports timeout_policy=wait."""
        return

    def resolve(self, approval_id, decision, *, device_connection=None):
        approval = self.get(approval_id)
        if (decision not in ("approve", "reject") or approval is None or approval.state != "PENDING"
                or (device_connection is not None and self.presentations.get(device_connection) != approval_id)):
            logger.warning("ignored invalid, stale or unauthorized approval response")
            return False
        return self.finish(self.entries[approval_id], "APPROVED" if decision == "approve" else "REJECTED")

    def cancel(self, approval_id):
        entry = self.entries.get(approval_id)
        return bool(entry and self.finish(entry, "CANCELLED"))

    def cancel_owner(self, owner):
        for entry in list(self.entries.values()):
            if entry.approval.owner == owner:
                self.finish(entry, "CANCELLED")

    def cancel_session(self, agent, session_id):
        for approval in self.list_pending(agent, session_id):
            self.cancel(approval.approval_id)

    async def wait(self, approval):
        return await asyncio.shield(self.entries[approval.approval_id].future)

    def wire(self, approval):
        return {"type": "approval_request", **approval.model_dump(mode="json"), "expires_in": None}

    def overlay(self, session):
        pending = self.list_pending(session.agent, session.session_id)
        if pending:
            session.state = "WAITING_APPROVAL"
            session.waiting_reason = pending[0].title + ": " + pending[0].detail
        return session

    def close(self):
        for entry in list(self.entries.values()):
            self.finish(entry, "CANCELLED")
        self.presentations.clear()
