"""Builder that translates Formulaic tokens into the Document IR."""

from __future__ import annotations

from typing import Dict, Iterable, List

from tokenmoulds.colors import (
    ColorEngine,
    simulate_colorblind_palette,
    generate_palette,
)
from tokenmoulds.colors.accessibility import (
    contrast_ratio,
    ensure_contrast,
    generate_shades,
    generate_tints,
)
from tokenmoulds.colors.parser import parse_color
from tokenmoulds.colors.qa import evaluate_palette
from tokenmoulds.ir.model import (
    ColorTheme,
    ColorAnalysis,
    DocumentIR,
    FallbackFonts,
    HeadingStyle,
    HyperlinkStyle,
    ListStyle,
    LocaleSettings,
    ParagraphSpacing,
    ParagraphStyle,
    PresentationLayout,
    QuoteStyle,
    SlidePlaceholder,
    TextAlignment,
    TrackingSettings,
    TypographyScheme,
)

try:
    from tokenmoulds.design.layout_defaults import (
        DEFAULT_BASELINE_EMU as _DEFAULT_BASELINE_EMU,
    )
except Exception:  # pragma: no cover
    _DEFAULT_BASELINE_EMU = 152400  # 12pt in EMU

try:
    from tokenmoulds.design.units.conversion import pt_to_emu as _pt_to_emu
except Exception:  # pragma: no cover - fallback when subpackage missing
    from tokenmoulds.design.units import EMU_PER_POINT as _EMU_PER_POINT

    def _pt_to_emu(points: float) -> int:  # type: ignore[misc]
        return int(points * _EMU_PER_POINT)


def _build_presentation_layout(tokens: Dict[str, Dict]) -> PresentationLayout:
    from tokenmoulds.design.layout_defaults import (
        SLIDE_4_3_HEIGHT_EMU,
        SLIDE_4_3_WIDTH_EMU,
    )

    grid = tokens.get("grid", {})
    spacing_tokens = tokens.get("spacing", {})
    spacing_scale = spacing_tokens.get("scale_pt", {})

    slide_width = SLIDE_4_3_WIDTH_EMU  # 10in slide width (4:3)
    slide_height = SLIDE_4_3_HEIGHT_EMU  # 7.5in slide height

    margins = grid.get("margins", {})
    margin_x = int(margins.get("left_emu", slide_width * 0.08))
    margin_y = int(margins.get("top_emu", slide_height * 0.08))

    margin_x = min(max(margin_x, int(slide_width * 0.04)), int(slide_width * 0.3))
    margin_y = min(max(margin_y, int(slide_height * 0.04)), int(slide_height * 0.3))

    usable_width = max(slide_width - (2 * margin_x), slide_width // 2)
    usable_height = max(slide_height - (2 * margin_y), slide_height // 2)

    gap = _pt_to_emu(float(spacing_scale.get("space_2", 8.0)))

    title_height = int(usable_height * 0.25)
    body_height = int(usable_height * 0.45)
    remaining = usable_height - title_height - body_height - (2 * gap)
    min_table_height = _pt_to_emu(72)
    table_height = max(remaining, min_table_height)

    total_needed = title_height + body_height + table_height + (2 * gap)
    if total_needed > usable_height:
        overflow = total_needed - usable_height
        table_height = max(min_table_height, table_height - overflow)

    placeholders: List[SlidePlaceholder] = []
    current_y = margin_y
    placeholders.append(
        SlidePlaceholder(
            kind="title",
            x=margin_x,
            y=current_y,
            cx=usable_width,
            cy=title_height,
            text_style="heading",
            level=0,
        )
    )
    current_y += title_height + gap
    placeholders.append(
        SlidePlaceholder(
            kind="body",
            x=margin_x,
            y=current_y,
            cx=usable_width,
            cy=body_height,
            text_style="body",
            level=1,
        )
    )
    current_y += body_height + gap
    placeholders.append(
        SlidePlaceholder(
            kind="table",
            x=margin_x,
            y=current_y,
            cx=usable_width,
            cy=table_height,
            text_style="table",
            level=0,
        )
    )

    colors = tokens.get("colors", {})
    palette = colors.get("palette", {})
    neutral = palette.get("neutral", {})
    from tokenmoulds.design.color_defaults import SURFACE_WHITE

    background = neutral.get("white", SURFACE_WHITE)

    return PresentationLayout(
        slide_width=slide_width,
        slide_height=slide_height,
        background_color=background,
        placeholders=placeholders,
    )


def _build_locale_settings(tokens: Dict) -> LocaleSettings:
    """Build locale settings from tokens."""
    locale_tokens = tokens.get("locale", {})
    line_breaking = locale_tokens.get("line_breaking", {})

    return LocaleSettings(
        rtl=locale_tokens.get("rtl", False),
        ea_ln_brk=line_breaking.get("ea_ln_brk", True),
        latin_ln_brk=line_breaking.get("latin_ln_brk", False),
        hanging_punct=line_breaking.get("hanging_punct", True),
    )


def _build_text_alignment(tokens: Dict) -> TextAlignment:
    """Build text alignment settings from tokens."""
    typography = tokens.get("typography", {})
    alignment = typography.get("alignment", {})

    return TextAlignment(
        title_horizontal=alignment.get("title", "l"),
        body_horizontal=alignment.get("body", "l"),
        title_vertical=alignment.get("title_vertical", "t"),
        body_vertical=alignment.get("body_vertical", "t"),
    )


def _build_tracking_settings(tokens: Dict) -> TrackingSettings:
    """Build tracking settings from tokens."""
    typography = tokens.get("typography", {})
    tracking = typography.get("tracking", {})

    return TrackingSettings(
        display=tracking.get("display", -20),
        heading=tracking.get("heading", -10),
        body=tracking.get("body", 0),
        small=tracking.get("small", 10),
    )


def _build_fallback_fonts(tokens: Dict, locale: str) -> FallbackFonts:
    """Build fallback font settings from tokens.

    Selects appropriate fonts based on locale.
    """
    typography = tokens.get("typography", {})
    fonts = typography.get("fonts", {})
    fallback = fonts.get("fallback", {})

    # Determine EA font based on locale
    ea_fonts = fallback.get("ea", {})
    locale_lower = locale.lower().replace("-", "_")

    # Try to match locale to an EA font
    ea_font = "Microsoft YaHei"  # Default
    if locale_lower.startswith("zh_hans") or locale_lower.startswith("zh_cn"):
        ea_font = ea_fonts.get("zh_hans", ea_font)
    elif locale_lower.startswith("zh_hant") or locale_lower.startswith("zh_tw"):
        ea_font = ea_fonts.get("zh_hant", "Microsoft JhengHei")
    elif locale_lower.startswith("ja"):
        ea_font = ea_fonts.get("ja", "Meiryo")
    elif locale_lower.startswith("ko"):
        ea_font = ea_fonts.get("ko", "Malgun Gothic")

    # Determine CS font based on locale
    cs_fonts = fallback.get("cs", {})
    cs_font = "Arial"  # Default
    if locale_lower.startswith("ar"):
        cs_font = cs_fonts.get("ar", "Arabic Typesetting")
    elif locale_lower.startswith("he"):
        cs_font = cs_fonts.get("he", "Arial")
    elif locale_lower.startswith("th"):
        cs_font = cs_fonts.get("th", "Tahoma")
    elif locale_lower.startswith("hi"):
        cs_font = cs_fonts.get("hi", "Mangal")

    return FallbackFonts(
        ea_font=ea_font,
        cs_font=cs_font,
    )


def build_document_ir(tokens: Dict, org_id: str, locale: str) -> DocumentIR:
    """Build a DocumentIR from a tokens dictionary."""
    colors = tokens.get("colors", {})
    typography = tokens.get("typography", {})
    styles = tokens.get("styles", {})

    from tokenmoulds.design.color_defaults import (
        BORDER_DEFAULT,
        DEFAULT_PRIMARY,
        DEFAULT_SECONDARY,
        DEFAULT_THEME_MAPPING,
    )

    theme_map = colors.get("theme_mapping", {})
    if not theme_map:
        # Provide sensible defaults if theme_mapping is missing
        theme_map = dict(DEFAULT_THEME_MAPPING)
        theme_map["accent1"] = (
            colors.get("palette", {}).get("primary", {}).get("base", DEFAULT_PRIMARY)
        )
        theme_map["accent2"] = (
            colors.get("palette", {})
            .get("secondary", {})
            .get("base", DEFAULT_SECONDARY)
        )

    color_theme = ColorTheme(
        dk1=theme_map["dk1"],
        lt1=theme_map["lt1"],
        dk2=theme_map["dk2"],
        lt2=theme_map["lt2"],
        accent1=theme_map["accent1"],
        accent2=theme_map["accent2"],
        accent3=theme_map["accent3"],
        accent4=theme_map["accent4"],
        accent5=theme_map["accent5"],
        accent6=theme_map["accent6"],
        hyperlink=theme_map.get("hlink", theme_map["accent1"]),
        hyperlink_followed=theme_map.get("folHlink", theme_map["accent2"]),
    )

    engine = ColorEngine.default()

    fonts = typography.get("fonts", {})
    body_font = fonts.get("minor") or fonts.get("sans", "Inter")
    heading_font = fonts.get("serif") or fonts.get("major") or body_font
    mono_font = fonts.get("mono", "Courier New")

    typography_scheme = TypographyScheme(
        body_font=body_font,
        heading_font=heading_font,
        baseline_emu=typography.get("baseline_emu", _DEFAULT_BASELINE_EMU),
        size_map_pt=typography.get("scale_pt", {}),
        monospace_font=mono_font,
    )

    body_base = styles["body"]["base"]
    body_style = ParagraphStyle(
        style_id="Normal",
        name="Normal",
        font_family=body_base["font_family"],
        font_size_pt=body_base["font_size_pt"],
        color=body_base["color"],
        spacing=ParagraphSpacing(
            before_twips=body_base["paragraph_spacing"]["before_twips"],
            after_twips=body_base["paragraph_spacing"]["after_twips"],
        ),
    )

    heading_base = styles["heading"]
    heading_styles: List[HeadingStyle] = []
    for key, data in heading_base["levels"].items():
        level = int(key.lstrip("h"))
        heading_styles.append(
            HeadingStyle(
                style_id=f"Heading{level}",
                name=f"Heading {level}",
                font_family=heading_base["base"]["font_family"],
                font_size_pt=data["font_size_twips"] / 20,
                color=heading_base["base"]["color"],
                spacing=ParagraphSpacing(
                    before_twips=data["paragraph_spacing"]["before_twips"],
                    after_twips=data["paragraph_spacing"]["after_twips"],
                ),
                bold=True,
                level=level,
                outline_level=data.get("outline_level", level - 1),
            )
        )

    list_base = styles["list"]["base"]
    list_style = ListStyle(
        indent_twips=list_base["indent_twips"],
        hanging_twips=list_base["hanging_twips"],
    )

    quote_base = styles["quote"]["base"]
    quote_spacing = quote_base.get(
        "paragraph_spacing",
        {
            "before_twips": quote_base.get("indent_twips", 0),
            "after_twips": quote_base.get("indent_twips", 0),
        },
    )
    quote_style = QuoteStyle(
        style_id="Quote",
        name="Quote",
        font_family=quote_base["font_family"],
        font_size_pt=quote_base["font_size_pt"],
        color=quote_base["color"],
        spacing=ParagraphSpacing(
            before_twips=quote_spacing["before_twips"],
            after_twips=quote_spacing["after_twips"],
        ),
        italic=quote_base.get("italic", False),
        border_color=quote_base.get("border_color", BORDER_DEFAULT),
        border_width_twips=quote_base.get("border_width_twips", 15),
        indent_twips=quote_base.get("indent_twips", 360),
    )

    hyperlink_tokens = styles.get("hyperlink", {})
    hyperlink_style = HyperlinkStyle(
        link_color=hyperlink_tokens.get("link", {}).get("color", color_theme.hyperlink),
        visited_color=hyperlink_tokens.get("visited", {}).get(
            "color", color_theme.hyperlink_followed
        ),
    )
    hyperlink_style = _adjust_hyperlink_style(hyperlink_style, body_style.color)

    presentation_layout = _build_presentation_layout(tokens)

    palette_stats = engine.summarize_tokens(theme_map.values())
    palette_warnings = evaluate_palette(palette_stats)
    previews = _build_palette_previews(theme_map.values())
    heading_contrast = _build_heading_contrast_preview(body_style.color, heading_styles)
    if heading_contrast:
        previews["heading_contrast"] = heading_contrast
    color_blind = _build_color_blind_previews(theme_map.values())
    for vision_mode, values in color_blind.items():
        previews[f"color_blind_{vision_mode}"] = values
    color_analysis = ColorAnalysis(
        palette_summary=palette_stats,
        warnings=palette_warnings,
        previews=previews,
    )

    # Build extended typography and locale settings
    locale_settings = _build_locale_settings(tokens)
    text_alignment = _build_text_alignment(tokens)
    tracking_settings = _build_tracking_settings(tokens)
    fallback_fonts = _build_fallback_fonts(tokens, locale)

    # Extract brand tone from tokens (default 0.5 if not present)
    brand_tone = float(tokens.get("brand_tone", 0.5))

    # Generate color palette with all tint/shade variants
    # This is used by ODF emitters for pre-computed colors
    color_palette = generate_palette(
        color_theme.to_dict(),
        tint_levels=[20, 40, 60, 80],
        shade_levels=[20, 40],
    )

    return DocumentIR(
        org_id=org_id,
        locale=locale,
        color_theme=color_theme,
        typography=typography_scheme,
        body_style=body_style,
        heading_styles=sorted(heading_styles, key=lambda h: h.level),
        list_style=list_style,
        quote_style=quote_style,
        hyperlink_style=hyperlink_style,
        presentation_layout=presentation_layout,
        color_analysis=color_analysis,
        locale_settings=locale_settings,
        text_alignment=text_alignment,
        tracking=tracking_settings,
        fallback_fonts=fallback_fonts,
        brand_tone=brand_tone,
        color_palette=color_palette,
    )


def _adjust_hyperlink_style(
    style: HyperlinkStyle, body_color_hex: str
) -> HyperlinkStyle:
    body = parse_color(body_color_hex)
    link = parse_color(style.link_color)
    visited = parse_color(style.visited_color)
    if body and link:
        adjusted = ensure_contrast(link, body, target=4.5)
        style = HyperlinkStyle(
            link_color=adjusted.to_hex(), visited_color=style.visited_color
        )
    if body and visited:
        adjusted = ensure_contrast(visited, body, target=3.5)
        style = HyperlinkStyle(
            link_color=style.link_color, visited_color=adjusted.to_hex()
        )
    return style


def _build_palette_previews(values: Iterable[str]) -> dict[str, list[str]]:
    parsed = [parse_color(value) for value in values]
    parsed = [color for color in parsed if color]
    if not parsed:
        return {}
    samples = parsed[:4]
    lighten = [t.to_hex() for col in samples for t in generate_tints(col, steps=1)]
    darken = [s.to_hex() for col in samples for s in generate_shades(col, steps=1)]
    saturate = [col.saturate(0.2).to_hex() for col in samples]
    return {"lighten": lighten, "darken": darken, "saturate": saturate}


def _build_color_blind_previews(values: Iterable[str]) -> dict[str, list[str]]:
    parsed = [parse_color(value) for value in values]
    parsed = [color for color in parsed if color]
    if not parsed:
        return {}
    return simulate_colorblind_palette(parsed[:6])


def _build_heading_contrast_preview(
    body_hex: str, headings: Iterable[HeadingStyle]
) -> list[str]:
    body = parse_color(body_hex)
    if not body:
        return []
    previews: list[str] = []
    for heading in headings:
        heading_color = parse_color(heading.color)
        if not heading_color:
            continue
        ratio = contrast_ratio(heading_color, body)
        previews.append(f"{heading.style_id}:{ratio:.2f}")
    return previews
