"""Execution eligibility checks for approvals and revocations."""

from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import TYPE_CHECKING

from mailio_receipt.models import IntentKind

if TYPE_CHECKING:
    from mailio_receipt.models import MailioReceipt


class BlockingReason(Enum):
    """Reasons why execution may be blocked."""

    MISSING_APPROVAL = "missing_approval"
    EXPIRED = "expired"
    REVOKED = "revoked"
    BINDING_MISMATCH = "binding_mismatch"


@dataclass(frozen=True)
class ExecutionDecision:
    """Result of execution eligibility check."""

    allowed: bool
    reason: str
    approval_digest: str | None = None
    revocation_digest: str | None = None
    blocking_reason: BlockingReason | None = None


def check_execution(
    receipts: list[MailioReceipt],
    *,
    action_digest: str,
    decision_time: datetime | None = None,
) -> ExecutionDecision:
    """
    Check if execution is allowed for an action.

    Args:
        receipts: All receipts in the flow
        action_digest: Digest of the action event to check
        decision_time: Time of execution decision (default: now UTC)

    Returns:
        ExecutionDecision with allowed/blocked status and details

    Decision logic:
        1. Find the action receipt by digest
        2. Collect all approvals referencing the action
        3. For each approval:
           a. Check if expired (ttl)
           b. Check for revocations where revocation.ts <= decision_time
        4. If valid approval exists and no blocking revocation: ALLOWED
        5. Otherwise: BLOCKED with reason
    """
    if decision_time is None:
        decision_time = datetime.now(timezone.utc)

    # Build event digest -> receipt mapping
    digest_map = {r.event_digest: r for r in receipts}

    # Find the action receipt
    if action_digest not in digest_map:
        return ExecutionDecision(
            allowed=False,
            reason=f"Action event {action_digest[:16]}... not found",
            blocking_reason=BlockingReason.MISSING_APPROVAL,
        )

    action_receipt = digest_map[action_digest]

    # Collect approvals for this action
    approvals = _collect_approvals(receipts, action_digest)

    if not approvals:
        return ExecutionDecision(
            allowed=False,
            reason="No approval found for action",
            blocking_reason=BlockingReason.MISSING_APPROVAL,
        )

    # Collect revocations
    revocations = _collect_revocations(receipts)

    # Check each approval
    for approval in approvals:
        approval_digest = approval.digest

        # Check if approval is expired
        if approval.event.ttl:
            expiry = _compute_expiry(approval.event.ts, approval.event.ttl)
            if expiry and decision_time > expiry:
                continue  # Try next approval

        # Check for revocations
        blocking_revocation = _find_blocking_revocation(
            revocations, approval_digest, decision_time
        )

        if blocking_revocation:
            continue  # Try next approval

        # Check binding validation if needed
        binding_valid = _validate_bindings(action_receipt, approval)
        if not binding_valid:
            continue  # Try next approval

        # Found a valid approval!
        return ExecutionDecision(
            allowed=True,
            reason="Valid approval found",
            approval_digest=approval_digest,
        )

    # No valid approval found - determine why
    if approvals:
        # Check if all approvals are revoked
        for approval in approvals:
            blocking_revocation = _find_blocking_revocation(
                revocations, approval.digest, decision_time
            )
            if blocking_revocation:
                return ExecutionDecision(
                    allowed=False,
                    reason="Approval has been revoked",
                    approval_digest=approval.digest,
                    revocation_digest=blocking_revocation.digest,
                    blocking_reason=BlockingReason.REVOKED,
                )

        # Check if all approvals are expired
        for approval in approvals:
            if approval.event.ttl:
                expiry = _compute_expiry(approval.event.ts, approval.event.ttl)
                if expiry and decision_time > expiry:
                    return ExecutionDecision(
                        allowed=False,
                        reason="Approval has expired",
                        approval_digest=approval.digest,
                        blocking_reason=BlockingReason.EXPIRED,
                    )

        # Check binding issues
        for approval in approvals:
            if not _validate_bindings(action_receipt, approval):
                return ExecutionDecision(
                    allowed=False,
                    reason="Binding digest mismatch",
                    approval_digest=approval.digest,
                    blocking_reason=BlockingReason.BINDING_MISMATCH,
                )

    return ExecutionDecision(
        allowed=False,
        reason="No valid approval found",
        blocking_reason=BlockingReason.MISSING_APPROVAL,
    )


def _collect_approvals(
    receipts: list[MailioReceipt], action_digest: str
) -> list[MailioReceipt]:
    """Collect all approval receipts for an action."""
    approvals = []
    for receipt in receipts:
        if receipt.event.intent.kind == IntentKind.APPROVAL_GRANTED:
            # Check if approval references the action
            scope = receipt.event.intent.scope
            approved_digest = scope.get("approvedReceiptDigest")
            if approved_digest == action_digest:
                approvals.append(receipt)
            # Also check chain reference
            elif receipt.event.chain:
                if receipt.event.chain.prev_receipt_digest == action_digest:
                    approvals.append(receipt)
    return approvals


def _collect_revocations(receipts: list[MailioReceipt]) -> list[MailioReceipt]:
    """Collect all revocation receipts."""
    return [
        r for r in receipts if r.event.intent.kind == IntentKind.APPROVAL_REVOKED
    ]


def _find_blocking_revocation(
    revocations: list[MailioReceipt],
    approval_digest: str,
    decision_time: datetime,
) -> MailioReceipt | None:
    """
    Find a revocation that blocks an approval.

    A revocation blocks if:
    1. It references the approval (by digest or chain)
    2. revocation.ts <= decision_time
    """
    for revocation in revocations:
        # Check if revocation references the approval
        scope = revocation.event.intent.scope
        revoked_digest = scope.get("revokedApprovalDigest")

        references_approval = False
        if revoked_digest == approval_digest:
            references_approval = True
        elif revocation.event.chain:
            if revocation.event.chain.prev_receipt_digest == approval_digest:
                references_approval = True

        if not references_approval:
            continue

        # Check timestamp
        revocation_ts = datetime.fromisoformat(revocation.event.ts)
        if revocation_ts <= decision_time:
            return revocation

    return None


def _compute_expiry(ts: str, ttl: str) -> datetime | None:
    """Compute expiry time from timestamp and ISO8601 duration TTL."""
    try:
        timestamp = datetime.fromisoformat(ts)
        duration = _parse_iso8601_duration(ttl)
        if duration:
            return timestamp + duration
    except (ValueError, TypeError):
        pass
    return None


def _parse_iso8601_duration(duration: str) -> timedelta | None:
    """Parse ISO8601 duration to timedelta."""
    # Simple parser for common durations like PT1H, PT30M, P1D, etc.
    pattern = re.compile(
        r"^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$"
    )
    match = pattern.match(duration)
    if not match:
        return None

    years, months, days, hours, minutes, seconds = match.groups()

    total_days = 0
    total_seconds = 0

    if years:
        total_days += int(years) * 365
    if months:
        total_days += int(months) * 30
    if days:
        total_days += int(days)
    if hours:
        total_seconds += int(hours) * 3600
    if minutes:
        total_seconds += int(minutes) * 60
    if seconds:
        total_seconds += float(seconds)

    return timedelta(days=total_days, seconds=total_seconds)


def _validate_bindings(
    action_receipt: MailioReceipt, approval_receipt: MailioReceipt
) -> bool:
    """
    Validate that bindings match between action and approval.

    If the approval has bound artifacts, verify they match the action's artifacts.
    """
    action_binding = action_receipt.event.binding
    approval_binding = approval_receipt.event.binding

    # If approval has no bindings, validation passes
    if not approval_binding or not approval_binding.artifacts:
        return True

    # If action has no bindings but approval expects them, fail
    if not action_binding or not action_binding.artifacts:
        return False

    # Build digest set from action
    action_digests = {a.digest for a in action_binding.artifacts}

    # Verify all approval binding digests are in action
    for artifact in approval_binding.artifacts:
        if artifact.digest not in action_digests:
            return False

    return True
