"""Font resolution service with pluggable provider chain.

Borrowed from svg2ooxml's FontService pattern:
- FontQuery/FontMatch frozen dataclasses for type-safe lookup
- FontProvider protocol for pluggable resolution
- Provider chain with fallback (vendored → system → default)
"""

from __future__ import annotations

import logging
import platform
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Iterator, Protocol, runtime_checkable
from urllib.parse import urlparse

from tokenmoulds.caching import LRUCache
from tokenmoulds._compat import HAS_HTTPX, httpx
from tokenmoulds.settings import get_settings

logger = logging.getLogger(__name__)


@dataclass(frozen=True, slots=True)
class FontQuery:
    """Immutable query for font resolution."""

    family: str
    weight: int = 400
    style: str = "normal"  # "normal" | "italic"


@dataclass(frozen=True, slots=True)
class FontMatch:
    """Result of a font resolution query."""

    family: str
    path: Path
    weight: int = 400
    style: str = "normal"
    found_via: str = ""


@dataclass(frozen=True, slots=True)
class DownloadableFontFamily:
    """Download source definition for a single embeddable family."""

    family: str
    styles: dict[str, str]
    aliases: tuple[str, ...] = ()


@runtime_checkable
class FontProvider(Protocol):
    """Protocol for pluggable font resolution."""

    def resolve(self, query: FontQuery) -> FontMatch | None:
        """Try to resolve a font query. Return None if not found."""
        ...

    def list_families(self) -> list[str]:
        """List all available font families."""
        ...


_DEFAULT_DOWNLOADABLE_FONTS: tuple[DownloadableFontFamily, ...] = (
    DownloadableFontFamily(
        family="JetBrains Mono",
        aliases=("SF Mono", "SFMono", "ui-monospace", "Menlo", "Monaco"),
        styles={
            "regular": (
                "https://raw.githubusercontent.com/JetBrains/JetBrainsMono/"
                "master/fonts/ttf/JetBrainsMono-Regular.ttf"
            ),
            "bold": (
                "https://raw.githubusercontent.com/JetBrains/JetBrainsMono/"
                "master/fonts/ttf/JetBrainsMono-Bold.ttf"
            ),
            "italic": (
                "https://raw.githubusercontent.com/JetBrains/JetBrainsMono/"
                "master/fonts/ttf/JetBrainsMono-Italic.ttf"
            ),
            "bold_italic": (
                "https://raw.githubusercontent.com/JetBrains/JetBrainsMono/"
                "master/fonts/ttf/JetBrainsMono-BoldItalic.ttf"
            ),
        },
    ),
)


class VendoredFontProvider:
    """Resolve fonts from a vendored directory.

    Scans a directory tree for .ttf/.otf files, building a family → path
    index on first query.
    """

    def __init__(self, fonts_dir: Path) -> None:
        self._fonts_dir = fonts_dir
        self._index: dict[tuple[str, int, str], Path] | None = None

    def _build_index(self) -> dict[tuple[str, int, str], Path]:
        """Scan fonts directory and build lookup index."""
        index: dict[tuple[str, int, str], Path] = {}
        if not self._fonts_dir.is_dir():
            return index

        for font_file in self._fonts_dir.rglob("*"):
            if font_file.suffix.lower() not in (".ttf", ".otf"):
                continue
            name = font_file.stem.lower()
            family = font_file.parent.name

            # Infer weight/style from filename conventions
            weight = 700 if "bold" in name else 400
            style = "italic" if "italic" in name else "normal"

            key = (family.lower(), weight, style)
            index[key] = font_file

        return index

    def _ensure_index(self) -> dict[tuple[str, int, str], Path]:
        if self._index is None:
            self._index = self._build_index()
        return self._index

    def resolve(self, query: FontQuery) -> FontMatch | None:
        index = self._ensure_index()
        key = (query.family.lower(), query.weight, query.style)
        path = index.get(key)
        if path is not None:
            return FontMatch(
                family=query.family,
                path=path,
                weight=query.weight,
                style=query.style,
                found_via="vendored",
            )
        # Try relaxed match (ignore weight/style)
        for (fam, w, s), p in index.items():
            if fam == query.family.lower():
                return FontMatch(
                    family=query.family,
                    path=p,
                    weight=w,
                    style=s,
                    found_via="vendored",
                )
        return None

    def list_families(self) -> list[str]:
        index = self._ensure_index()
        return sorted({fam for fam, _, _ in index})


class SystemFontProvider:
    """Resolve fonts from system font directories.

    Platform-aware: scans macOS, Windows, and Linux font directories.
    """

    def __init__(self, extra_dirs: list[Path] | None = None) -> None:
        self._dirs = list(extra_dirs or []) + _platform_font_dirs()
        self._index: dict[str, Path] | None = None

    def _build_index(self) -> dict[str, Path]:
        index: dict[str, Path] = {}
        for d in self._dirs:
            if not d.is_dir():
                continue
            for font_file in d.rglob("*"):
                if font_file.suffix.lower() not in (".ttf", ".otf"):
                    continue
                # Use stem as approximate family name
                index.setdefault(font_file.stem.lower(), font_file)
        return index

    def _ensure_index(self) -> dict[str, Path]:
        if self._index is None:
            self._index = self._build_index()
        return self._index

    def resolve(self, query: FontQuery) -> FontMatch | None:
        index = self._ensure_index()

        # Try exact stem match
        candidates = [
            stem
            for stem in index
            if query.family.lower().replace(" ", "")
            in stem.replace("-", "").replace("_", "")
        ]
        if not candidates:
            return None

        # Prefer regular weight, then bold
        for stem in candidates:
            is_bold = "bold" in stem
            is_italic = "italic" in stem
            if query.weight >= 700 and is_bold:
                return FontMatch(
                    family=query.family,
                    path=index[stem],
                    weight=700,
                    style="italic" if is_italic else "normal",
                    found_via="system",
                )
            if query.weight < 700 and not is_bold:
                return FontMatch(
                    family=query.family,
                    path=index[stem],
                    weight=400,
                    style="italic" if is_italic else "normal",
                    found_via="system",
                )

        # Fallback to first candidate
        stem = candidates[0]
        return FontMatch(
            family=query.family,
            path=index[stem],
            weight=query.weight,
            style=query.style,
            found_via="system",
        )

    def list_families(self) -> list[str]:
        index = self._ensure_index()
        return sorted(set(index.keys()))


class DownloadedFontProvider:
    """Resolve fonts by downloading approved embeddable faces into cache."""

    def __init__(
        self,
        cache_dir: Path | None = None,
        *,
        catalog: tuple[DownloadableFontFamily, ...] | None = None,
        fetcher: Callable[[str], bytes] | None = None,
    ) -> None:
        settings = get_settings()
        self._cache_dir = cache_dir or settings.cache_dir / "fonts" / "downloaded"
        self._catalog = catalog or _DEFAULT_DOWNLOADABLE_FONTS
        self._fetcher = fetcher or self._download_bytes
        self._families: dict[str, DownloadableFontFamily] = {}
        self._aliases: dict[str, str] = {}
        for item in self._catalog:
            family_key = _normalize_font_name(item.family)
            self._families[family_key] = item
            self._aliases[family_key] = family_key
            for alias in item.aliases:
                self._aliases[_normalize_font_name(alias)] = family_key

    def resolve(self, query: FontQuery) -> FontMatch | None:
        entry = self._lookup_family(query.family)
        if entry is None:
            return None

        style_key = _query_style_kind(query)
        url = entry.styles.get(style_key)
        if url is None:
            url = entry.styles.get("regular")
            style_key = "regular"
        if url is None:
            return None

        target_path = self._cache_path(entry.family, url)
        if not target_path.exists():
            try:
                target_path.write_bytes(self._fetcher(url))
            except Exception as exc:
                logger.warning(
                    "Failed to download font '%s' from %s: %s",
                    entry.family,
                    url,
                    exc,
                )
                return None

        weight, style = _style_kind_to_match(style_key)
        return FontMatch(
            family=entry.family,
            path=target_path,
            weight=weight,
            style=style,
            found_via="downloaded",
        )

    def list_families(self) -> list[str]:
        return sorted(item.family for item in self._catalog)

    def _lookup_family(self, family: str) -> DownloadableFontFamily | None:
        canonical_key = self._aliases.get(_normalize_font_name(family))
        if canonical_key is None:
            return None
        return self._families.get(canonical_key)

    def _cache_path(self, family: str, url: str) -> Path:
        parsed = urlparse(url)
        filename = Path(parsed.path).name or f"{_normalize_font_name(family)}.ttf"
        family_dir = self._cache_dir / _normalize_font_name(family)
        family_dir.mkdir(parents=True, exist_ok=True)
        return family_dir / filename

    def _download_bytes(self, url: str) -> bytes:
        if not HAS_HTTPX:
            raise RuntimeError("httpx is required for downloadable font resolution")
        assert httpx is not None
        response = httpx.get(url, timeout=30.0, follow_redirects=True)
        response.raise_for_status()
        return response.content


class FontService:
    """Multi-provider font resolution with fallback chain.

    Usage:
        service = FontService()
        service.register(VendoredFontProvider(fonts_dir))
        service.register(SystemFontProvider())

        match = service.find_font(FontQuery("Inter", weight=400))
        if match:
            print(f"Found {match.family} at {match.path} via {match.found_via}")
    """

    def __init__(self) -> None:
        self._providers: list[FontProvider] = []
        self._cache: LRUCache[FontMatch | None] = LRUCache(max_items=1024)

    @staticmethod
    def _query_key(query: FontQuery) -> str:
        return f"{query.family}:{query.weight}:{query.style}"

    def register(self, provider: FontProvider) -> None:
        """Add a provider to the end of the chain."""
        self._providers.append(provider)
        self._cache.clear()

    def prepend(self, provider: FontProvider) -> None:
        """Add a high-priority provider to the front of the chain."""
        self._providers.insert(0, provider)
        self._cache.clear()

    def find_font(self, query: FontQuery) -> FontMatch | None:
        """Resolve a font query through the provider chain.

        Returns the first match, or None if no provider can resolve.
        """
        key = self._query_key(query)
        if key in self._cache:
            return self._cache.get(key)

        for provider in self._providers:
            match = provider.resolve(query)
            if match is not None:
                self._cache.put(key, match)
                return match

        self._cache.put(key, None)
        return None

    def iter_alternatives(self, query: FontQuery) -> Iterator[FontMatch]:
        """Yield all matches across all providers."""
        for provider in self._providers:
            match = provider.resolve(query)
            if match is not None:
                yield match

    def list_all_families(self) -> list[str]:
        """List families from all providers (deduplicated)."""
        families: set[str] = set()
        for provider in self._providers:
            families.update(provider.list_families())
        return sorted(families)

    def clear_cache(self) -> None:
        """Clear the resolution cache."""
        self._cache.clear()


def build_font_service(
    fonts_dir: Path | None,
    *,
    enable_downloads: bool = True,
    include_system: bool = False,
    download_cache_dir: Path | None = None,
    download_catalog: tuple[DownloadableFontFamily, ...] | None = None,
    download_fetcher: Callable[[str], bytes] | None = None,
) -> FontService:
    """Create the default runtime font service for template generation."""
    service = FontService()
    if fonts_dir is not None:
        service.register(VendoredFontProvider(fonts_dir))
    if enable_downloads:
        service.register(
            DownloadedFontProvider(
                cache_dir=download_cache_dir,
                catalog=download_catalog,
                fetcher=download_fetcher,
            )
        )
    if include_system:
        service.register(SystemFontProvider())
    return service


def _normalize_font_name(name: str) -> str:
    return "".join(ch for ch in name.lower() if ch.isalnum())


def _query_style_kind(query: FontQuery) -> str:
    is_bold = query.weight >= 700
    is_italic = query.style == "italic"
    if is_bold and is_italic:
        return "bold_italic"
    if is_bold:
        return "bold"
    if is_italic:
        return "italic"
    return "regular"


def _style_kind_to_match(style_kind: str) -> tuple[int, str]:
    if style_kind == "bold_italic":
        return 700, "italic"
    if style_kind == "bold":
        return 700, "normal"
    if style_kind == "italic":
        return 400, "italic"
    return 400, "normal"


def _platform_font_dirs() -> list[Path]:
    """Return platform-specific system font directories."""
    home = Path.home()
    system = platform.system()
    if system == "Darwin":
        return [
            Path("/System/Library/Fonts"),
            Path("/Library/Fonts"),
            home / "Library/Fonts",
        ]
    elif system == "Windows":
        return [
            Path("C:/Windows/Fonts"),
            home / "AppData/Local/Microsoft/Windows/Fonts",
        ]
    else:  # Linux
        return [
            Path("/usr/share/fonts"),
            Path("/usr/local/share/fonts"),
            home / ".fonts",
            home / ".local/share/fonts",
        ]
