"""Reference resolver: route cleaned references to data sources and return candidates.

Checks an optional citation cache before querying external APIs. For identified
references, dispatches to the registered source for that identifier type. For
free-text queries, uses the domain router to pick a source and falls back to
CrossRef if the primary source returns no results.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from alice_cli.cite.models import CitationRecord, IdentifierType
from alice_cli.cite.router import route_query

if TYPE_CHECKING:
    from alice_cli.cite.cache import CitationCache
    from alice_cli.cite.models import CleanedReference
    from alice_cli.cite.sources import DataSourceProtocol

logger = logging.getLogger(__name__)


class ReferenceResolver:
    """Resolve cleaned references into candidate ``CitationRecord`` objects.

    Parameters
    ----------
    sources:
        Mapping from ``IdentifierType`` to the data-source client responsible
        for that identifier kind (DOI → CrossRef, arXiv → ArXivSource, etc.).
    default_source:
        Fallback data source used for free-text queries when no domain-specific
        source is selected (typically CrossRef).
    cache:
        Optional ``CitationCache`` backed by AgentCore Memory.  When provided,
        the resolver checks the cache before querying external APIs and stores
        results after successful resolution.
    """

    def __init__(
        self,
        sources: dict[IdentifierType, DataSourceProtocol],
        default_source: DataSourceProtocol,
        cache: CitationCache | None = None,
    ) -> None:
        self._sources = sources
        self._default_source = default_source
        self._cache = cache

    def resolve(self, ref: CleanedReference) -> list[CitationRecord]:
        """Return candidate matches for *ref*.

        Resolution strategy:

        1. **Cache check** – if a cache is available, look up the reference.
           On a hit, return the cached record immediately (no external calls).
        2. **Identifier-based lookup** – when the reference carries a recognised
           identifier, dispatch to the registered source for that type.
        3. **Free-text search** – use ``route_query`` to pick the best source
           based on domain keywords, then search.  If the routed source returns
           no results, fall back to the default source (CrossRef).
        4. **Cache store** – after a successful resolution, store the top
           candidate in the cache for future lookups.
        """
        # ── 1. Cache check ────────────────────────────────────────────
        if self._cache is not None:
            try:
                cached = self._cache.get(ref)
                if cached is not None:
                    return [cached]
            except Exception:
                logger.debug("Cache lookup failed, proceeding with external query", exc_info=True)

        # ── 2. Identifier-based lookup ────────────────────────────────
        candidates: list[CitationRecord] = []

        if ref.identifier is not None:
            source = self._sources.get(ref.identifier.type, self._default_source)
            try:
                result = source.lookup(ref.identifier)
                if result is not None:
                    result.resolved_source = ref.identifier.type
                    candidates = [result]
            except Exception:
                logger.warning(
                    "Lookup failed for identifier %s via %s",
                    ref.identifier.value,
                    ref.identifier.type,
                    exc_info=True,
                )
        else:
            # ── 3. Free-text search ───────────────────────────────────
            routed_type = route_query(ref.cleaned_text)

            if routed_type is not None and routed_type in self._sources:
                source = self._sources[routed_type]
                try:
                    candidates = source.search(ref.cleaned_text)
                    for c in candidates:
                        c.resolved_source = routed_type
                except Exception:
                    logger.warning(
                        "Search failed for routed source %s",
                        routed_type,
                        exc_info=True,
                    )

            # Fallback to default source (CrossRef) if no results
            if not candidates:
                try:
                    candidates = self._default_source.search(ref.cleaned_text)
                    for c in candidates:
                        c.resolved_source = IdentifierType.DOI  # CrossRef → DOI
                except Exception:
                    logger.warning(
                        "Default source search failed",
                        exc_info=True,
                    )

        # ── 4. Cache store ────────────────────────────────────────────
        if candidates and self._cache is not None:
            try:
                self._cache.put(ref, candidates[0])
            except Exception:
                logger.debug("Cache store failed", exc_info=True)

        return candidates
