"""Font resolution with embeddable fallbacks for TokenMoulds MCP.

Ensures all fonts used in documents have embedding rights by:
1. Checking against known OFL/Apache fonts (Google Fonts, SIL)
2. Auto-substituting proprietary fonts with similar open alternatives
3. Reporting what was substituted and why

Font index and substitutions are loaded from the TokenMoulds font server:
  https://github.com/tokenmoulds/fonts

Local cache at ~/.tokenmoulds/fonts/index.yaml
"""

from __future__ import annotations

import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Set
import logging

try:
    import yaml

    HAS_YAML = True
except ImportError:
    yaml = None  # type: ignore[assignment]
    HAS_YAML = False

from tokenmoulds._compat import HAS_HTTPX, httpx


logger = logging.getLogger(__name__)


# ============================================================================
# Font Server Configuration
# ============================================================================

FONT_SERVER_URL = os.environ.get(
    "TOKENMOULDS_FONT_SERVER_URL",
    "https://raw.githubusercontent.com/tokenmoulds/fonts/main",
)
FONT_INDEX_URL = f"{FONT_SERVER_URL}/index.yaml"

CACHE_DIR = Path(os.environ.get("TOKENMOULDS_CACHE", str(Path.home() / ".tokenmoulds")))
FONT_CACHE_DIR = CACHE_DIR / "fonts"
FONT_INDEX_CACHE = FONT_CACHE_DIR / "index.yaml"


# ============================================================================
# Font Index (loaded from server or cache)
# ============================================================================


@dataclass
class FontIndex:
    """Font index loaded from server."""

    embeddable: Set[str] = field(default_factory=set)
    substitutions: Dict[str, str] = field(default_factory=dict)
    version: str = "0.0.0"
    loaded_from: str = "fallback"


# Global cached index
_font_index: Optional[FontIndex] = None


def _get_fallback_index() -> FontIndex:
    """Minimal fallback index if server/cache unavailable."""
    return FontIndex(
        embeddable={
            "inter",
            "roboto",
            "open sans",
            "lato",
            "montserrat",
            "poppins",
            "merriweather",
            "source serif pro",
            "eb garamond",
            "lora",
            "fira code",
            "jetbrains mono",
            "source code pro",
        },
        substitutions={
            "helvetica": "Inter",
            "helvetica neue": "Inter",
            "arial": "Inter",
            "georgia": "Merriweather",
            "times new roman": "Source Serif Pro",
            "times": "Source Serif Pro",
            "proxima nova": "Montserrat",
            "gotham": "Poppins",
            "avenir": "Nunito",
            "futura": "Jost",
            "calibri": "Open Sans",
            "sf mono": "JetBrains Mono",
            "sfmono": "JetBrains Mono",
            "menlo": "JetBrains Mono",
            "monaco": "JetBrains Mono",
        },
        version="fallback",
        loaded_from="fallback",
    )


def _load_index_from_yaml(content: str, source: str) -> FontIndex:
    """Parse YAML index content."""
    if not HAS_YAML:
        logger.warning("PyYAML not installed, using fallback font index")
        return _get_fallback_index()

    try:
        assert yaml is not None
        data = yaml.safe_load(content)

        embeddable = set()
        for family in data.get("embeddable", []):
            embeddable.add(family.lower().strip())

        substitutions = {}
        for prop, sub in data.get("substitutions", {}).items():
            substitutions[prop.lower().strip()] = sub

        return FontIndex(
            embeddable=embeddable,
            substitutions=substitutions,
            version=data.get("version", "unknown"),
            loaded_from=source,
        )
    except Exception as e:
        logger.warning(f"Failed to parse font index: {e}")
        return _get_fallback_index()


def _fetch_remote_index() -> Optional[str]:
    """Fetch font index from remote server."""
    if not HAS_HTTPX:
        logger.debug("httpx not installed, cannot fetch remote font index")
        return None

    try:
        assert httpx is not None
        with httpx.Client(timeout=5.0) as client:
            response = client.get(FONT_INDEX_URL)
            if response.status_code == 200:
                return response.text
    except Exception as e:
        logger.debug(f"Failed to fetch remote font index: {e}")

    return None


def _load_cached_index() -> Optional[str]:
    """Load font index from local cache."""
    if FONT_INDEX_CACHE.exists():
        try:
            return FONT_INDEX_CACHE.read_text(encoding="utf-8")
        except Exception as e:
            logger.debug(f"Failed to read cached font index: {e}")
    return None


def _save_index_cache(content: str) -> None:
    """Save font index to local cache."""
    try:
        FONT_CACHE_DIR.mkdir(parents=True, exist_ok=True)
        FONT_INDEX_CACHE.write_text(content, encoding="utf-8")
    except Exception as e:
        logger.debug(f"Failed to cache font index: {e}")


def get_font_index() -> FontIndex:
    """Get font index, fetching from server if needed.

    Resolution order:
    1. Cached in memory
    2. Remote server (with local cache update)
    3. Local cache
    4. Fallback defaults
    """
    global _font_index

    if _font_index is not None:
        return _font_index

    # Try remote first
    remote_content = _fetch_remote_index()
    if remote_content:
        _save_index_cache(remote_content)
        _font_index = _load_index_from_yaml(remote_content, "remote")
        logger.debug(f"Loaded font index from remote (v{_font_index.version})")
        return _font_index

    # Try local cache
    cached_content = _load_cached_index()
    if cached_content:
        _font_index = _load_index_from_yaml(cached_content, "cache")
        logger.debug(f"Loaded font index from cache (v{_font_index.version})")
        return _font_index

    # Fallback
    _font_index = _get_fallback_index()
    logger.debug("Using fallback font index")
    return _font_index


# ============================================================================
# Font Resolution
# ============================================================================


@dataclass
class FontResolution:
    """Result of font resolution."""

    requested: str
    resolved: str
    source: str  # "embeddable", "substitution", "fallback"
    license: str  # "OFL-1.1", "Apache-2.0", "unknown"
    embedding_allowed: bool
    note: Optional[str] = None


@dataclass
class FontComplianceReport:
    """Font compliance report for a brand."""

    fonts: Dict[str, FontResolution] = field(default_factory=dict)
    all_embeddable: bool = True
    substitutions_made: int = 0
    warnings: List[str] = field(default_factory=list)
    index_version: str = "unknown"
    index_source: str = "unknown"

    def add_font(self, role: str, resolution: FontResolution) -> None:
        """Add a font resolution to the report."""
        self.fonts[role] = resolution
        if resolution.source == "substitution":
            self.substitutions_made += 1
        if resolution.source == "fallback":
            self.warnings.append(
                f"{role}: '{resolution.requested}' not recognized, using {resolution.resolved}"
            )


def normalize_font_name(name: str) -> str:
    """Normalize font name for lookup."""
    return name.lower().strip()


def resolve_font(font_family: str) -> FontResolution:
    """Resolve a font to an embeddable alternative.

    Args:
        font_family: Font family name from CSS

    Returns:
        FontResolution with the resolved embeddable font
    """
    index = get_font_index()

    if not font_family:
        return FontResolution(
            requested="(none)",
            resolved="Inter",
            source="fallback",
            license="OFL-1.1",
            embedding_allowed=True,
            note="No font specified, using Inter",
        )

    normalized = normalize_font_name(font_family)

    # Check if already embeddable
    if normalized in index.embeddable:
        # Capitalize properly for display
        display_name = font_family.strip()
        return FontResolution(
            requested=display_name,
            resolved=display_name,
            source="embeddable",
            license="OFL-1.1",  # Most Google Fonts are OFL
            embedding_allowed=True,
        )

    # Check for substitution
    if normalized in index.substitutions:
        substitute = index.substitutions[normalized]
        return FontResolution(
            requested=font_family,
            resolved=substitute,
            source="substitution",
            license="OFL-1.1",
            embedding_allowed=True,
            note=f"'{font_family}' requires embedding license, using '{substitute}'",
        )

    # Unknown font - fall back to Inter
    return FontResolution(
        requested=font_family,
        resolved="Inter",
        source="fallback",
        license="OFL-1.1",
        embedding_allowed=True,
        note=f"'{font_family}' not in font index, using Inter as fallback",
    )


def resolve_brand_fonts(
    heading_font: Optional[str],
    body_font: Optional[str],
) -> FontComplianceReport:
    """Resolve fonts for a brand with compliance reporting.

    Args:
        heading_font: Font for headings
        body_font: Font for body text

    Returns:
        FontComplianceReport with resolved fonts and any warnings
    """
    index = get_font_index()
    report = FontComplianceReport(
        index_version=index.version,
        index_source=index.loaded_from,
    )

    # Resolve heading font
    heading = resolve_font(heading_font or "Inter")
    report.add_font("heading", heading)

    # Resolve body font
    body = resolve_font(body_font or "Merriweather")
    report.add_font("body", body)

    return report


def clean_font_value(font_value: Optional[str]) -> Optional[str]:
    """Clean CSS font value, filtering out keywords and variables.

    Args:
        font_value: Raw font value from CSS

    Returns:
        Cleaned font name or None if not a real font
    """
    if not font_value:
        return None

    font_lower = font_value.lower().strip()

    # Filter out CSS keywords
    css_keywords = {
        "inherit",
        "initial",
        "unset",
        "revert",
        "none",
        "auto",
        "serif",
        "sans-serif",
        "monospace",
        "cursive",
        "fantasy",
        "system-ui",
        "ui-serif",
        "ui-sans-serif",
        "ui-monospace",
        "ui-rounded",
        "emoji",
        "math",
        "fangsong",
    }

    if font_lower in css_keywords:
        return None

    # Filter out CSS variables
    if font_lower.startswith("var("):
        return None

    # Filter out quoted generic families
    if font_lower in ('"sans-serif"', "'sans-serif'", '"serif"', "'serif'"):
        return None

    # Remove quotes if present
    if (font_value.startswith('"') and font_value.endswith('"')) or (
        font_value.startswith("'") and font_value.endswith("'")
    ):
        font_value = font_value[1:-1]

    return font_value.strip() if font_value.strip() else None
