# app/src/storage/instrument_store.py

from __future__ import annotations

from typing import Protocol, runtime_checkable

from src.market_data.acquisition.models.instrument import Instrument
from src.storage.exceptions import InstrumentStoreError


# Контракт runtime-хранилища канонического справочника инструментов.
@runtime_checkable
class InstrumentStoreProtocol(Protocol):
    def get(
        self,
        source_name: str,
    ) -> tuple[Instrument, ...] | None:
        """
        Вернуть сохранённый набор инструментов для источника.

        None означает cache miss: данные для источника ещё не сохранялись.
        Пустой tuple означает успешное сохранение пустого справочника.
        """

    def set(
        self,
        source_name: str,
        instruments: tuple[Instrument, ...],
    ) -> None:
        """Сохранить полный immutable-набор инструментов источника."""

    def clear(
        self,
        source_name: str | None = None,
    ) -> None:
        """
        Очистить данные одного источника или всё хранилище.

        source_name=None очищает все сохранённые источники.
        """


# In-memory реализация runtime-хранилища справочника инструментов.
class InMemoryInstrumentStore:
    def __init__(self) -> None:
        self._items: dict[str, tuple[Instrument, ...]] = {}

    def get(
        self,
        source_name: str,
    ) -> tuple[Instrument, ...] | None:
        normalized_source_name = self._normalize_source_name(
            source_name
        )

        return self._items.get(normalized_source_name)

    def set(
        self,
        source_name: str,
        instruments: tuple[Instrument, ...],
    ) -> None:
        normalized_source_name = self._normalize_source_name(
            source_name
        )

        if not isinstance(instruments, tuple):
            raise InstrumentStoreError(
                "Справочник инструментов должен быть передан как tuple."
            )

        if not all(
            isinstance(instrument, Instrument)
            for instrument in instruments
        ):
            raise InstrumentStoreError(
                "Справочник содержит объект, не являющийся Instrument."
            )

        self._items[normalized_source_name] = instruments

    def clear(
        self,
        source_name: str | None = None,
    ) -> None:
        if source_name is None:
            self._items.clear()
            return

        normalized_source_name = self._normalize_source_name(
            source_name
        )

        self._items.pop(
            normalized_source_name,
            None,
        )

    def _normalize_source_name(
        self,
        source_name: str,
    ) -> str:
        normalized_source_name = str(source_name or "").strip()

        if not normalized_source_name:
            raise InstrumentStoreError(
                "Имя источника Instrument Store не должно быть пустым."
            )

        return normalized_source_name