from __future__ import annotations

import re
from dataclasses import dataclass, field, replace

from .deck_models import (
    FOUR_CARD_TEXT_BODY_MAX_BEATS,
    FOUR_CARD_TEXT_BODY_MAX_CHARS,
    FOUR_CARD_TEXT_TITLE_MAX_CHARS,
    KPI_CARD_DETAIL_MAX_BEATS,
    KPI_CARD_DETAIL_MAX_CHARS,
    KPI_CARD_LABEL_MAX_CHARS,
    KPI_CARD_LABEL_MAX_WORDS,
    KPI_CARD_VALUE_MAX_CHARS,
    KPI_CARD_VALUE_MAX_WORDS,
    ColumnContent,
    DeckSpec,
    DesignSpec,
    KpiMetric,
)
from .theme_tokens import DEFAULT_THEME_PALETTE, ThemePalette


@dataclass(frozen=True)
class ResolvedTheme:
    font_family: str
    background_color: str
    background_accent_color: str
    text_color: str
    muted_text_color: str
    surface_color: str
    surface_alt_color: str
    border_color: str
    accent_color: str
    accent_soft_color: str
    accent_strong_color: str


@dataclass(frozen=True)
class ResolvedSpacing:
    outer_x: float
    outer_y: float
    content_gap: float
    panel_gap: float
    panel_padding: float
    accent_width: float


@dataclass(frozen=True)
class ResolvedTypeScale:
    hero: int
    title: int
    section: int
    subtitle: int
    card_title: int
    body: int
    detail: int
    caption: int
    quote: int
    kpi_value: int


@dataclass(frozen=True)
class TextInsets:
    left: float
    right: float
    top: float
    bottom: float


@dataclass(frozen=True)
class ResolvedTextInsets:
    default: TextInsets
    card_title: TextInsets
    card_body: TextInsets
    side_panel: TextInsets
    timeline_card: TextInsets
    section_summary: TextInsets


@dataclass(frozen=True)
class TextBoxStyle:
    font_size: int
    color: str | None = None
    bold: bool = False
    align: object | None = None
    fit_text: bool = True
    insets: TextInsets = field(default_factory=lambda: TextInsets(0.0, 0.0, 0.0, 0.0))
    paragraph_space_after: int = 6


@dataclass(frozen=True)
class ResolvedPanelChrome:
    border_width: float
    shadow_blur_pt: int
    shadow_distance_pt: int
    shadow_alpha: int


def _normalize_hex(color: str | None, fallback: str) -> str:
    if not color:
        return fallback
    normalized = color.strip().lstrip("#")
    if len(normalized) != 6:
        return fallback
    return f"#{normalized.upper()}"


def _mix_hex(color_a: str, color_b: str, ratio: float) -> str:
    ratio = max(0.0, min(1.0, ratio))

    def _split(color: str) -> tuple[int, int, int]:
        normalized = _normalize_hex(color, "#000000").lstrip("#")
        return int(normalized[0:2], 16), int(normalized[2:4], 16), int(normalized[4:6], 16)

    a_r, a_g, a_b = _split(color_a)
    b_r, b_g, b_b = _split(color_b)
    mixed = (
        round(a_r * (1 - ratio) + b_r * ratio),
        round(a_g * (1 - ratio) + b_g * ratio),
        round(a_b * (1 - ratio) + b_b * ratio),
    )
    return "#{:02X}{:02X}{:02X}".format(*mixed)


def _resolve_font_family(font_family: str | None) -> str:
    normalized = (font_family or "").strip()
    if not normalized:
        return "Arial"
    if normalized.lower() in {"aptos", "aptos display", "calibri light", "inter"}:
        return "Arial"
    return normalized


def _palette_for_spec(spec: DeckSpec) -> ThemePalette:
    base = DEFAULT_THEME_PALETTE
    primary = _normalize_hex(spec.deck.theme.primary_color, base.accent_primary)
    background = _normalize_hex(spec.deck.theme.background_color, base.background_default)
    return replace(
        base,
        accent_primary=primary,
        background_default=background,
        background_emphasis=primary,
        surface_default=background,
    )


def _resolve_theme(spec: DeckSpec, design: DesignSpec) -> ResolvedTheme:
    # Start from semantic defaults, but honor deck-level customer theme overrides.
    palette = _palette_for_spec(spec)
    primary = palette.accent_primary

    if design.background == "dark":
        background_color = palette.background_emphasis
        background_accent = palette.accent_support
        text_color = palette.text_inverse
        muted_text_color = palette.accent_soft
        surface_color = palette.surface_emphasis
        surface_alt_color = palette.surface_emphasis_alt
        border_color = palette.border_emphasis
        accent_color = primary
        accent_soft = palette.accent_soft
        accent_strong = palette.text_inverse
    elif design.background == "gradient":
        background_color = palette.background_default
        background_accent = palette.surface_subtle
        text_color = palette.text_primary
        muted_text_color = palette.text_secondary
        surface_color = palette.surface_default
        surface_alt_color = palette.surface_subtle
        border_color = palette.border_default
        accent_color = primary
        accent_soft = palette.accent_soft
        accent_strong = palette.accent_primary
    else:
        background_color = palette.background_default
        background_accent = palette.surface_subtle
        text_color = palette.text_primary
        muted_text_color = palette.text_secondary
        surface_color = palette.surface_default
        surface_alt_color = palette.surface_subtle
        border_color = palette.border_default
        accent_color = primary
        accent_soft = palette.accent_soft
        accent_strong = palette.accent_primary

    if design.variant == "plain":
        surface_color = _mix_hex(surface_color, background_color, 0.2)
        border_color = _mix_hex(border_color, background_color, 0.45)
    elif design.variant == "bordered":
        border_color = _mix_hex(accent_color, border_color, 0.28)
    elif design.variant == "accent_left":
        surface_alt_color = _mix_hex(surface_color, accent_soft, 0.18)

    if design.tone == "minimal":
        accent_soft = _mix_hex(accent_soft, background_color, 0.4)
        border_color = _mix_hex(border_color, background_color, 0.25)
    elif design.tone == "bold":
        accent_strong = _mix_hex(accent_color, text_color, 0.08)
        surface_alt_color = _mix_hex(surface_alt_color, accent_soft, 0.12)

    return ResolvedTheme(
        font_family=_resolve_font_family(spec.deck.theme.font_family),
        background_color=background_color,
        background_accent_color=background_accent,
        text_color=text_color,
        muted_text_color=muted_text_color,
        surface_color=surface_color,
        surface_alt_color=surface_alt_color,
        border_color=border_color,
        accent_color=accent_color,
        accent_soft_color=accent_soft,
        accent_strong_color=accent_strong,
    )


def _resolve_spacing(design: DesignSpec) -> ResolvedSpacing:
    if design.density == "compact":
        return ResolvedSpacing(
            outer_x=0.58, outer_y=0.46, content_gap=0.18, panel_gap=0.24, panel_padding=0.22, accent_width=0.16
        )
    if design.density == "spacious":
        return ResolvedSpacing(
            outer_x=0.82, outer_y=0.58, content_gap=0.3, panel_gap=0.38, panel_padding=0.34, accent_width=0.22
        )
    return ResolvedSpacing(
        outer_x=0.7, outer_y=0.5, content_gap=0.24, panel_gap=0.3, panel_padding=0.28, accent_width=0.18
    )


def _resolve_text_insets(spacing: ResolvedSpacing) -> ResolvedTextInsets:
    base_horizontal = max(0.1, spacing.panel_padding * 0.42)
    base_vertical = max(0.06, spacing.panel_padding * 0.28)
    return ResolvedTextInsets(
        default=TextInsets(left=base_horizontal, right=base_horizontal, top=base_vertical, bottom=base_vertical),
        card_title=TextInsets(left=base_horizontal, right=base_horizontal, top=base_vertical * 0.7, bottom=0.02),
        card_body=TextInsets(left=base_horizontal, right=base_horizontal, top=0.02, bottom=base_vertical + 0.03),
        side_panel=TextInsets(
            left=base_horizontal + 0.03,
            right=base_horizontal + 0.03,
            top=base_vertical + 0.01,
            bottom=base_vertical + 0.02,
        ),
        timeline_card=TextInsets(left=base_horizontal, right=base_horizontal, top=base_vertical, bottom=base_vertical),
        section_summary=TextInsets(
            left=base_horizontal + 0.04,
            right=base_horizontal + 0.04,
            top=base_vertical + 0.02,
            bottom=base_vertical + 0.04,
        ),
    )


def _resolve_panel_chrome(design: DesignSpec) -> ResolvedPanelChrome:
    if design.tone == "bold":
        return ResolvedPanelChrome(
            border_width=0.016,
            shadow_blur_pt=5,
            shadow_distance_pt=2,
            shadow_alpha=18000,
        )
    if design.tone == "minimal":
        return ResolvedPanelChrome(
            border_width=0.012,
            shadow_blur_pt=4,
            shadow_distance_pt=1.5,
            shadow_alpha=12000,
        )
    return ResolvedPanelChrome(
        border_width=0.014,
        shadow_blur_pt=5,
        shadow_distance_pt=2,
        shadow_alpha=15000,
    )


def _resolve_type_scale(design: DesignSpec) -> ResolvedTypeScale:
    if design.tone == "minimal":
        return ResolvedTypeScale(
            hero=30,
            title=24,
            section=32,
            subtitle=15,
            card_title=18,
            body=13,
            detail=10,
            caption=10,
            quote=24,
            kpi_value=26,
        )
    if design.tone == "bold":
        return ResolvedTypeScale(
            hero=38,
            title=28,
            section=40,
            subtitle=17,
            card_title=20,
            body=14,
            detail=10,
            caption=10,
            quote=28,
            kpi_value=32,
        )
    return ResolvedTypeScale(
        hero=34,
        title=25,
        section=35,
        subtitle=16,
        card_title=19,
        body=13,
        detail=10,
        caption=10,
        quote=25,
        kpi_value=28,
    )


def _split_text_lines(text: str) -> list[str]:
    lines = [line.strip() for line in text.splitlines() if line.strip()]
    return lines or [text.strip()]


def _count_text_words(text: str) -> int:
    return len([word for word in text.strip().split() if word])


def _count_content_beats(text: str | None) -> int:
    if not text:
        return 0
    lines = [line.strip(" -\t").strip() for line in text.splitlines() if line.strip()]
    if len(lines) > 1:
        return len(lines)
    return len([chunk.strip() for chunk in re.split(r"(?<=[.!?;:])\s+", text.strip()) if chunk.strip()])


def _is_compact_kpi_value(value: str) -> bool:
    normalized = value.strip()
    return (
        bool(normalized)
        and len(_split_text_lines(normalized)) == 1
        and len(normalized) <= KPI_CARD_VALUE_MAX_CHARS
        and _count_text_words(normalized) <= KPI_CARD_VALUE_MAX_WORDS
    )


def _is_compact_kpi_label(label: str) -> bool:
    normalized = label.strip()
    return (
        bool(normalized)
        and len(_split_text_lines(normalized)) == 1
        and len(normalized) <= KPI_CARD_LABEL_MAX_CHARS
        and _count_text_words(normalized) <= KPI_CARD_LABEL_MAX_WORDS
    )


def _is_compact_kpi_detail(detail: str | None) -> bool:
    if detail is None or not detail.strip():
        return True
    normalized = detail.strip()
    return (
        len(normalized) <= KPI_CARD_DETAIL_MAX_CHARS and _count_content_beats(normalized) <= KPI_CARD_DETAIL_MAX_BEATS
    )


def _is_compact_kpi_metric(metric: KpiMetric) -> bool:
    return (
        _is_compact_kpi_value(metric.value)
        and _is_compact_kpi_label(metric.label)
        and _is_compact_kpi_detail(metric.detail)
    )


def _is_compact_four_card_column(column: ColumnContent) -> bool:
    if column.table is not None:
        return False
    title = column.title.strip()
    body = (column.body or "").strip()
    return (
        bool(title)
        and len(title) <= FOUR_CARD_TEXT_TITLE_MAX_CHARS
        and len(body) <= FOUR_CARD_TEXT_BODY_MAX_CHARS
        and _count_content_beats(body) <= FOUR_CARD_TEXT_BODY_MAX_BEATS
    )
