from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from enum import StrEnum
import hashlib
import json
from typing import Any, Protocol, runtime_checkable
import uuid

from sqlalchemy import DateTime, Index, Integer, JSON, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, Session, mapped_column

from govoplan_core.db.base import Base, TimestampMixin, utcnow


class OwnershipTransferKind(StrEnum):
    OWNER_INITIATED = "owner_initiated"
    TARGET_REQUESTED = "target_requested"
    ADMINISTRATIVE_RECOVERY = "administrative_recovery"


class OwnershipTransferStatus(StrEnum):
    AWAITING_OWNER_APPROVAL = "awaiting_owner_approval"
    AWAITING_TARGET_ACCEPTANCE = "awaiting_target_acceptance"
    AWAITING_RECOVERY_APPROVALS = "awaiting_recovery_approvals"
    RECOVERY_SCHEDULED = "recovery_scheduled"
    COMPLETED = "completed"
    DECLINED = "declined"
    CANCELLED = "cancelled"
    EXPIRED = "expired"


TERMINAL_OWNERSHIP_TRANSFER_STATUSES = frozenset(
    {
        OwnershipTransferStatus.COMPLETED.value,
        OwnershipTransferStatus.DECLINED.value,
        OwnershipTransferStatus.CANCELLED.value,
        OwnershipTransferStatus.EXPIRED.value,
    }
)


class OwnershipTransferError(ValueError):
    pass


class OwnershipAuthorizationError(PermissionError):
    pass


class OwnershipIdempotencyConflict(OwnershipTransferError):
    pass


class OwnershipTransferExpired(OwnershipTransferError):
    pass


@dataclass(frozen=True, slots=True)
class OwnershipSubjectRef:
    type: str
    id: str
    label: str | None = None
    scopes: frozenset[str] = frozenset()
    group_ids: frozenset[str] = frozenset()
    recently_authenticated: bool = False

    def __post_init__(self) -> None:
        if not self.type.strip() or not self.id.strip():
            raise ValueError("Ownership subjects require a type and id")


@dataclass(frozen=True, slots=True)
class OwnershipResourceRef:
    module_id: str
    resource_type: str
    resource_id: str

    def __post_init__(self) -> None:
        if not self.module_id.strip() or not self.resource_type.strip() or not self.resource_id.strip():
            raise ValueError("Ownership resources require module, type, and id")


@dataclass(frozen=True, slots=True)
class OwnershipActionDecision:
    allowed: bool
    reason: str | None = None
    requirements: tuple[str, ...] = ()


@dataclass(frozen=True, slots=True)
class OwnershipTransferPolicy:
    default_expiry_days: int = 7
    min_expiry_days: int = 1
    max_expiry_days: int = 30
    recovery_assurance_profile: str = "standard"
    recovery_required_approvals: int = 2
    recovery_delay_hours: int = 24
    recent_authentication_required: bool = True

    def __post_init__(self) -> None:
        if not 1 <= self.min_expiry_days <= self.default_expiry_days <= self.max_expiry_days:
            raise ValueError("Ownership transfer expiry policy is inconsistent")
        if self.recovery_required_approvals < 1:
            raise ValueError("Ownership recovery requires at least one approval")
        if self.recovery_delay_hours < 0:
            raise ValueError("Ownership recovery delay cannot be negative")

    @classmethod
    def development(cls) -> OwnershipTransferPolicy:
        return cls(
            recovery_assurance_profile="development-single-admin",
            recovery_required_approvals=1,
            recovery_delay_hours=0,
            recent_authentication_required=False,
        )


@runtime_checkable
class ResourceOwnershipProvider(Protocol):
    def current_owner(
        self,
        session: object,
        *,
        tenant_id: str,
        resource_id: str,
    ) -> OwnershipSubjectRef | None:
        ...

    def authorize_ownership_action(
        self,
        session: object,
        *,
        tenant_id: str,
        resource_id: str,
        action: str,
        actor: OwnershipSubjectRef,
        current_owner: OwnershipSubjectRef,
        target_owner: OwnershipSubjectRef,
    ) -> OwnershipActionDecision:
        ...

    def apply_owner(
        self,
        session: object,
        *,
        tenant_id: str,
        resource_id: str,
        expected_owner: OwnershipSubjectRef,
        target_owner: OwnershipSubjectRef,
        actor: OwnershipSubjectRef,
        reason: str | None,
    ) -> None:
        ...


@dataclass(frozen=True, slots=True)
class OwnershipProviderRegistration:
    resource_type: str
    provider: ResourceOwnershipProvider
    module_id: str | None = None


class OwnershipTransfer(Base, TimestampMixin):
    __tablename__ = "core_ownership_transfers"
    __table_args__ = (
        UniqueConstraint(
            "tenant_id",
            "resource_module",
            "idempotency_key",
            name="uq_core_ownership_transfer_idempotency",
        ),
        Index(
            "ix_core_ownership_transfer_resource",
            "tenant_id",
            "resource_module",
            "resource_type",
            "resource_id",
            "status",
        ),
        Index(
            "ix_core_ownership_transfer_expiry",
            "status",
            "expires_at",
        ),
    )

    id: Mapped[str] = mapped_column(
        String(36),
        primary_key=True,
        default=lambda: str(uuid.uuid4()),
    )
    tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
    resource_module: Mapped[str] = mapped_column(String(100), nullable=False)
    resource_type: Mapped[str] = mapped_column(String(100), nullable=False)
    resource_id: Mapped[str] = mapped_column(String(255), nullable=False)
    kind: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
    status: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
    current_owner_type: Mapped[str] = mapped_column(String(40), nullable=False)
    current_owner_id: Mapped[str] = mapped_column(String(255), nullable=False)
    target_owner_type: Mapped[str] = mapped_column(String(40), nullable=False)
    target_owner_id: Mapped[str] = mapped_column(String(255), nullable=False)
    initiated_by_type: Mapped[str] = mapped_column(String(40), nullable=False)
    initiated_by_id: Mapped[str] = mapped_column(String(255), nullable=False)
    owner_approved_by_type: Mapped[str | None] = mapped_column(String(40))
    owner_approved_by_id: Mapped[str | None] = mapped_column(String(255))
    target_accepted_by_type: Mapped[str | None] = mapped_column(String(40))
    target_accepted_by_id: Mapped[str | None] = mapped_column(String(255))
    reason: Mapped[str | None] = mapped_column(Text)
    assurance_profile: Mapped[str | None] = mapped_column(String(80))
    required_approvals: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
    approvals: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
    decisions: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
    idempotency_key: Mapped[str] = mapped_column(String(200), nullable=False)
    canonical_request_hash: Mapped[str] = mapped_column(String(64), nullable=False)
    expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
    execute_after: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
    completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
    declined_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
    cancelled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
    expired_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
    revision: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
    metadata_: Mapped[dict[str, Any]] = mapped_column(
        "metadata",
        JSON,
        default=dict,
        nullable=False,
    )


def start_owner_initiated_transfer(
    session: Session,
    *,
    tenant_id: str,
    resource: OwnershipResourceRef,
    provider: ResourceOwnershipProvider,
    actor: OwnershipSubjectRef,
    target_owner: OwnershipSubjectRef,
    idempotency_key: str,
    reason: str | None = None,
    expiry_days: int | None = None,
    policy: OwnershipTransferPolicy = OwnershipTransferPolicy(),
    now: datetime | None = None,
) -> OwnershipTransfer:
    return _start_transfer(
        session,
        tenant_id=tenant_id,
        resource=resource,
        provider=provider,
        actor=actor,
        target_owner=target_owner,
        kind=OwnershipTransferKind.OWNER_INITIATED,
        initial_status=OwnershipTransferStatus.AWAITING_TARGET_ACCEPTANCE,
        authorization_action="propose_transfer",
        idempotency_key=idempotency_key,
        reason=reason,
        expiry_days=expiry_days,
        policy=policy,
        now=now,
        assurance_profile=None,
        required_approvals=1,
        execute_after=None,
        metadata={},
    )


def request_ownership(
    session: Session,
    *,
    tenant_id: str,
    resource: OwnershipResourceRef,
    provider: ResourceOwnershipProvider,
    actor: OwnershipSubjectRef,
    target_owner: OwnershipSubjectRef | None = None,
    idempotency_key: str,
    reason: str | None = None,
    expiry_days: int | None = None,
    policy: OwnershipTransferPolicy = OwnershipTransferPolicy(),
    now: datetime | None = None,
) -> OwnershipTransfer:
    requested_owner = target_owner or actor
    return _start_transfer(
        session,
        tenant_id=tenant_id,
        resource=resource,
        provider=provider,
        actor=actor,
        target_owner=requested_owner,
        kind=OwnershipTransferKind.TARGET_REQUESTED,
        initial_status=OwnershipTransferStatus.AWAITING_OWNER_APPROVAL,
        authorization_action="request_ownership",
        idempotency_key=idempotency_key,
        reason=reason,
        expiry_days=expiry_days,
        policy=policy,
        now=now,
        assurance_profile=None,
        required_approvals=1,
        execute_after=None,
        metadata={},
    )


def approve_ownership_request(
    session: Session,
    *,
    transfer: OwnershipTransfer,
    provider: ResourceOwnershipProvider,
    actor: OwnershipSubjectRef,
    now: datetime | None = None,
) -> OwnershipTransfer:
    effective_now = _utc(now)
    _require_active(session, transfer, effective_now)
    if (
        transfer.kind != OwnershipTransferKind.TARGET_REQUESTED.value
        or transfer.status != OwnershipTransferStatus.AWAITING_OWNER_APPROVAL.value
    ):
        raise OwnershipTransferError("Ownership request is not awaiting owner approval")
    _authorize(provider, session, transfer, actor, "approve_requested_transfer")
    transfer.owner_approved_by_type = actor.type
    transfer.owner_approved_by_id = actor.id
    transfer.status = OwnershipTransferStatus.AWAITING_TARGET_ACCEPTANCE.value
    _touch(transfer)
    _record_decision(
        transfer,
        action="owner_approved",
        actor=actor,
        decided_at=effective_now,
    )
    _emit_transfer_event(session, transfer, "owner_approved", actor)
    session.flush()
    return transfer


def accept_ownership_transfer(
    session: Session,
    *,
    transfer: OwnershipTransfer,
    provider: ResourceOwnershipProvider,
    actor: OwnershipSubjectRef,
    now: datetime | None = None,
) -> OwnershipTransfer:
    effective_now = _utc(now)
    _require_active(session, transfer, effective_now)
    if transfer.status != OwnershipTransferStatus.AWAITING_TARGET_ACCEPTANCE.value:
        raise OwnershipTransferError("Ownership transfer is not awaiting target acceptance")
    action = (
        "accept_group_transfer"
        if transfer.target_owner_type == "group"
        else "accept_transfer"
    )
    _authorize(provider, session, transfer, actor, action)
    current_owner = _owner_ref(transfer, target=False)
    target_owner = _owner_ref(transfer, target=True)
    provider.apply_owner(
        session,
        tenant_id=transfer.tenant_id,
        resource_id=transfer.resource_id,
        expected_owner=current_owner,
        target_owner=target_owner,
        actor=actor,
        reason=transfer.reason,
    )
    transfer.target_accepted_by_type = actor.type
    transfer.target_accepted_by_id = actor.id
    transfer.status = OwnershipTransferStatus.COMPLETED.value
    transfer.completed_at = effective_now
    _touch(transfer)
    _record_decision(
        transfer,
        action="accepted",
        actor=actor,
        decided_at=effective_now,
    )
    _emit_transfer_event(session, transfer, "completed", actor)
    session.flush()
    return transfer


def decline_ownership_transfer(
    session: Session,
    *,
    transfer: OwnershipTransfer,
    provider: ResourceOwnershipProvider,
    actor: OwnershipSubjectRef,
    now: datetime | None = None,
) -> OwnershipTransfer:
    effective_now = _utc(now)
    _require_active(session, transfer, effective_now)
    _authorize(provider, session, transfer, actor, "decline_transfer")
    transfer.status = OwnershipTransferStatus.DECLINED.value
    transfer.declined_at = effective_now
    _touch(transfer)
    _record_decision(
        transfer,
        action="declined",
        actor=actor,
        decided_at=effective_now,
    )
    _emit_transfer_event(session, transfer, "declined", actor)
    session.flush()
    return transfer


def cancel_ownership_transfer(
    session: Session,
    *,
    transfer: OwnershipTransfer,
    provider: ResourceOwnershipProvider,
    actor: OwnershipSubjectRef,
    now: datetime | None = None,
) -> OwnershipTransfer:
    effective_now = _utc(now)
    _require_active(session, transfer, effective_now)
    _authorize(provider, session, transfer, actor, "cancel_transfer")
    transfer.status = OwnershipTransferStatus.CANCELLED.value
    transfer.cancelled_at = effective_now
    _touch(transfer)
    _record_decision(
        transfer,
        action="cancelled",
        actor=actor,
        decided_at=effective_now,
    )
    _emit_transfer_event(session, transfer, "cancelled", actor)
    session.flush()
    return transfer


def start_administrative_recovery(
    session: Session,
    *,
    tenant_id: str,
    resource: OwnershipResourceRef,
    provider: ResourceOwnershipProvider,
    actor: OwnershipSubjectRef,
    target_owner: OwnershipSubjectRef,
    idempotency_key: str,
    reason: str,
    policy: OwnershipTransferPolicy = OwnershipTransferPolicy(),
    now: datetime | None = None,
) -> OwnershipTransfer:
    if not reason.strip():
        raise OwnershipTransferError("Administrative recovery requires a reason")
    if policy.recent_authentication_required and not actor.recently_authenticated:
        raise OwnershipAuthorizationError(
            "Administrative recovery requires recent authentication"
        )
    effective_now = _utc(now)
    transfer = _start_transfer(
        session,
        tenant_id=tenant_id,
        resource=resource,
        provider=provider,
        actor=actor,
        target_owner=target_owner,
        kind=OwnershipTransferKind.ADMINISTRATIVE_RECOVERY,
        initial_status=OwnershipTransferStatus.AWAITING_RECOVERY_APPROVALS,
        authorization_action="request_recovery",
        idempotency_key=idempotency_key,
        reason=reason,
        expiry_days=policy.default_expiry_days,
        policy=policy,
        now=effective_now,
        assurance_profile=policy.recovery_assurance_profile,
        required_approvals=policy.recovery_required_approvals,
        execute_after=effective_now
        + timedelta(hours=policy.recovery_delay_hours),
        metadata={
            "recent_authentication_required": (
                policy.recent_authentication_required
            ),
            "encryption_keys_included": False,
        },
    )
    return transfer


def approve_administrative_recovery(
    session: Session,
    *,
    transfer: OwnershipTransfer,
    provider: ResourceOwnershipProvider,
    actor: OwnershipSubjectRef,
    now: datetime | None = None,
) -> OwnershipTransfer:
    effective_now = _utc(now)
    _require_active(session, transfer, effective_now)
    _require_recovery_authentication(transfer, actor)
    if (
        transfer.kind != OwnershipTransferKind.ADMINISTRATIVE_RECOVERY.value
        or transfer.status
        not in {
            OwnershipTransferStatus.AWAITING_RECOVERY_APPROVALS.value,
            OwnershipTransferStatus.RECOVERY_SCHEDULED.value,
        }
    ):
        raise OwnershipTransferError("Ownership recovery is not awaiting approval")
    _authorize(provider, session, transfer, actor, "approve_recovery")
    approvals = list(transfer.approvals or [])
    if any(
        item.get("actor_type") == actor.type and item.get("actor_id") == actor.id
        for item in approvals
    ):
        return transfer
    approvals.append(
        {
            "actor_type": actor.type,
            "actor_id": actor.id,
            "approved_at": effective_now.isoformat(),
        }
    )
    transfer.approvals = approvals
    if len(approvals) >= transfer.required_approvals:
        transfer.status = OwnershipTransferStatus.RECOVERY_SCHEDULED.value
    _touch(transfer)
    _record_decision(
        transfer,
        action="recovery_approved",
        actor=actor,
        decided_at=effective_now,
        details={"approval_count": len(approvals)},
    )
    _emit_transfer_event(session, transfer, "recovery_approved", actor)
    session.flush()
    return transfer


def execute_administrative_recovery(
    session: Session,
    *,
    transfer: OwnershipTransfer,
    provider: ResourceOwnershipProvider,
    actor: OwnershipSubjectRef,
    now: datetime | None = None,
) -> OwnershipTransfer:
    effective_now = _utc(now)
    _require_active(session, transfer, effective_now)
    _require_recovery_authentication(transfer, actor)
    if transfer.status != OwnershipTransferStatus.RECOVERY_SCHEDULED.value:
        raise OwnershipTransferError("Ownership recovery has not reached its approval quorum")
    if transfer.execute_after and _utc(transfer.execute_after) > effective_now:
        raise OwnershipTransferError("Ownership recovery assurance delay has not elapsed")
    _authorize(provider, session, transfer, actor, "execute_recovery")
    provider.apply_owner(
        session,
        tenant_id=transfer.tenant_id,
        resource_id=transfer.resource_id,
        expected_owner=_owner_ref(transfer, target=False),
        target_owner=_owner_ref(transfer, target=True),
        actor=actor,
        reason=transfer.reason,
    )
    transfer.status = OwnershipTransferStatus.COMPLETED.value
    transfer.completed_at = effective_now
    _touch(transfer)
    _record_decision(
        transfer,
        action="recovery_executed",
        actor=actor,
        decided_at=effective_now,
    )
    _emit_transfer_event(session, transfer, "recovery_completed", actor)
    session.flush()
    return transfer


def expire_due_ownership_transfers(
    session: Session,
    *,
    now: datetime | None = None,
    limit: int = 250,
) -> int:
    effective_now = _utc(now)
    rows = (
        session.query(OwnershipTransfer)
        .filter(
            OwnershipTransfer.status.notin_(
                sorted(TERMINAL_OWNERSHIP_TRANSFER_STATUSES)
            ),
            OwnershipTransfer.expires_at <= effective_now,
        )
        .order_by(OwnershipTransfer.expires_at.asc(), OwnershipTransfer.id.asc())
        .limit(limit)
        .all()
    )
    for transfer in rows:
        transfer.status = OwnershipTransferStatus.EXPIRED.value
        transfer.expired_at = effective_now
        _touch(transfer)
        _record_decision(
            transfer,
            action="expired",
            actor=None,
            decided_at=effective_now,
        )
        _emit_transfer_event(session, transfer, "expired", None)
    session.flush()
    return len(rows)


def _start_transfer(
    session: Session,
    *,
    tenant_id: str,
    resource: OwnershipResourceRef,
    provider: ResourceOwnershipProvider,
    actor: OwnershipSubjectRef,
    target_owner: OwnershipSubjectRef,
    kind: OwnershipTransferKind,
    initial_status: OwnershipTransferStatus,
    authorization_action: str,
    idempotency_key: str,
    reason: str | None,
    expiry_days: int | None,
    policy: OwnershipTransferPolicy,
    now: datetime | None,
    assurance_profile: str | None,
    required_approvals: int,
    execute_after: datetime | None,
    metadata: dict[str, Any],
) -> OwnershipTransfer:
    if not resource.module_id or resource.module_id != resource.module_id.strip():
        raise OwnershipTransferError("Ownership resource module id is invalid")
    clean_key = idempotency_key.strip()
    if not clean_key or len(clean_key) > 200:
        raise OwnershipTransferError("A bounded ownership idempotency key is required")
    effective_now = _utc(now)
    days = policy.default_expiry_days if expiry_days is None else expiry_days
    if not policy.min_expiry_days <= days <= policy.max_expiry_days:
        raise OwnershipTransferError(
            "Ownership transfer expiry is outside the effective policy"
        )
    current_owner = provider.current_owner(
        session,
        tenant_id=tenant_id,
        resource_id=resource.resource_id,
    )
    if current_owner is None:
        raise OwnershipTransferError("Owned resource was not found")
    if current_owner.type == target_owner.type and current_owner.id == target_owner.id:
        raise OwnershipTransferError("Target is already the resource owner")
    _authorize_direct(
        provider,
        session,
        tenant_id=tenant_id,
        resource_id=resource.resource_id,
        current_owner=current_owner,
        target_owner=target_owner,
        actor=actor,
        action=authorization_action,
    )
    request_hash = _request_hash(
        tenant_id=tenant_id,
        resource=resource,
        kind=kind,
        current_owner=current_owner,
        target_owner=target_owner,
        actor=actor,
        reason=reason,
        expiry_days=days,
        assurance_profile=assurance_profile,
        required_approvals=required_approvals,
        execute_after=execute_after,
        metadata=metadata,
    )
    existing = (
        session.query(OwnershipTransfer)
        .filter(
            OwnershipTransfer.tenant_id == tenant_id,
            OwnershipTransfer.resource_module == resource.module_id,
            OwnershipTransfer.idempotency_key == clean_key,
        )
        .one_or_none()
    )
    if existing is not None:
        if existing.canonical_request_hash != request_hash:
            raise OwnershipIdempotencyConflict(
                "Ownership idempotency key is already bound to another request"
            )
        return existing
    transfer = OwnershipTransfer(
        tenant_id=tenant_id,
        resource_module=resource.module_id,
        resource_type=resource.resource_type,
        resource_id=resource.resource_id,
        kind=kind.value,
        status=initial_status.value,
        current_owner_type=current_owner.type,
        current_owner_id=current_owner.id,
        target_owner_type=target_owner.type,
        target_owner_id=target_owner.id,
        initiated_by_type=actor.type,
        initiated_by_id=actor.id,
        reason=reason.strip() if reason else None,
        assurance_profile=assurance_profile,
        required_approvals=required_approvals,
        approvals=[],
        decisions=[],
        idempotency_key=clean_key,
        canonical_request_hash=request_hash,
        expires_at=effective_now + timedelta(days=days),
        execute_after=execute_after,
        metadata_=dict(metadata),
    )
    session.add(transfer)
    session.flush()
    _record_decision(
        transfer,
        action="started",
        actor=actor,
        decided_at=effective_now,
        details={
            "kind": kind.value,
            "initial_status": initial_status.value,
            "assurance_profile": assurance_profile,
            "required_approvals": required_approvals,
        },
    )
    _emit_transfer_event(session, transfer, "started", actor)
    return transfer


def _authorize(
    provider: ResourceOwnershipProvider,
    session: Session,
    transfer: OwnershipTransfer,
    actor: OwnershipSubjectRef,
    action: str,
) -> None:
    _authorize_direct(
        provider,
        session,
        tenant_id=transfer.tenant_id,
        resource_id=transfer.resource_id,
        current_owner=_owner_ref(transfer, target=False),
        target_owner=_owner_ref(transfer, target=True),
        actor=actor,
        action=action,
    )


def _authorize_direct(
    provider: ResourceOwnershipProvider,
    session: Session,
    *,
    tenant_id: str,
    resource_id: str,
    current_owner: OwnershipSubjectRef,
    target_owner: OwnershipSubjectRef,
    actor: OwnershipSubjectRef,
    action: str,
) -> None:
    decision = provider.authorize_ownership_action(
        session,
        tenant_id=tenant_id,
        resource_id=resource_id,
        action=action,
        actor=actor,
        current_owner=current_owner,
        target_owner=target_owner,
    )
    if not decision.allowed:
        raise OwnershipAuthorizationError(
            decision.reason or f"Ownership action is not allowed: {action}"
        )


def _require_active(
    session: Session,
    transfer: OwnershipTransfer,
    now: datetime,
) -> None:
    if transfer.status in TERMINAL_OWNERSHIP_TRANSFER_STATUSES:
        raise OwnershipTransferError("Ownership transfer is already final")
    if _utc(transfer.expires_at) <= now:
        transfer.status = OwnershipTransferStatus.EXPIRED.value
        transfer.expired_at = now
        _touch(transfer)
        _record_decision(
            transfer,
            action="expired",
            actor=None,
            decided_at=now,
        )
        _emit_transfer_event(session, transfer, "expired", None)
        raise OwnershipTransferExpired("Ownership transfer has expired")


def _require_recovery_authentication(
    transfer: OwnershipTransfer,
    actor: OwnershipSubjectRef,
) -> None:
    if (
        bool((transfer.metadata_ or {}).get("recent_authentication_required"))
        and not actor.recently_authenticated
    ):
        raise OwnershipAuthorizationError(
            "Administrative recovery requires recent authentication"
        )


def _owner_ref(
    transfer: OwnershipTransfer,
    *,
    target: bool,
) -> OwnershipSubjectRef:
    return OwnershipSubjectRef(
        type=transfer.target_owner_type if target else transfer.current_owner_type,
        id=transfer.target_owner_id if target else transfer.current_owner_id,
    )


def _touch(transfer: OwnershipTransfer) -> None:
    transfer.revision = int(transfer.revision or 0) + 1


def _record_decision(
    transfer: OwnershipTransfer,
    *,
    action: str,
    actor: OwnershipSubjectRef | None,
    decided_at: datetime,
    details: dict[str, Any] | None = None,
) -> None:
    decisions = list(transfer.decisions or [])
    decisions.append(
        {
            "sequence": len(decisions) + 1,
            "action": action,
            "actor_type": actor.type if actor else None,
            "actor_id": actor.id if actor else None,
            "decided_at": _utc(decided_at).isoformat(),
            "status": transfer.status,
            "details": dict(details or {}),
        }
    )
    transfer.decisions = decisions


def _utc(value: datetime | None) -> datetime:
    if value is None:
        return utcnow()
    if value.tzinfo is None:
        return value.replace(tzinfo=timezone.utc)
    return value.astimezone(timezone.utc)


def _request_hash(
    *,
    tenant_id: str,
    resource: OwnershipResourceRef,
    kind: OwnershipTransferKind,
    current_owner: OwnershipSubjectRef,
    target_owner: OwnershipSubjectRef,
    actor: OwnershipSubjectRef,
    reason: str | None,
    expiry_days: int,
    assurance_profile: str | None,
    required_approvals: int,
    execute_after: datetime | None,
    metadata: dict[str, Any],
) -> str:
    payload = {
        "tenant_id": tenant_id,
        "resource": {
            "module_id": resource.module_id,
            "type": resource.resource_type,
            "id": resource.resource_id,
        },
        "kind": kind.value,
        "current_owner": {"type": current_owner.type, "id": current_owner.id},
        "target_owner": {"type": target_owner.type, "id": target_owner.id},
        "actor": {"type": actor.type, "id": actor.id},
        "reason": reason.strip() if reason else None,
        "expiry_days": expiry_days,
        "assurance_profile": assurance_profile,
        "required_approvals": required_approvals,
        "execute_after": (
            _utc(execute_after).isoformat()
            if execute_after is not None
            else None
        ),
        "metadata": metadata,
    }
    encoded = json.dumps(
        payload,
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")
    return hashlib.sha256(encoded).hexdigest()


def _emit_transfer_event(
    session: Session,
    transfer: OwnershipTransfer,
    action: str,
    actor: OwnershipSubjectRef | None,
) -> None:
    from govoplan_core.core.events import (
        EventActorRef,
        EventObjectRef,
        EventTenantRef,
        PlatformEvent,
        emit_platform_event,
    )

    emit_platform_event(
        session,
        PlatformEvent(
            type=f"core.ownership_transfer.{action}.v1",
            module_id="core",
            tenant=EventTenantRef(id=transfer.tenant_id),
            actor=EventActorRef(type=actor.type, id=actor.id) if actor else None,
            resource=EventObjectRef(
                type="ownership_transfer",
                id=transfer.id,
            ),
            classification="confidential",
            payload={
                "schema_version": 1,
                "resource_module": transfer.resource_module,
                "resource_type": transfer.resource_type,
                "resource_id": transfer.resource_id,
                "kind": transfer.kind,
                "status": transfer.status,
                "current_owner_type": transfer.current_owner_type,
                "current_owner_id": transfer.current_owner_id,
                "target_owner_type": transfer.target_owner_type,
                "target_owner_id": transfer.target_owner_id,
                "assurance_profile": transfer.assurance_profile,
                "required_approvals": transfer.required_approvals,
                "approval_count": len(transfer.approvals or []),
                "encryption_keys_included": False,
            },
        ),
    )


__all__ = [
    "OwnershipActionDecision",
    "OwnershipAuthorizationError",
    "OwnershipIdempotencyConflict",
    "OwnershipProviderRegistration",
    "OwnershipResourceRef",
    "OwnershipSubjectRef",
    "OwnershipTransfer",
    "OwnershipTransferError",
    "OwnershipTransferExpired",
    "OwnershipTransferKind",
    "OwnershipTransferPolicy",
    "OwnershipTransferStatus",
    "ResourceOwnershipProvider",
    "accept_ownership_transfer",
    "approve_administrative_recovery",
    "approve_ownership_request",
    "cancel_ownership_transfer",
    "decline_ownership_transfer",
    "execute_administrative_recovery",
    "expire_due_ownership_transfers",
    "request_ownership",
    "start_administrative_recovery",
    "start_owner_initiated_transfer",
]
