"""Opaque, short-lived, single-read grants for the runtime fetch provider."""

from __future__ import annotations

import secrets
import threading
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Callable, Dict, Optional

from arr_ingestion.contracts import OPAQUE_ID_RE, IngestionError
from arr_storage.store import ManagedObjectStore


MAX_READ_GRANT_TTL_SECONDS = 300


@dataclass(frozen=True)
class OpaqueReadGrant:
    handle: str
    job_id: str
    expires_at: datetime
    max_reads: int = 1


@dataclass
class _GrantRecord:
    public: OpaqueReadGrant
    object_key: str
    consumed: bool = False


class InMemoryReadGrantBroker:
    """Reference broker for tests; production must use a shared atomic grant ledger."""

    def __init__(
        self,
        store: ManagedObjectStore,
        *,
        clock: Optional[Callable[[], datetime]] = None,
    ) -> None:
        self._store = store
        self._clock = clock or (lambda: datetime.now(timezone.utc))
        self._lock = threading.Lock()
        self._records: Dict[str, _GrantRecord] = {}

    def issue(
        self,
        *,
        job_id: str,
        object_key: str,
        original_filename: str,
        ttl_seconds: int = MAX_READ_GRANT_TTL_SECONDS,
    ) -> OpaqueReadGrant:
        if not isinstance(job_id, str) or not OPAQUE_ID_RE.fullmatch(job_id):
            raise IngestionError("READ_GRANT_INVALID", "read grant job is invalid")
        if (
            not isinstance(ttl_seconds, int)
            or isinstance(ttl_seconds, bool)
            or not 1 <= ttl_seconds <= MAX_READ_GRANT_TTL_SECONDS
        ):
            raise IngestionError("READ_GRANT_INVALID", "read grant lifetime is invalid")
        descriptor = self._store.inspect_committed(object_key, original_filename)
        if descriptor.job_id != job_id or descriptor.role != "source_xml":
            raise IngestionError("READ_GRANT_INVALID", "read grant target is invalid")
        now = self._aware_now()
        public = OpaqueReadGrant(
            handle="arr_file_" + secrets.token_urlsafe(24),
            job_id=job_id,
            expires_at=now + timedelta(seconds=ttl_seconds),
        )
        with self._lock:
            self._records[public.handle] = _GrantRecord(public, object_key)
        return public

    def materialize(
        self,
        *,
        handle: str,
        job_id: str,
        destination: Path,
        max_bytes: int,
    ) -> None:
        now = self._aware_now()
        with self._lock:
            record = self._records.get(handle)
            if (
                record is None
                or record.consumed
                or record.public.job_id != job_id
                or now >= record.public.expires_at
            ):
                raise IngestionError("READ_GRANT_INVALID", "read grant is unavailable")
            record.consumed = True
            object_key = record.object_key
        self._store.materialize(object_key, destination, max_bytes)

    def purge_expired(self) -> int:
        now = self._aware_now()
        with self._lock:
            expired = [
                handle
                for handle, record in self._records.items()
                if record.consumed or now >= record.public.expires_at
            ]
            for handle in expired:
                del self._records[handle]
        return len(expired)

    def _aware_now(self) -> datetime:
        value = self._clock()
        if value.tzinfo is None or value.utcoffset() is None:
            raise ValueError("grant clock must return a timezone-aware datetime")
        return value.astimezone(timezone.utc)
