"""Thread-safe reference ledger for job/run and callback correlation."""

from __future__ import annotations

import copy
import threading
from dataclasses import dataclass
from typing import Dict, Optional, Protocol, Tuple

from arr_processing.contracts import ProcessingRequest, ProcessingResult
from arr_processing.errors import ProcessingError


@dataclass(frozen=True)
class ProcessingRecord:
    request: ProcessingRequest
    request_sha256: str
    idempotency_key: str
    remote_run_id: Optional[str]
    remote_status: str
    result: Optional[ProcessingResult] = None
    failure_code: Optional[str] = None


class ProcessingLedger(Protocol):
    def reserve(
        self,
        request: ProcessingRequest,
        request_sha256: str,
        idempotency_key: str,
    ) -> ProcessingRecord:
        ...

    def get(self, job_id: str, attempt_no: int) -> Optional[ProcessingRecord]:
        ...

    def bind_run(
        self,
        job_id: str,
        attempt_no: int,
        remote_run_id: str,
        remote_status: str,
    ) -> ProcessingRecord:
        ...

    def update_status(
        self,
        job_id: str,
        attempt_no: int,
        remote_status: str,
    ) -> ProcessingRecord:
        ...

    def fail_start(
        self,
        job_id: str,
        attempt_no: int,
        failure_code: str,
    ) -> ProcessingRecord:
        ...

    def attach_result(self, result: ProcessingResult) -> Tuple[ProcessingRecord, bool]:
        ...

    def accept_callback(self, delivery_id: str, signed_sha256: str) -> bool:
        ...


class InMemoryProcessingLedger:
    """Reference implementation; production should map this protocol to PostgreSQL."""

    def __init__(self) -> None:
        self._lock = threading.RLock()
        self._records: Dict[Tuple[str, int], ProcessingRecord] = {}
        self._runs: Dict[str, Tuple[str, int]] = {}
        self._callbacks: Dict[str, str] = {}

    def reserve(
        self,
        request: ProcessingRequest,
        request_sha256: str,
        idempotency_key: str,
    ) -> ProcessingRecord:
        key = (request.job_id, request.attempt_no)
        with self._lock:
            existing = self._records.get(key)
            if existing is not None:
                if (
                    existing.request != request
                    or existing.request_sha256 != request_sha256
                    or existing.idempotency_key != idempotency_key
                ):
                    raise ProcessingError(
                        "PROCESSING_JOB_CONFLICT", "processing attempt identity conflicts"
                    )
                return copy.deepcopy(existing)
            record = ProcessingRecord(
                request=request,
                request_sha256=request_sha256,
                idempotency_key=idempotency_key,
                remote_run_id=None,
                remote_status="reserved",
            )
            self._records[key] = record
            return copy.deepcopy(record)

    def get(self, job_id: str, attempt_no: int) -> Optional[ProcessingRecord]:
        with self._lock:
            record = self._records.get((job_id, attempt_no))
            return copy.deepcopy(record) if record is not None else None

    def bind_run(
        self,
        job_id: str,
        attempt_no: int,
        remote_run_id: str,
        remote_status: str,
    ) -> ProcessingRecord:
        key = (job_id, attempt_no)
        with self._lock:
            record = self._required(key)
            run_owner = self._runs.get(remote_run_id)
            if run_owner is not None and run_owner != key:
                raise ProcessingError(
                    "PROCESSING_RUN_CONFLICT", "remote run is already correlated"
                )
            if record.remote_run_id is not None and record.remote_run_id != remote_run_id:
                raise ProcessingError(
                    "PROCESSING_RUN_CONFLICT", "processing attempt has a different remote run"
                )
            updated = ProcessingRecord(
                request=record.request,
                request_sha256=record.request_sha256,
                idempotency_key=record.idempotency_key,
                remote_run_id=remote_run_id,
                remote_status=remote_status,
                result=record.result,
                failure_code=record.failure_code,
            )
            self._records[key] = updated
            self._runs[remote_run_id] = key
            return copy.deepcopy(updated)

    def update_status(
        self,
        job_id: str,
        attempt_no: int,
        remote_status: str,
    ) -> ProcessingRecord:
        key = (job_id, attempt_no)
        with self._lock:
            record = self._required(key)
            updated = ProcessingRecord(
                request=record.request,
                request_sha256=record.request_sha256,
                idempotency_key=record.idempotency_key,
                remote_run_id=record.remote_run_id,
                remote_status=remote_status,
                result=record.result,
                failure_code=record.failure_code,
            )
            self._records[key] = updated
            return copy.deepcopy(updated)

    def fail_start(
        self,
        job_id: str,
        attempt_no: int,
        failure_code: str,
    ) -> ProcessingRecord:
        key = (job_id, attempt_no)
        with self._lock:
            record = self._required(key)
            if record.remote_run_id is not None:
                raise ProcessingError(
                    "PROCESSING_RUN_CONFLICT",
                    "processing attempt already has a remote run",
                )
            if record.remote_status == "failed":
                return copy.deepcopy(record)
            updated = ProcessingRecord(
                request=record.request,
                request_sha256=record.request_sha256,
                idempotency_key=record.idempotency_key,
                remote_run_id=None,
                remote_status="failed",
                result=record.result,
                failure_code=failure_code,
            )
            self._records[key] = updated
            return copy.deepcopy(updated)

    def attach_result(self, result: ProcessingResult) -> Tuple[ProcessingRecord, bool]:
        key = (result.job_id, result.attempt_no)
        with self._lock:
            record = self._required(key)
            if record.remote_run_id != result.remote_run_id:
                raise ProcessingError(
                    "PROCESSING_RESULT_MISMATCH", "result remote run does not match its attempt"
                )
            if record.result is not None:
                if record.result != result:
                    raise ProcessingError(
                        "PROCESSING_RESULT_CONFLICT", "processing attempt has a different result"
                    )
                return copy.deepcopy(record), True
            updated = ProcessingRecord(
                request=record.request,
                request_sha256=record.request_sha256,
                idempotency_key=record.idempotency_key,
                remote_run_id=record.remote_run_id,
                remote_status="delivered",
                result=result,
                failure_code=None,
            )
            self._records[key] = updated
            return copy.deepcopy(updated), False

    def accept_callback(self, delivery_id: str, signed_sha256: str) -> bool:
        with self._lock:
            existing = self._callbacks.get(delivery_id)
            if existing is None:
                self._callbacks[delivery_id] = signed_sha256
                return False
            if existing != signed_sha256:
                raise ProcessingError(
                    "PROCESSING_CALLBACK_CONFLICT", "delivery identifier conflicts"
                )
            return True

    def _required(self, key: Tuple[str, int]) -> ProcessingRecord:
        try:
            return self._records[key]
        except KeyError:
            raise ProcessingError(
                "PROCESSING_JOB_NOT_FOUND", "processing attempt is not registered"
            ) from None
