from dataclasses import dataclass, replace
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any
from uuid import UUID

from .security import canonical_payload_hash, payload_matches


class ApprovalStatus(StrEnum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"
    EXPIRED = "expired"


@dataclass(frozen=True, slots=True)
class Approval:
    id: UUID
    workspace_id: UUID
    action_id: UUID
    permission: str
    payload_hash: str
    expires_at: datetime
    status: ApprovalStatus = ApprovalStatus.PENDING
    decided_at: datetime | None = None
    decided_by_user_id: UUID | None = None


def make_approval(*, id: UUID, workspace_id: UUID, action_id: UUID, permission: str,
                  payload: dict[str, Any], expires_at: datetime) -> Approval:
    if expires_at.tzinfo is None:
        raise ValueError("Approval expiry must be timezone-aware")
    return Approval(id=id, workspace_id=workspace_id, action_id=action_id,
                    permission=permission, payload_hash=canonical_payload_hash(payload),
                    expires_at=expires_at)


def decide_approval(approval: Approval, *, payload: dict[str, Any], workspace_id: UUID,
                    approver_user_id: UUID, approver_permissions: set[str], approve: bool,
                    now: datetime | None = None) -> Approval:
    current_time = now or datetime.now(UTC)
    if approval.status != ApprovalStatus.PENDING:
        raise ValueError("Approval has already been decided")
    if approval.workspace_id != workspace_id:
        raise PermissionError("Approval belongs to another workspace")
    if "approvals.approve" not in approver_permissions:
        raise PermissionError("Approver lacks approvals.approve")
    if current_time >= approval.expires_at:
        return replace(approval, status=ApprovalStatus.EXPIRED, decided_at=current_time)
    if not payload_matches(payload, approval.payload_hash):
        raise ValueError("Approved payload does not match the requested action")
    status = ApprovalStatus.APPROVED if approve else ApprovalStatus.REJECTED
    return replace(approval, status=status, decided_at=current_time,
                   decided_by_user_id=approver_user_id)
