"""RAM-only coordinate batches: producers publish, input consumers resolve IDs.

The owner supplies the complete layout/scene/rule signature and validated
coordinate objects. This store neither calculates coordinates nor performs I/O.
Record objects retain their identity; producers must not mutate published values.
"""
from __future__ import annotations

from collections.abc import Hashable, Mapping
from dataclasses import dataclass
from threading import RLock
from types import MappingProxyType
from typing import Any

from .errors import InputInterrupted


class CoordinateStoreError(InputInterrupted):
    """No current coordinate is available; observe/publish before any input."""


@dataclass(frozen=True)
class CoordinateRef:
    button_id: str
    generation: int


@dataclass(frozen=True)
class CoordinateSnapshot:
    generation: int
    signature: Hashable | None
    records: Mapping[str, Any]


class CoordinateStore:
    def __init__(self):
        self._lock = RLock()
        self._snapshot = CoordinateSnapshot(0, None, MappingProxyType({}))

    @property
    def snapshot(self):
        with self._lock:
            return self._snapshot

    def _replace(self, records, signature):
        self._snapshot = CoordinateSnapshot(self._snapshot.generation + 1,
                                            signature, MappingProxyType(records))
        return self._snapshot

    def publish(self, records, *, signature):
        """Replace the entire batch atomically; any failed update clears it."""
        with self._lock:
            try:
                if signature is None:
                    raise CoordinateStoreError("좌표 묶음의 화면·장면·규칙 식별값이 필요합니다.")
                hash(signature)  # A mutable signature cannot identify a snapshot.
                if not isinstance(records, Mapping):
                    raise CoordinateStoreError("좌표 묶음은 버튼 ID별 매핑이어야 합니다.")
                copied = dict(records)
                if any(not isinstance(key, str) or not key.strip() or value is None
                       for key, value in copied.items()):
                    raise CoordinateStoreError("좌표 묶음의 버튼 ID 또는 좌표가 올바르지 않습니다.")
                return self._replace(copied, signature)
            except BaseException:
                self._replace({}, None)
                raise

    def invalidate(self):
        """Discard all current coordinates and invalidate outstanding references."""
        with self._lock:
            return self._replace({}, None)

    clear = invalidate

    def ref(self, button_id, *, snapshot=None):
        with self._lock:
            current = self._snapshot
            if snapshot is not None and snapshot is not current:
                raise CoordinateStoreError("이전 좌표 묶음은 사용할 수 없습니다.")
            if not isinstance(button_id, str) or button_id not in current.records:
                raise CoordinateStoreError("현재 좌표 묶음에 해당 버튼이 없습니다.")
            return CoordinateRef(button_id, current.generation)

    def resolve(self, reference, *, current_signature=None):
        """Read a current reference, optionally checking the caller's live context.

        A stale reference does not damage a newer batch. A supplied live-context
        mismatch invalidates that batch so omitting the check cannot reuse it.
        """
        with self._lock:
            current = self._snapshot
            if current_signature is not None and current_signature != current.signature:
                self._replace({}, None)
                raise CoordinateStoreError("화면·장면·규칙이 변경되어 좌표 묶음을 폐기했습니다.")
            if (not isinstance(reference, CoordinateRef)
                    or reference.generation != current.generation
                    or reference.button_id not in current.records):
                raise CoordinateStoreError("이전 또는 없는 좌표 참조는 사용할 수 없습니다.")
            return current.records[reference.button_id]
