from __future__ import annotations

import asyncio
from logging import getLogger
from threading import Lock
from typing import TYPE_CHECKING

from .subclients.catalog import CatalogClient

if TYPE_CHECKING:
    from .client import DSGCoreClient

logger = getLogger(__name__)


class ItemTypePathCache:
    _instance: ItemTypePathCache | None = None
    _instance_lock = Lock()

    @classmethod
    def get_instance(
        cls,
        catalog_client: CatalogClient,
        preload_keys: tuple[str, ...] | None = None,
    ) -> ItemTypePathCache:
        if cls._instance is not None:
            return cls._instance

        with cls._instance_lock:
            if cls._instance is None:
                cls._instance = cls(
                    catalog_client=catalog_client,
                    preload_keys=preload_keys,
                )
        return cls._instance

    def __init__(
        self,
        catalog_client: CatalogClient,
        preload_keys: tuple[str, ...] | None = None,
    ):
        self._catalog_client = catalog_client
        self._preload_keys = preload_keys or tuple()
        self._path_to_item_type_id: dict[str, str] = {}
        self._alias_to_path: dict[str, str] = {}
        self._inflight: dict[str, asyncio.Task[str]] = {}
        self._cache_lock = asyncio.Lock()
        self._cache_hits = 0
        self._cache_misses = 0
        self._inflight_waits = 0
        self._initialized = False
        self._init_lock = asyncio.Lock()

    @staticmethod
    def _normalize_key(key: str) -> str:
        normalized = key.strip().strip("/").lower()
        if normalized.startswith("jnjct/"):
            return f"jnj/{normalized.split('/', 1)[1]}"
        return normalized

    async def ensure_initialized(self) -> None:
        if self._initialized:
            return
        async with self._init_lock:
            if self._initialized:
                return
            await self._load_known_keys()
            self._initialized = True
            logger.info(
                "ItemTypePathCache initialized with %s mappings",
                len(self._path_to_item_type_id),
            )

    async def _store_mapping(
        self, requested_key: str, path: str, item_type_id: str
    ) -> None:
        canonical_path = self._normalize_key(path)
        normalized_requested = self._normalize_key(requested_key)

        async with self._cache_lock:
            self._path_to_item_type_id[canonical_path] = item_type_id
            self._alias_to_path[canonical_path] = canonical_path
            self._alias_to_path[normalized_requested] = canonical_path

    async def _resolve_via_catalog(self, normalized_key: str) -> str:
        response = await self._catalog_client.map_item_type(normalized_key)
        value = str(response.item_type_id)
        await self._store_mapping(
            requested_key=normalized_key,
            path=response.path,
            item_type_id=value,
        )
        return value

    async def _load_known_keys(self) -> None:
        for key in self._preload_keys:
            normalized = self._normalize_key(key)
            try:
                response = await self._catalog_client.map_item_type(normalized)
            except Exception:
                logger.debug(
                    "Failed to map item type key '%s' from catalog endpoint",
                    normalized,
                    exc_info=True,
                )
                continue
            await self._store_mapping(
                requested_key=normalized,
                path=response.path,
                item_type_id=str(response.item_type_id),
            )

    async def get_item_type_id(self, key: str) -> str:
        await self.ensure_initialized()
        normalized = self._normalize_key(key)

        async with self._cache_lock:
            canonical_path = self._alias_to_path.get(normalized, normalized)
            cached = self._path_to_item_type_id.get(canonical_path)
            if cached is not None:
                self._cache_hits += 1
                return cached
            in_flight = self._inflight.get(normalized)
            if in_flight is None:
                self._cache_misses += 1
                in_flight = asyncio.create_task(self._resolve_via_catalog(normalized))
                self._inflight[normalized] = in_flight
            else:
                self._inflight_waits += 1

        try:
            return await in_flight
        finally:
            async with self._cache_lock:
                active = self._inflight.get(normalized)
                if active is in_flight:
                    self._inflight.pop(normalized, None)

    async def get_all_mappings(self) -> dict[str, str]:
        await self.ensure_initialized()
        async with self._cache_lock:
            return dict(self._path_to_item_type_id)

    async def get_cache_stats(self) -> dict[str, int | bool]:
        async with self._cache_lock:
            return {
                "initialized": self._initialized,
                "entries": len(self._path_to_item_type_id),
                "aliases": len(self._alias_to_path),
                "inflight": len(self._inflight),
                "hits": self._cache_hits,
                "misses": self._cache_misses,
                "inflight_waits": self._inflight_waits,
            }

    async def reset_cache_stats(self) -> None:
        async with self._cache_lock:
            self._cache_hits = 0
            self._cache_misses = 0
            self._inflight_waits = 0


async def get_item_type_from_path(item_type_key: str, client: "DSGCoreClient") -> str:
    return await client.item_type_cache.get_item_type_id(item_type_key)
