"""Delegation lifecycle — GCAP-SPEC-LLM-PRINCIPAL-BROKER-0001 §3.

Originally a separate llm-principal-broker service that called GSAP's
``/governance/authorize/`` endpoint over HTTP. Now an in-process module:
``create_delegation`` opens an AsyncSession against GSAP's own DB engine
and inserts an ``AuthorizationContextDB`` row directly. No process boundary,
no HTTP hop, same Chronicle event emission.
"""

import logging
import uuid
from datetime import datetime, timedelta, UTC

from sqlmodel.ext.asyncio.session import AsyncSession

from gsap_broker import chronicle
from gsap_broker.db import engine
from gsap_broker.db_models import AuthorizationContextDB
from gsap_broker.settings import settings as gsap_settings

from .models import (
    AgentPrincipal,
    DelegationRequest,
    DelegationResponse,
    DelegationScope,
)
from .registrars.factory import create_registrar
from .storage import (
    DelegationDB,
    create_delegation as db_create,
    expire_stale,
    get_delegation as db_get,
    revoke_delegation as db_revoke,
)

logger = logging.getLogger(__name__)


class DelegationManager:
    """Owns the delegation lifecycle. Constructed once at app startup."""

    def __init__(self, config=gsap_settings):
        self.config = config
        self.registrar = create_registrar(config)

    async def create_delegation(
        self, request: DelegationRequest, delegator_did: str,
        delegator_capability_mask: int = 0x7,
    ) -> DelegationResponse:
        delegation_id = f"del-{uuid.uuid4().hex[:8]}"
        scope = request.scope or DelegationScope()

        # Fix C-9: delegated capability cannot exceed delegator's
        requested_mask = _capability_mask_for(scope.capability_ceiling)
        if requested_mask & ~delegator_capability_mask:
            raise ValueError(
                f"Delegated capability ({scope.capability_ceiling} = {requested_mask}) "
                f"exceeds delegator's capability ({delegator_capability_mask})"
            )
        now = datetime.now(UTC)
        expires_at = now + timedelta(minutes=scope.max_ttl_minutes)

        agent_did = f"did:web:guildhouse.dev/agent/{request.agent_type}-{delegation_id}"
        delegator_short = delegator_did.rsplit("/", 1)[-1]
        agent_display = f"{request.agent_type} (delegated by {delegator_short})"

        # 1. Register agent identity via the configured registrar.
        credentials = await self.registrar.register_agent(
            delegation_id=delegation_id,
            agent_type=request.agent_type,
            delegator_id=delegator_did,
            display_name=agent_display,
            expires_at=expires_at.isoformat(),
            metadata={"model": request.agent_model},
        )

        # 2. Issue a delegated AuthorizationContext directly against the
        #    GSAP DB. Mirrors routers/authorize.py for the on_behalf_of
        #    trusted-caller path. We bypass the HTTP layer because GSAP
        #    is calling itself.
        try:
            ac_result = await self._issue_delegated_ac(
                delegation_id=delegation_id,
                agent_did=agent_did,
                accord_template=request.accord_template,
                expires_at=expires_at,
                scope=scope,
            )
        except Exception as e:
            await self.registrar.delete_agent(credentials.client_id)
            raise RuntimeError(f"Failed to issue delegated AC: {e}")

        # 3. Chronicle event for the delegation creation.
        chronicle_cid = await chronicle.emit(
            "DELEGATION_CREATED",
            {
                "event_code": "0x3001",
                "delegation_id": delegation_id,
                "delegator_did": delegator_did,
                "agent_did": agent_did,
                "agent_type": request.agent_type,
                "scope": scope.model_dump(),
                "timestamp": now.isoformat(),
            },
        )

        # 4. Persist the delegation row.
        delegation = DelegationDB(
            delegation_id=delegation_id,
            status="active",
            agent_type=request.agent_type,
            agent_model=request.agent_model,
            agent_did=agent_did,
            agent_keycloak_client_id=credentials.client_id,
            delegator_did=delegator_did,
            delegator_ac_id=request.delegator_ac_id,
            delegated_ac_id=ac_result.get("context_id", ""),
            capability_ceiling=scope.capability_ceiling,
            ceremony_required_for=",".join(scope.ceremony_required_for),
            max_commands=scope.max_commands,
            created_at=now.replace(tzinfo=None),
            expires_at=expires_at.replace(tzinfo=None),
            chronicle_cid=chronicle_cid or None,
        )
        await db_create(delegation)

        logger.info(
            "Delegation created: %s (%s -> %s) via %s",
            delegation_id,
            delegator_did,
            agent_did,
            credentials.idp_backend,
        )

        return DelegationResponse(
            delegation_id=delegation_id,
            agent_principal=AgentPrincipal(
                did=agent_did,
                keycloak_client_id=credentials.client_id,
                display_name=credentials.agent_display_name,
            ),
            delegated_ac=ac_result,
            agent_token=credentials.client_secret,
            expires_at=expires_at.isoformat(),
            max_commands=scope.max_commands,
            chronicle_cid=chronicle_cid,
        )

    async def _issue_delegated_ac(
        self,
        delegation_id: str,
        agent_did: str,
        accord_template: str,
        expires_at: datetime,
        scope: DelegationScope,
    ) -> dict:
        """Insert an AuthorizationContextDB row representing the delegated AC.

        Mirrors the trusted-caller (on_behalf_of) path of
        ``routers/authorize.authorize`` without going through HTTP.
        """
        ctx_id = uuid.uuid4()
        now = datetime.now(UTC)

        ac_db = AuthorizationContextDB(
            context_id=ctx_id,
            principal_did=agent_did,
            driver_id="delegation",
            playbook=f"delegation:{delegation_id}",
            corpus_entry_cid="sha256:delegated-agent",
            parameters_cid=f"sha256:delegation-{delegation_id}",
            accord_template=accord_template,
            capability_mask=_capability_mask_for(scope.capability_ceiling),
            idp_vendor="delegation",
            token_jti="",
            elevation_active=[],
            mfa_satisfied=False,
            status="authorized",
            issued_at=now.replace(tzinfo=None),
            expires_at=expires_at.replace(tzinfo=None),
            session_mode=True,
        )

        async with AsyncSession(engine) as session:
            session.add(ac_db)
            await session.commit()

        return {
            "status": "authorized",
            "context_id": str(ctx_id),
            "principal_did": agent_did,
            "delegation_id": delegation_id,
            "capability_ceiling": scope.capability_ceiling,
            "expires_at": expires_at.isoformat(),
        }

    async def revoke_delegation(self, delegation_id: str, reason: str) -> bool:
        delegation = await db_get(delegation_id)
        if not delegation or delegation.status != "active":
            return False

        if delegation.agent_keycloak_client_id:
            await self.registrar.delete_agent(delegation.agent_keycloak_client_id)

        await db_revoke(delegation_id, reason)

        await chronicle.emit(
            "DELEGATION_REVOKED",
            {
                "event_code": "0x3003",
                "delegation_id": delegation_id,
                "reason": reason,
                "timestamp": datetime.now(UTC).isoformat(),
            },
        )

        logger.info("Delegation revoked: %s (%s)", delegation_id, reason)
        return True

    async def cleanup_expired(self) -> int:
        """Expire stale delegations and clean up IdP registrations."""
        expired = await expire_stale()
        for d in expired:
            if d.agent_keycloak_client_id:
                await self.registrar.delete_agent(d.agent_keycloak_client_id)
            await chronicle.emit(
                "DELEGATION_EXPIRED",
                {
                    "event_code": "0x3004",
                    "delegation_id": d.delegation_id,
                    "timestamp": datetime.now(UTC).isoformat(),
                },
            )
        if expired:
            logger.info("Expired %d stale delegations", len(expired))
        return len(expired)


def _capability_mask_for(ceiling: str) -> int:
    """Convert a capability ceiling string to the GSAP capability mask bits."""
    bits = {"CAP_READ": 1, "CAP_PROPOSE": 2, "CAP_MUTATE": 4, "CAP_ADMIN": 8}
    if ceiling in bits:
        # Ceiling is inclusive: CAP_MUTATE means READ | PROPOSE | MUTATE.
        order = ["CAP_READ", "CAP_PROPOSE", "CAP_MUTATE", "CAP_ADMIN"]
        idx = order.index(ceiling)
        mask = 0
        for cap in order[: idx + 1]:
            mask |= bits[cap]
        return mask
    return 1  # default to READ
