"""Entity resolution - Asset class for mapping identifiers to YUKKA entity IDs."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path

import polars as pl
from httpx import Client

from .utils import METADATA_API_URL

_MASTER_FILE = Path(__file__).parent / "data" / "master_constituents.parquet"


class EntityNotFound(ValueError):
    """Raised when an entity cannot be resolved to a YUKKA ID."""

    pass


@dataclass(frozen=True)
class Asset:
    """Represents a YUKKA entity that can be created from different identifier types.

    Assets are immutable and can be created from:
    - YUKKA IDs (e.g., "company:bmw")
    - ISINs (e.g., "DE0005190003") - requires EntityResolver
    - RIC codes (e.g., "AAPL.OQ") - uses master constituents file

    The Asset stores the resolved yukka_id which is used when making API calls.

    Example:
        >>> bmw = Asset.from_yukka_id("company:bmw")
        >>> philips = Asset.from_isin("NL0000009538", resolver=resolver)
        >>> apple = Asset.from_ric("AAPL.OQ")
    """

    yukka_id: str
    isin: str | None = None
    ric_code: str | None = None

    @classmethod
    def from_yukka_id(cls, yukka_id: str | list[str]) -> Asset | list[Asset]:
        """Create Asset(s) from YUKKA ID(s).

        Args:
            yukka_id: Single YUKKA ID or list of IDs (e.g., "company:bmw")

        Returns:
            Single Asset or list of Assets
        """
        if isinstance(yukka_id, list):
            return [cls(yukka_id=yid) for yid in yukka_id]
        return cls(yukka_id=yukka_id)

    @classmethod
    def from_isin(cls, isin: str | list[str], resolver: EntityResolver) -> Asset | list[Asset]:
        """Create Asset(s) from ISIN(s).

        Args:
            isin: Single ISIN or list of ISINs (e.g., "DE0005190003")
            resolver: EntityResolver instance for looking up yukka_ids

        Returns:
            Single Asset or list of Assets

        Raises:
            EntityNotFound: If any ISIN cannot be resolved
        """
        if isinstance(isin, list):
            resolved = resolver.resolve_isins(isin)
            return [cls(yukka_id=resolved[i], isin=i) for i in isin]
        yukka_id = resolver.resolve_isin(isin)
        return cls(yukka_id=yukka_id, isin=isin)

    @classmethod
    def from_ric(cls, ric_code: str | list[str]) -> Asset | list[Asset]:
        """Create Asset(s) from RIC code(s) using the master constituents file.

        Args:
            ric_code: Single RIC code or list of RIC codes (e.g., "AAPL.OQ")

        Returns:
            Single Asset or list of Assets (only for successfully resolved RICs)

        Raises:
            EntityNotFound: If a single RIC code cannot be resolved
        """
        master_df = pl.read_parquet(_MASTER_FILE)
        ric_to_yukka = dict(
            zip(
                master_df["constituent_ric"].to_list(),
                master_df["yukka_id"].to_list(),
                strict=False,
            )
        )

        if isinstance(ric_code, list):
            resolved = []
            not_found = []
            for ric in ric_code:
                if ric in ric_to_yukka and ric_to_yukka[ric] is not None:
                    resolved.append(cls(yukka_id=ric_to_yukka[ric], ric_code=ric))
                else:
                    not_found.append(ric)
            if not_found:
                print(f"Could not resolve {len(not_found)} RIC codes: {not_found}")
            return resolved

        if ric_code not in ric_to_yukka or ric_to_yukka[ric_code] is None:
            raise EntityNotFound(f"Could not resolve RIC code: {ric_code}")
        return cls(yukka_id=ric_to_yukka[ric_code], ric_code=ric_code)

    # TODO: RIC code support via API commented out due to poor coverage in ticker_to_entity API
    # @classmethod
    # def from_ric_code(
    #     cls, ric_code: str | list[str], resolver: EntityResolver
    # ) -> Asset | list[Asset]:
    #     """Create Asset(s) from RIC code(s).
    #
    #     Args:
    #         ric_code: Single RIC code or list of RIC codes (e.g., "BMWG.DE")
    #         resolver: EntityResolver instance for looking up yukka_ids
    #
    #     Returns:
    #         Single Asset or list of Assets
    #
    #     Raises:
    #         EntityNotFound: If RIC code cannot be resolved
    #     """
    #     if isinstance(ric_code, list):
    #         resolved = resolver.resolve_ric_codes(ric_code)
    #         return [cls(yukka_id=resolved[r], ric_code=r) for r in ric_code]
    #     yukka_id = resolver.resolve_ric_code(ric_code)
    #     return cls(yukka_id=yukka_id, ric_code=ric_code)


@dataclass
class EntityResolver:
    """Resolves ISINs and RIC codes to YUKKA entity IDs via the Metadata API.

    Args:
        client: HTTP client for API requests
        token_getter: Callable that returns a valid JWT token

    Example:
        >>> resolver = EntityResolver(client, token.get)
        >>> yukka_id = resolver.resolve_isin("DE0005190003")
    """

    _client: Client
    _token_getter: Callable[[], str]

    @property
    def _auth_headers(self) -> dict[str, str]:
        """Get authorization headers with current JWT token."""
        return {"Authorization": f"Bearer {self._token_getter()}"}

    def resolve_isin(self, isin: str) -> str:
        """Resolve a single ISIN to a YUKKA entity ID.

        Args:
            isin: ISIN to resolve (e.g., "DE0005190003")

        Returns:
            YUKKA entity ID (e.g., "company:bmw")

        Raises:
            EntityNotFound: If ISIN cannot be resolved
        """
        result = self.resolve_isins([isin])
        if isin not in result:
            raise EntityNotFound(f"Could not resolve ISIN: {isin}")
        return result[isin]

    def resolve_isins(self, isins: list[str]) -> dict[str, str]:
        """Resolve multiple ISINs to YUKKA entity IDs.

        Args:
            isins: List of ISINs to resolve

        Returns:
            Dict mapping ISIN -> yukka_id for successfully resolved entities

        Raises:
            EntityNotFound: If any ISIN cannot be resolved
        """
        if not isins:
            return {}

        resp = self._client.post(
            str(METADATA_API_URL / "isin_to_entity"),
            json=isins,
            headers=self._auth_headers,
        )
        resp.raise_for_status()
        data = resp.json()

        resolved = {}
        not_found = []
        for isin in isins:
            if isin in data and data[isin] and "compound_key" in data[isin]:
                resolved[isin] = data[isin]["compound_key"]
            else:
                not_found.append(isin)

        if not_found:
            raise EntityNotFound(f"Could not resolve ISINs: {not_found}")

        return resolved

    # TODO: RIC code support commented out due to poor coverage in ticker_to_entity API
    # def resolve_ric_code(self, ric_code: str) -> str:
    #     """Resolve a single RIC code to a YUKKA entity ID."""
    #     result = self.resolve_ric_codes([ric_code])
    #     if ric_code not in result:
    #         raise EntityNotFound(f"Could not resolve RIC code: {ric_code}")
    #     return result[ric_code]
    #
    # def resolve_ric_codes(self, ric_codes: list[str]) -> dict[str, str]:
    #     """Resolve multiple RIC codes to YUKKA entity IDs."""
    #     resp = self._client.post(
    #         str(METADATA_API_URL / "v2" / "ticker_to_entity"),
    #         json=[ric.split('.')[0] for ric in ric_codes],  # Strip exchange suffix
    #         headers=self._auth_headers,
    #     )
    #     resp.raise_for_status()
    #     data = resp.json()
    #
    #     resolved = {}
    #     not_found = []
    #     for ric in ric_codes:
    #         ticker = ric.split('.')[0]
    #         if ticker in data and data[ticker] and "compound_key" in data[ticker]:
    #             resolved[ric] = data[ticker]["compound_key"]
    #         else:
    #             not_found.append(ric)
    #
    #     if not_found:
    #         raise EntityNotFound(f"Could not resolve RIC codes: {not_found}")
    #
    #     return resolved
