"""Design Resolution Engine - Typography and Color Calculator.

This is the single authoritative calculator for all design token resolution.
It computes typography (type scale, line heights, spacing) and colors from
input parameters and font metrics.

Key concepts:
- Baseline grid: Derived from body font x-height, all vertical rhythm aligns to this
- Type scale: Modular scale with configurable ratios
- Line height: Snapped to baseline grid for vertical rhythm
- Spacing: Multiples of baseline grid
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple

from tokenmoulds.design.roles import ROLE_SCALE_MAP
from tokenmoulds.design.units import (
    EMU_PER_POINT,
    HALF_POINTS_PER_POINT,
    pt_to_emu,
    pt_to_twips,
    TypographyConverter,
)

if TYPE_CHECKING:
    from tokenmoulds.design.page_geometry import PageGeometry


class ScaleRatio(Enum):
    """Classic typographic scale ratios."""

    MINOR_SECOND = 1.067  # Tight
    MAJOR_SECOND = 1.125  # Good for dense UI
    MINOR_THIRD = 1.2  # Balanced
    MAJOR_THIRD = 1.25  # Classic
    PERFECT_FOURTH = 1.333  # Strong contrast
    AUGMENTED_FOURTH = 1.414  # Geometric (sqrt 2)
    PERFECT_FIFTH = 1.5  # Dramatic
    GOLDEN_RATIO = 1.618  # Natural harmony


class LineHeightMode(Enum):
    """How line height is calculated."""

    MULTIPLIER = "multiplier"  # Relative to font size
    SNAPPED = "snapped"  # Snapped to baseline grid


@dataclass(frozen=True)
class Baseline:
    """Baseline grid for vertical rhythm."""

    grid_emu: int
    grid_pt: float

    @property
    def grid_twips(self) -> int:
        return pt_to_twips(self.grid_pt)

    def snap_pt(self, value_pt: float) -> float:
        """Snap a point value to the baseline grid."""
        if self.grid_pt <= 0:
            return value_pt
        return round(value_pt / self.grid_pt) * self.grid_pt

    def snap_up_pt(self, value_pt: float) -> float:
        """Snap a point value up to next baseline grid line."""
        if self.grid_pt <= 0:
            return value_pt
        return math.ceil(value_pt / self.grid_pt) * self.grid_pt


LigatureMode = Literal[
    "none",
    "standard",
    "contextual",
    "historical",
    "discretional",
    "standardContextual",
    "all",
]
NumberForm = Literal["default", "lining", "oldStyle"]
NumberSpacing = Literal["default", "proportional", "tabular"]


@dataclass(frozen=True)
class OpenTypeSettings:
    """Derived OpenType feature settings.

    These are derived from brand tone, content role, and typographic context.
    """

    # Ligatures: formal brands use standard, casual brands can use all
    ligatures: LigatureMode = "standard"
    # Number form: lining for headers/tables, oldstyle for body
    number_form: NumberForm = "default"
    # Number spacing: tabular for tables/data, proportional for text
    number_spacing: NumberSpacing = "default"
    # Contextual alternates: usually on
    contextual_alternates: bool = True
    # Stylistic sets (font-specific)
    stylistic_sets: Tuple[int, ...] = ()


@dataclass(frozen=True)
class TrackingSettings:
    """Derived tracking (letter-spacing) settings.

    Tracking varies by:
    - Font size: larger text needs tighter tracking
    - Role: headings tighter, body normal, captions looser
    - Caps: all-caps and small-caps need looser tracking
    """

    # Base tracking in twips (20 twips = 1pt)
    tracking_twips: int = 0
    # Tracking for all-caps text (always looser)
    caps_tracking_twips: int = 80
    # Tracking for small-caps (moderately looser)
    small_caps_tracking_twips: int = 40


@dataclass(frozen=True)
class RoleTypography:
    """Typography settings for a semantic role."""

    font_family: str
    font_size_pt: float
    line_height_pt: float
    kerning_emu: int
    space_before_pt: float
    space_after_pt: float
    # Derived OpenType and tracking settings
    opentype: OpenTypeSettings = OpenTypeSettings()
    tracking: TrackingSettings = TrackingSettings()

    @property
    def font_size_twips(self) -> int:
        return pt_to_twips(self.font_size_pt)

    @property
    def font_size_half_points(self) -> int:
        """Font size in half-points (OOXML sz attribute)."""
        return int(round(self.font_size_pt * HALF_POINTS_PER_POINT))

    @property
    def line_height_twips(self) -> int:
        return pt_to_twips(self.line_height_pt)

    @property
    def space_before_twips(self) -> int:
        return pt_to_twips(self.space_before_pt)

    @property
    def space_after_twips(self) -> int:
        return pt_to_twips(self.space_after_pt)

    @property
    def line_height_multiplier(self) -> float:
        """Effective line height as multiplier of font size."""
        if self.font_size_pt == 0:
            return 1.0
        return self.line_height_pt / self.font_size_pt


@dataclass(frozen=True)
class TypeScaleStep:
    """A single step in the type scale."""

    index: int
    name: str
    size_pt: float
    size_emu: int
    size_twips: int


@dataclass(frozen=True)
class SpacingScale:
    """Spacing scale based on baseline grid."""

    baseline_pt: float
    scale: Dict[str, float]  # name -> pt value

    def get_pt(self, name: str) -> float:
        return self.scale.get(name, self.baseline_pt)

    def get_twips(self, name: str) -> int:
        return pt_to_twips(self.get_pt(name))

    def get_emu(self, name: str) -> int:
        return pt_to_emu(self.get_pt(name))


@dataclass(frozen=True)
class ResolvedDesign:
    """Complete resolved design system."""

    baseline: Baseline
    type_scale: Tuple[TypeScaleStep, ...]
    roles: Dict[str, RoleTypography]
    spacing: SpacingScale
    colors: Dict[str, str]
    page_geometry: Optional[PageGeometry] = None

    def get_type_step(self, name: str) -> Optional[TypeScaleStep]:
        """Get type scale step by name."""
        for step in self.type_scale:
            if step.name == name:
                return step
        return None

    @property
    def has_page_geometry(self) -> bool:
        """Check if page geometry is available."""
        return self.page_geometry is not None


class DesignResolutionEngine:
    """Single authoritative calculator for typography and color.

    Example:
        engine = DesignResolutionEngine(
            fonts=font_config,
            brand=BrandConfig(tone=0.6, density=0.4),
            locale="en-US",
            colors=color_inputs,
            base_font_size_pt=12.0,
            scale_ratio=ScaleRatio.MAJOR_THIRD,
        )
        design = engine.resolve()

        # Access resolved typography
        body = design.roles["text.body.primary"]
        h1 = design.roles["display.primary"]

        # Access spacing
        paragraph_space = design.spacing.get_twips("paragraph")

        # Access column layouts
        columns = engine.calculate_column_layouts(page_width_pt=500, baseline=design.baseline)
        two_col = columns["body_2col"]
    """

    # Standard column configurations
    COLUMN_PRESETS = {
        "body_2col": {"columns": 2, "description": "Standard 2-column body text"},
        "body_3col": {"columns": 3, "description": "3-column for wider content"},
        "sidebar": {"columns": 3, "description": "Main content + sidebar (2:1 ratio)"},
        "narrow_2col": {"columns": 2, "description": "Tight 2-column for narrow pages"},
        "wide_4col": {"columns": 4, "description": "4-column for wide layouts"},
        "magazine_6col": {"columns": 6, "description": "6-column magazine layout"},
    }

    # Default type scale multipliers (10 steps)
    DEFAULT_SCALE = [0.75, 0.875, 1.0, 1.125, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0]

    # Semantic names for scale steps
    SCALE_NAMES = [
        "footnote",
        "caption",
        "body",
        "large",
        "h6",
        "h5",
        "h4",
        "h3",
        "h2",
        "h1",
    ]

    # Line height presets
    LINE_HEIGHT_PRESETS = {
        "tight": 1.2,
        "snug": 1.3,
        "normal": 1.4,
        "relaxed": 1.5,
        "loose": 1.6,
    }

    # Candidate grid modules in points — clean integers that divide
    # common page widths well.
    GRID_CANDIDATES_PT = (4, 6, 8, 9, 10, 12)

    def __init__(
        self,
        fonts,
        brand,
        locale: str,
        colors: Dict,
        base_font_size_pt: float = 11.0,
        base_grid_pt: float = 0,
        scale_ratio: Optional[ScaleRatio] = None,
        line_height_mode: LineHeightMode = LineHeightMode.SNAPPED,
        line_height_multiplier: float = 0,
        page_geometry: Optional[PageGeometry] = None,
    ):
        """Initialize the design resolution engine.

        Args:
            fonts: Font configuration with metrics (body, heading fonts)
            brand: Brand configuration (tone, density values 0-1)
            locale: Locale code for language-specific adjustments
            colors: Color input configuration
            base_font_size_pt: Desired body font size in points
            base_grid_pt: Explicit grid module in points (0 = auto-select)
            scale_ratio: Optional modular scale ratio (uses DEFAULT_SCALE if None)
            line_height_mode: How to calculate line heights
            line_height_multiplier: Explicit line-height multiplier (0 = derive from x-height)
            page_geometry: Optional page geometry for layout calculations
        """
        self.fonts = fonts
        self.brand = brand
        self.locale = locale
        self.colors_input = colors
        self.base_font_size_pt = base_font_size_pt
        self._explicit_grid_pt = base_grid_pt
        self.scale_ratio = scale_ratio
        self.line_height_mode = line_height_mode
        self.page_geometry = page_geometry
        self._converter = TypographyConverter()

        # Pre-compute the line-height multiplier (invariant across roles)
        if line_height_multiplier > 0:
            self._lh_multiplier = line_height_multiplier
        else:
            x_ratio = fonts.body.x_height / fonts.body.units_per_em
            # Large x-height → more leading needed.
            # Range: x_ratio 0.40 → 1.30,  x_ratio 0.55 → 1.50
            self._lh_multiplier = 1.1 + x_ratio * 0.75

    # Keep old name working for callers that still pass base_grid_pt
    @property
    def base_grid_pt(self) -> float:
        return self.base_font_size_pt

    def resolve(self) -> ResolvedDesign:
        """Resolve all design tokens."""
        baseline = self._calculate_baseline()
        type_scale = self._build_type_scale(baseline)
        roles = self._resolve_roles(baseline, type_scale)
        spacing = self._build_spacing_scale(baseline)
        colors: Dict[str, str] = {}

        # Update page geometry with calculated baseline grid if provided
        page_geometry = self._update_page_geometry(baseline)

        return ResolvedDesign(
            baseline=baseline,
            type_scale=type_scale,
            roles=roles,
            spacing=spacing,
            colors=colors,
            page_geometry=page_geometry,
        )

    def _calculate_baseline(self) -> Baseline:
        """Choose a clean baseline grid module.

        If ``base_grid_pt`` was given explicitly, use it.  Otherwise
        pick the largest candidate from ``GRID_CANDIDATES_PT`` that is
        ≤ the minimum comfortable line height for the body font (derived
        from x-height).  This ensures body text always fits in a whole
        number of grid lines.
        """
        if self._explicit_grid_pt > 0:
            grid_pt = float(self._explicit_grid_pt)
        else:
            # Minimum line height ≈ ascender − descender + line gap,
            # but a simple heuristic: body size * ~1.2 gives the
            # tightest readable line height.
            min_lh_pt = self.base_font_size_pt * 1.2

            grid_pt = self.GRID_CANDIDATES_PT[0]
            for candidate in self.GRID_CANDIDATES_PT:
                if candidate <= min_lh_pt:
                    grid_pt = float(candidate)

        grid_emu = pt_to_emu(grid_pt)
        return Baseline(grid_emu=grid_emu, grid_pt=grid_pt)

    def _build_type_scale(self, baseline: Baseline) -> Tuple[TypeScaleStep, ...]:
        """Build the type scale.

        The base of the scale is ``base_font_size_pt`` (the desired body
        font size).  Font sizes are rounded to half-point precision
        (OOXML's native ``w:sz`` granularity).  Line heights and spacing
        are snapped to the baseline grid separately.
        """
        if self.scale_ratio:
            # Use modular scale rooted at the desired body size
            scale = self._generate_modular_scale(
                self.base_font_size_pt, self.scale_ratio.value, len(self.SCALE_NAMES)
            )
        else:
            # Use default multiplier scale
            scale = [self.base_font_size_pt * m for m in self.DEFAULT_SCALE]

        steps = []
        for i, (size_pt, name) in enumerate(zip(scale, self.SCALE_NAMES)):
            # Round to nearest half-point (0.5 pt precision)
            rounded_pt = round(size_pt * 2) / 2
            steps.append(
                TypeScaleStep(
                    index=i,
                    name=name,
                    size_pt=rounded_pt,
                    size_emu=pt_to_emu(rounded_pt),
                    size_twips=pt_to_twips(rounded_pt),
                )
            )

        return tuple(steps)

    def _generate_modular_scale(
        self, base_size: float, ratio: float, steps: int
    ) -> List[float]:
        """Generate a modular scale from a ratio.

        Creates sizes both above and below the base size.
        """
        # Calculate how many steps below base (roughly 1/4 of total)
        steps_below = steps // 4
        sizes = []

        for i in range(-steps_below, steps - steps_below):
            size = base_size * (ratio**i)
            sizes.append(size)

        return sizes

    def _resolve_roles(
        self, baseline: Baseline, type_scale: Tuple[TypeScaleStep, ...]
    ) -> Dict[str, RoleTypography]:
        """Resolve typography for each semantic role."""
        roles: Dict[str, RoleTypography] = {}

        # Create lookup for type scale by index
        scale_lookup = {step.index: step for step in type_scale}

        for role, scale_index in ROLE_SCALE_MAP.items():
            # Get font size from type scale
            step = scale_lookup.get(scale_index)
            if step:
                font_size_pt = step.size_pt
            else:
                # Fallback to base size
                font_size_pt = baseline.grid_pt

            # Calculate line height
            line_height_pt = self._calculate_line_height(font_size_pt, baseline)

            # Calculate kerning from brand parameters
            kerning_emu = self._calculate_kerning(baseline)

            # Calculate spacing (before/after)
            space_before_pt, space_after_pt = self._calculate_spacing(
                role, baseline, font_size_pt
            )

            # Calculate derived OpenType settings
            opentype = self._calculate_opentype_settings(role, font_size_pt)

            # Calculate derived tracking settings
            tracking = self._calculate_tracking_settings(role, font_size_pt, baseline)

            roles[role] = RoleTypography(
                font_family=self.fonts.for_role(role),
                font_size_pt=font_size_pt,
                line_height_pt=line_height_pt,
                kerning_emu=kerning_emu,
                space_before_pt=space_before_pt,
                space_after_pt=space_after_pt,
                opentype=opentype,
                tracking=tracking,
            )

        return roles

    def _calculate_line_height(self, font_size_pt: float, baseline: Baseline) -> float:
        """Calculate line height for a font size."""
        raw_line_height = font_size_pt * self._lh_multiplier

        if self.line_height_mode == LineHeightMode.SNAPPED:
            return baseline.snap_up_pt(raw_line_height)
        else:
            return raw_line_height

    def _calculate_kerning(self, baseline: Baseline) -> int:
        """Calculate kerning from brand parameters."""
        # tone: 0 = formal/tight, 1 = casual/loose
        # density: 0 = spacious, 1 = compact
        tone_multiplier = 0.8 + (self.brand.tone * 0.6)
        density_multiplier = 1.5 - (self.brand.density * 0.8)

        return int(baseline.grid_emu * tone_multiplier * density_multiplier)

    def _calculate_opentype_settings(
        self, role: str, font_size_pt: float
    ) -> OpenTypeSettings:
        """Calculate OpenType feature settings based on role and brand.

        Derivation rules:
        - Ligatures: formal (tone < 0.3) uses standard, casual uses all
        - Number form: display/heading uses lining, body uses oldstyle for casual brands
        - Number spacing: table roles use tabular, others proportional
        - Contextual alternates: always on unless explicitly disabled
        """
        tone = self.brand.tone

        # Ligatures: more features for casual brands
        if tone < 0.3:
            ligatures = "standard"  # Formal: just fi, fl
        elif tone < 0.7:
            ligatures = "standardContextual"  # Balanced: standard + contextual
        else:
            ligatures = "all"  # Casual: all available ligatures

        # Number form based on role and brand tone
        if role.startswith("display") or role.startswith("heading"):
            # Headers always use lining figures (align with caps)
            number_form = "lining"
        elif "table" in role or "data" in role:
            # Tables use lining for vertical alignment
            number_form = "lining"
        elif tone > 0.5:
            # Casual body text can use oldstyle (more elegant)
            number_form = "oldStyle"
        else:
            # Formal body text uses lining or default
            number_form = "default"

        # Number spacing based on role
        if "table" in role or "data" in role or "code" in role:
            # Tabular for data alignment
            number_spacing = "tabular"
        else:
            # Proportional for running text
            number_spacing = "proportional"

        # Contextual alternates: generally beneficial
        contextual_alternates = True

        return OpenTypeSettings(
            ligatures=ligatures,
            number_form=number_form,
            number_spacing=number_spacing,
            contextual_alternates=contextual_alternates,
            stylistic_sets=(),
        )

    def _calculate_tracking_settings(
        self,
        role: str,
        font_size_pt: float,
        baseline: Baseline,
    ) -> TrackingSettings:
        """Calculate tracking (letter-spacing) based on role and size.

        Derivation rules:
        - Large text (display, h1-h2): tighter tracking (-20 to -40 twips)
        - Body text: neutral (0)
        - Small text (caption, footnote): slightly looser (+10 to +20 twips)
        - All-caps: always looser (+60 to +100 twips)
        - Small-caps: moderately looser (+30 to +50 twips)

        Brand tone affects the base: casual = looser, formal = tighter
        """
        tone = self.brand.tone

        # Base tracking adjustment from brand tone
        # Formal (0) = tighter, casual (1) = looser
        tone_adjustment = int((tone - 0.5) * 20)  # -10 to +10 twips

        # Size-based tracking (larger = tighter)
        if font_size_pt >= baseline.grid_pt * 2.5:
            # Very large (display): tight
            size_tracking = -30
        elif font_size_pt >= baseline.grid_pt * 1.75:
            # Large (h1-h2): moderately tight
            size_tracking = -20
        elif font_size_pt >= baseline.grid_pt * 1.25:
            # Medium-large (h3-h4): slightly tight
            size_tracking = -10
        elif font_size_pt <= baseline.grid_pt * 0.85:
            # Small (caption, footnote): looser for readability
            size_tracking = 15
        else:
            # Normal body size: neutral
            size_tracking = 0

        # Role-based adjustments
        if role.startswith("display"):
            role_tracking = -10  # Extra tight for display
        elif "caption" in role or "footnote" in role:
            role_tracking = 10  # Extra loose for small text
        elif "ui" in role or "label" in role:
            role_tracking = 5  # Slightly loose for UI elements
        else:
            role_tracking = 0

        # Combine all factors
        base_tracking = size_tracking + role_tracking + tone_adjustment

        # Caps tracking (always positive/looser)
        # More formal brands use less caps tracking
        caps_base = 80 - int(tone * 20)  # 60-80 twips
        small_caps_base = 40 - int(tone * 10)  # 30-40 twips

        return TrackingSettings(
            tracking_twips=base_tracking,
            caps_tracking_twips=caps_base,
            small_caps_tracking_twips=small_caps_base,
        )

    def _calculate_spacing(
        self, role: str, baseline: Baseline, font_size_pt: float
    ) -> Tuple[float, float]:
        """Calculate space before/after for a role."""
        # Display roles get more space
        if role.startswith("display"):
            space_before = baseline.grid_pt * 2
            space_after = baseline.grid_pt
        # Body text gets standard spacing
        elif role.startswith("text.body"):
            space_before = 0
            space_after = baseline.grid_pt
        # Other roles
        else:
            space_before = baseline.grid_pt
            space_after = baseline.grid_pt

        return space_before, space_after

    def calculate_columns(
        self,
        page_width_pt: float,
        gutter_pt: float,
        num_columns: int,
        baseline: Baseline,
    ) -> Dict[str, Any]:
        """Calculate column layout based on grid system.

        Args:
            page_width_pt: Total page width in points
            gutter_pt: Gutter width between columns in points
            num_columns: Number of columns (2, 3, 4, 6, 12)
            baseline: Baseline grid for alignment

        Returns:
            Dict with column layout specifications
        """
        # Snap gutter to baseline grid
        gutter_snapped = baseline.snap_pt(gutter_pt)

        # Calculate total gutter space
        total_gutter = gutter_snapped * (num_columns - 1)

        # Available space for columns
        available_width = page_width_pt - total_gutter

        # Calculate column width (ensure it snaps to baseline)
        column_width_raw = available_width / num_columns
        column_width = baseline.snap_pt(column_width_raw)

        # Recalculate total width with snapped columns
        total_column_width = column_width * num_columns
        total_width = total_column_width + total_gutter

        # Adjust if we exceed page width (reduce gutter slightly)
        if total_width > page_width_pt:
            # Reduce gutter to fit
            available_for_gutters = page_width_pt - total_column_width
            gutter_snapped = available_for_gutters / (num_columns - 1)
            gutter_snapped = baseline.snap_pt(gutter_snapped)
            total_width = total_column_width + (gutter_snapped * (num_columns - 1))

        # Convert to EMU for Word
        column_width_emu = int(column_width * EMU_PER_POINT)
        gutter_emu = int(gutter_snapped * EMU_PER_POINT)
        total_width_emu = int(total_width * EMU_PER_POINT)

        return {
            "num_columns": num_columns,
            "column_width_pt": column_width,
            "column_width_emu": column_width_emu,
            "gutter_pt": gutter_snapped,
            "gutter_emu": gutter_emu,
            "total_width_pt": total_width,
            "total_width_emu": total_width_emu,
            "spacing_equal": True,  # All columns equal width
            "baseline_snapped": True,
        }

    def _build_spacing_scale(self, baseline: Baseline) -> SpacingScale:
        """Build spacing scale from baseline grid."""
        grid = baseline.grid_pt

        scale = {
            # Micro spacing
            "micro": grid * 0.25,
            "tiny": grid * 0.5,
            # Standard spacing
            "small": grid,
            "medium": grid * 2,
            "large": grid * 4,
            "xlarge": grid * 8,
            # Semantic spacing
            "paragraph": grid,
            "section": grid * 2,
            "page": grid * 4,
            # List indents
            "indent": grid * 2,
            "hanging": grid,
        }

        return SpacingScale(baseline_pt=grid, scale=scale)

    def _update_page_geometry(self, baseline: Baseline) -> Optional[PageGeometry]:
        """Update page geometry with resolved baseline grid.

        If page geometry was provided but without a baseline grid,
        create a new one with the calculated baseline grid.
        """
        if self.page_geometry is None:
            return None

        # If page geometry already has a baseline grid, use it
        if self.page_geometry.baseline_grid_emu > 0:
            return self.page_geometry

        # Create updated page geometry with the calculated baseline
        from tokenmoulds.design.page_geometry import (
            PageGeometry as PG,
        )

        return PG(
            width_emu=self.page_geometry.width_emu,
            height_emu=self.page_geometry.height_emu,
            margins=self.page_geometry.margins,
            content=self.page_geometry.content,
            baseline_grid_emu=baseline.grid_emu,
            column_grid_emu=self.page_geometry.column_grid_emu,
        )
