"""Local reference only: single-process approval logic, no network publisher.

Production requires authenticated reviewer input, durable transactions,
secret management, reconciliation and integration tests.
"""
from dataclasses import dataclass
import hashlib
import json
from typing import Callable

@dataclass(frozen=True)
class Draft:
    draft_id: str
    title: str
    body: str
    destination: str

    def fingerprint(self) -> str:
        payload = json.dumps(self.__dict__, sort_keys=True, ensure_ascii=False)
        return hashlib.sha256(payload.encode('utf-8')).hexdigest()

@dataclass
class Decision:
    fingerprint: str
    approved: bool
    expires_at: int
    used: bool = False

class ApprovalGate:
    def __init__(self, authorized_reviewers: set[str]):
        self.reviewers = set(authorized_reviewers)
        self.decisions: dict[str, Decision] = {}

    def review(self, draft: Draft, reviewer: str, approved: bool,
               now: int, lifetime: int) -> None:
        # reviewer must come from an authenticated server-side identity.
        if reviewer not in self.reviewers:
            raise PermissionError('Unauthorized reviewer')
        if lifetime <= 0:
            raise ValueError('Approval lifetime must be positive')
        prior = self.decisions.get(draft.draft_id)
        if prior and prior.used:
            raise PermissionError('Already consumed; reconcile before retrying')
        self.decisions[draft.draft_id] = Decision(
            draft.fingerprint(), approved, now + lifetime
        )

    def publish(self, draft: Draft, now: int,
                publisher: Callable[[Draft], str]) -> str:
        decision = self.decisions.get(draft.draft_id)
        if not decision or not decision.approved:
            raise PermissionError('No approval')
        if decision.used or now >= decision.expires_at:
            raise PermissionError('Approval consumed or expired')
        if decision.fingerprint != draft.fingerprint():
            raise PermissionError('Draft or destination changed')
        # Fail closed after an uncertain write. No automatic retry here.
        decision.used = True
        return publisher(draft)
