"""Font metrics extraction and adapter dataclasses for the DTCG generator."""

from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, Iterable, Mapping, Optional

try:
    from fontTools.ttLib import TTFont

    FONTTOOLS_AVAILABLE = True
except ImportError:
    TTFont = None  # type: ignore[assignment,misc]
    FONTTOOLS_AVAILABLE = False

from tokenmoulds.design.engine import ScaleRatio


@dataclass(frozen=True)
class FontMetricsAdapter:
    """Adapts raw font metrics dict to the interface DesignResolutionEngine expects."""

    x_height: float
    units_per_em: float
    cap_height: float
    ascender: float
    descender: float


@dataclass(frozen=True)
class FontConfigAdapter:
    """Provides .body and .for_role() for DesignResolutionEngine."""

    body: FontMetricsAdapter
    heading: FontMetricsAdapter
    _body_family: str
    _heading_family: str
    _mono_family: str = "Courier New"

    def for_role(self, role: str) -> str:
        if role.startswith("mono") or role.startswith("code"):
            return self._mono_family
        if role.startswith("display") or role.startswith("heading"):
            return self._heading_family
        return self._body_family


@dataclass(frozen=True)
class BrandAdapter:
    """Provides .tone and .density for DesignResolutionEngine."""

    tone: float
    density: float


TONE_SCALE_MAP: list[tuple[float, ScaleRatio]] = [
    (0.2, ScaleRatio.MINOR_SECOND),  # Very formal: tight scale
    (0.4, ScaleRatio.MAJOR_SECOND),  # Formal: moderate scale
    (0.6, ScaleRatio.MINOR_THIRD),  # Balanced: classic scale
    (0.8, ScaleRatio.MAJOR_THIRD),  # Casual: strong contrast
    (1.0, ScaleRatio.PERFECT_FOURTH),  # Very casual: dramatic
]


def select_scale_ratio(brand_tone: float) -> ScaleRatio:
    """Select modular scale ratio from brand tone (0-1)."""
    for threshold, ratio in TONE_SCALE_MAP:
        if brand_tone <= threshold:
            return ratio
    return ScaleRatio.MINOR_THIRD


def extract_font_metrics(
    *,
    font_pair: Mapping[str, str],
    font_metrics_cache: Optional[str],
    fonts_dir: Optional[str],
) -> Dict[str, Any]:
    """Extract font metrics for primary and serif families."""
    metrics_cache = load_metrics_cache(font_metrics_cache)
    # Canonical font role names: "heading" and "body".
    # Accept legacy aliases: "major"/"sans" → heading, "minor"/"serif" → body.
    primary_name = (
        font_pair.get("heading")
        or font_pair.get("major")
        or font_pair.get("sans")
        or font_pair.get("minor")
        or "Inter"
    )
    serif_name = font_pair.get("body") or font_pair.get("serif") or primary_name

    resolved_dir = resolve_fonts_dir(fonts_dir)

    if metrics_cache:
        primary_metrics = select_family_metrics(metrics_cache, primary_name)
        serif_metrics = select_family_metrics(metrics_cache, serif_name)
    else:
        metrics_cache = (
            extract_metrics_from_fonts(resolved_dir, {primary_name, serif_name})
            if resolved_dir
            else {}
        )
        primary_metrics = select_family_metrics(metrics_cache, primary_name)
        serif_metrics = select_family_metrics(metrics_cache, serif_name)

    if not primary_metrics:
        if not metrics_cache and resolved_dir:
            # Requested families not in vendored dir — load all available families
            available = [
                d.name
                for d in resolved_dir.iterdir()
                if d.is_dir() and any(d.glob("*.ttf"))
            ]
            metrics_cache = extract_metrics_from_fonts(resolved_dir, available)
        if not metrics_cache:
            raise ValueError(
                "No font metrics available. Provide font_metrics_cache or vendored "
                "fonts."
            )
        primary_metrics, primary_name = fallback_family_metrics(
            metrics_cache, primary_name, "primary"
        )

    if not serif_metrics:
        serif_metrics = primary_metrics

    return {
        "primary": primary_metrics,
        "serif": serif_metrics,
    }


def load_metrics_cache(
    font_metrics_cache: Optional[str],
) -> Dict[str, Dict[str, Dict[str, float]]]:
    """Load and merge font metrics from cache files."""
    metrics: Dict[str, Dict[str, Dict[str, float]]] = {}
    candidates = resolve_metrics_cache_paths(font_metrics_cache)

    for path in candidates:
        if not path.exists():
            if font_metrics_cache and path == Path(font_metrics_cache):
                raise FileNotFoundError(f"Font metrics cache not found: {path}")
            continue

        with path.open(encoding="utf-8") as handle:
            data = json.load(handle)

        for family_name, weights in data.items():
            family_key = str(family_name)
            metrics.setdefault(family_key, {})
            for weight_key, payload in weights.items():
                metrics[family_key][str(weight_key)] = normalize_metrics_payload(
                    payload
                )

    return metrics


def resolve_metrics_cache_paths(
    font_metrics_cache: Optional[str],
) -> list[Path]:
    """Return candidate paths for font metrics cache files."""
    candidates: list[Path] = []
    if font_metrics_cache:
        candidates.append(Path(font_metrics_cache))
    candidates.extend(
        [
            Path("data/fonts/cache/metrics.json"),
        ]
    )
    return candidates


def normalize_metrics_payload(payload: Mapping[str, Any]) -> Dict[str, float]:
    """Normalize a raw metrics payload to canonical float keys."""
    return {
        "units_per_em": float(payload["units_per_em"]),
        "x_height": float(payload["x_height"]),
        "cap_height": float(payload["cap_height"]),
        "ascender": float(payload["ascender"]),
        "descender": float(payload["descender"]),
        "line_gap": float(payload.get("line_gap", 0)),
    }


def select_family_metrics(
    metrics_cache: Mapping[str, Mapping[str, Mapping[str, float]]],
    family_name: str,
) -> Optional[Dict[str, float]]:
    """Select metrics for a font family from the cache, case-insensitive."""
    if not family_name:
        return None

    if family_name in metrics_cache:
        return select_weight_metrics(metrics_cache[family_name])

    family_lower = family_name.lower()
    for cached_family, weights in metrics_cache.items():
        if cached_family.lower() == family_lower:
            return select_weight_metrics(weights)

    return None


def select_weight_metrics(
    weights: Mapping[str, Mapping[str, float]],
) -> Optional[Dict[str, float]]:
    """Select the weight closest to 400 (regular) from a family's weights."""
    if not weights:
        return None
    if "400" in weights:
        return dict(weights["400"])
    numeric_weights = []
    for key, metrics in weights.items():
        if str(key).isdigit():
            numeric_weights.append((int(key), metrics))
    if numeric_weights:
        _, metrics = min(numeric_weights, key=lambda item: abs(item[0] - 400))
        return dict(metrics)
    return dict(next(iter(weights.values())))


def fallback_family_metrics(
    metrics_cache: Mapping[str, Mapping[str, Mapping[str, float]]],
    requested_family: str,
    role: str,
) -> tuple[Dict[str, float], str]:
    """Fall back to the first available family in the cache."""
    fallback_family = sorted(metrics_cache.keys())[0]
    metrics = select_family_metrics(metrics_cache, fallback_family)
    if not metrics:
        raise ValueError("Font metrics cache is missing usable entries.")
    return metrics, fallback_family


def resolve_fonts_dir(fonts_dir: Optional[str]) -> Optional[Path]:
    """Resolve the directory containing vendored font files."""
    if fonts_dir:
        return Path(fonts_dir)
    for candidate in (Path("data/fonts/vendored"),):
        if candidate.exists():
            return candidate
    return None


def extract_metrics_from_fonts(
    fonts_dir: Path, families: Iterable[str]
) -> Dict[str, Dict[str, Dict[str, float]]]:
    """Extract metrics from .ttf files on disk using fonttools."""
    if not FONTTOOLS_AVAILABLE:
        raise RuntimeError("fonttools is required to extract font metrics.")

    metrics: Dict[str, Dict[str, Dict[str, float]]] = {}
    for family in families:
        if not family:
            continue
        family_metrics = extract_family_metrics(fonts_dir, family)
        if family_metrics:
            metrics[family] = family_metrics
    return metrics


def extract_family_metrics(fonts_dir: Path, family: str) -> Dict[str, Dict[str, float]]:
    """Extract metrics for all weights of a font family."""
    family_metrics: Dict[str, Dict[str, float]] = {}
    candidate_dirs = [
        fonts_dir / family,
        fonts_dir / family.replace(" ", ""),
        fonts_dir / family.lower(),
        fonts_dir / family.lower().replace(" ", ""),
    ]
    ttf_files: list[Path] = []
    for candidate in candidate_dirs:
        if candidate.exists():
            ttf_files = list(candidate.glob("*.ttf"))
            if ttf_files:
                break
    if not ttf_files:
        family_slug = family.lower().replace(" ", "")
        ttf_files = [
            path
            for path in fonts_dir.rglob("*.ttf")
            if family_slug in path.stem.lower().replace(" ", "")
        ]

    for ttf_file in ttf_files:
        metrics = extract_single_font_metrics(ttf_file)
        if metrics:
            weight_key = str(metrics["weight"])
            family_metrics[weight_key] = metrics["metrics"]

    return family_metrics


def extract_single_font_metrics(ttf_file: Path) -> Optional[Dict[str, Any]]:
    """Extract metrics from a single .ttf file."""
    try:
        assert TTFont is not None
        font = TTFont(ttf_file)
    except Exception:
        return None

    try:
        head = font["head"]
        hhea = font["hhea"]
        os2 = font.get("OS/2")
        name = font["name"]

        family_name = _get_font_name(name, 1) or ttf_file.stem
        units_per_em: int = getattr(head, "unitsPerEm", 1000)
        x_height = (
            getattr(os2, "sxHeight", units_per_em * 0.5) if os2 else units_per_em * 0.5
        )
        cap_height = (
            getattr(os2, "sCapHeight", units_per_em * 0.7)
            if os2
            else units_per_em * 0.7
        )
        ascender = hhea.ascent
        descender = hhea.descent
        line_gap: int = getattr(hhea, "lineGap", 0)
        weight = _determine_weight(ttf_file.name, os2)
    finally:
        font.close()

    return {
        "family_name": family_name,
        "weight": weight,
        "metrics": {
            "units_per_em": float(units_per_em),
            "x_height": float(x_height),
            "cap_height": float(cap_height),
            "ascender": float(ascender),
            "descender": float(descender),
            "line_gap": float(line_gap),
        },
    }


def _get_font_name(name_table: Any, name_id: int) -> Optional[str]:
    """Get a font name from the name table."""
    for record in getattr(name_table, "names", []):
        if record.nameID == name_id and record.platformID == 3:
            try:
                return record.toUnicode()
            except Exception:
                continue
    return None


def _determine_weight(filename: str, os2_table: Any) -> int:
    """Determine font weight from OS/2 table or filename heuristics."""
    if os2_table and hasattr(os2_table, "usWeightClass"):
        return int(os2_table.usWeightClass)

    filename_lower = filename.lower()
    weight_map = {
        "thin": 100,
        "extralight": 200,
        "ultralight": 200,
        "light": 300,
        "regular": 400,
        "normal": 400,
        "medium": 500,
        "semibold": 600,
        "semi-bold": 600,
        "bold": 700,
        "extrabold": 800,
        "ultrabold": 800,
        "black": 900,
        "heavy": 900,
    }
    for weight_name, weight_value in weight_map.items():
        if weight_name in filename_lower:
            return weight_value
    return 400
