from __future__ import annotations

from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
from pptx.enum.text import MSO_AUTO_SIZE, MSO_VERTICAL_ANCHOR, PP_ALIGN
from pptx.oxml.ns import qn
from pptx.oxml.xmlchemy import OxmlElement
from pptx.util import Inches, Pt

from .deck_models import (
    FOUR_CARD_TEXT_TITLE_MAX_CHARS,
    TABLE_CARD_MAX_COLUMNS,
    TABLE_CARD_MAX_ROWS,
    DesignSpec,
    TableSpec,
)
from .rendering_styles import (
    ResolvedSpacing,
    ResolvedTheme,
    ResolvedTypeScale,
    TextBoxStyle,
    TextInsets,
    _normalize_hex,
    _resolve_panel_chrome,
    _resolve_text_insets,
    _split_text_lines,
)

EMU_PER_INCH = 914400


def _soften_round_rect(shape, adjustment: float = 0.06) -> None:
    try:
        shape.adjustments[0] = adjustment
    except (AttributeError, IndexError, TypeError, ValueError):
        return


def _hex_to_rgb(color: str):
    return RGBColor.from_string(_normalize_hex(color, "#000000").lstrip("#"))


def _set_fill(shape, color: str, transparency: float = 0.0) -> None:
    shape.fill.solid()
    shape.fill.fore_color.rgb = _hex_to_rgb(color)
    try:
        shape.fill.transparency = max(0.0, min(1.0, transparency))
    except (AttributeError, TypeError, ValueError):
        pass


def _set_line(shape, color: str | None, width: float | None = None, transparency: float = 0.0) -> None:
    if not color:
        shape.line.fill.background()
        return
    shape.line.color.rgb = _hex_to_rgb(color)
    try:
        shape.line.transparency = max(0.0, min(1.0, transparency))
    except (AttributeError, TypeError, ValueError):
        pass
    if width is not None:
        shape.line.width = Pt(width)


def _points_to_emu(value: int | float) -> int:
    return int(round(float(value) * 12700))


def _inches_to_emu(value: float) -> int:
    return int(round(value * EMU_PER_INCH))


def _remove_xml_children(parent, tag_name: str) -> None:
    target_tag = qn(tag_name)
    for child in list(parent):
        if child.tag == target_tag:
            parent.remove(child)


def _apply_outer_shadow(shape, color: str, *, blur_pt: int, distance_pt: int, alpha: int) -> None:
    sp_pr = shape.element.spPr
    _remove_xml_children(sp_pr, "a:effectLst")

    effect_list = OxmlElement("a:effectLst")
    outer_shadow = OxmlElement("a:outerShdw")
    outer_shadow.set("blurRad", str(_points_to_emu(blur_pt)))
    outer_shadow.set("dist", str(_points_to_emu(distance_pt)))
    outer_shadow.set("dir", "5400000")
    outer_shadow.set("algn", "ctr")
    outer_shadow.set("rotWithShape", "0")

    shadow_color = OxmlElement("a:srgbClr")
    shadow_color.set("val", _normalize_hex(color, "#000000").lstrip("#"))
    shadow_alpha = OxmlElement("a:alpha")
    shadow_alpha.set("val", str(max(0, min(100000, alpha))))
    shadow_color.append(shadow_alpha)
    outer_shadow.append(shadow_color)
    effect_list.append(outer_shadow)
    sp_pr.append(effect_list)


def _set_paragraph_bullet(paragraph, *, enabled: bool) -> None:
    p_pr = paragraph._p.get_or_add_pPr()
    for tag_name in ("a:buNone", "a:buChar", "a:buAutoNum"):
        _remove_xml_children(p_pr, tag_name)

    if not enabled:
        p_pr.append(OxmlElement("a:buNone"))
        p_pr.attrib.pop("marL", None)
        p_pr.attrib.pop("indent", None)
        return

    bullet = OxmlElement("a:buChar")
    bullet.set("char", "•")
    p_pr.append(bullet)
    p_pr.set("marL", str(_inches_to_emu(0.24)))
    p_pr.set("indent", str(_inches_to_emu(-0.14)))


def _slide_dimensions(prs) -> tuple[float, float]:
    return prs.slide_width / EMU_PER_INCH, prs.slide_height / EMU_PER_INCH


def _add_background_orb(slide, *, left: float, top: float, size: float, color: str, transparency: float) -> None:
    orb = slide.shapes.add_shape(
        MSO_AUTO_SHAPE_TYPE.OVAL,
        Inches(left),
        Inches(top),
        Inches(size),
        Inches(size),
    )
    _set_fill(orb, color, transparency)
    _set_line(orb, None)


def _apply_background(prs, slide, theme: ResolvedTheme, design: DesignSpec):
    slide.background.fill.solid()
    slide.background.fill.fore_color.rgb = _hex_to_rgb(theme.background_color)


def _set_text_style(
    paragraph, theme: ResolvedTheme, font_size: int, *, color: str | None = None, bold: bool = False, align=None
):
    paragraph.alignment = align if align is not None else PP_ALIGN.LEFT
    paragraph.font.name = theme.font_family
    paragraph.font.size = Pt(font_size)
    paragraph.font.bold = bold
    paragraph.font.color.rgb = _hex_to_rgb(color or theme.text_color)


def _normalize_text_line(line: str) -> tuple[str, bool]:
    stripped = line.strip()
    is_bullet = stripped.startswith(("-", "*", "•"))
    normalized = stripped.lstrip("-*• ").strip()
    return normalized, is_bullet


def _fill_text_frame(
    text_frame,
    text: str,
    theme: ResolvedTheme,
    font_size: int,
    *,
    color: str | None = None,
    bold: bool = False,
    align=None,
    paragraph_space_after: int = 6,
) -> None:
    lines = _split_text_lines(text)
    text_frame.clear()

    for index, raw_line in enumerate(lines):
        paragraph = text_frame.paragraphs[0] if index == 0 else text_frame.add_paragraph()
        line, is_bullet = _normalize_text_line(raw_line)
        paragraph.text = line
        _set_text_style(paragraph, theme, font_size=font_size, color=color, bold=bold, align=align)
        _set_paragraph_bullet(paragraph, enabled=is_bullet)
        paragraph.space_after = Pt(paragraph_space_after if index < len(lines) - 1 else 0)


def _apply_text_frame_insets(text_frame, insets: TextInsets) -> None:
    text_frame.margin_left = Inches(insets.left)
    text_frame.margin_right = Inches(insets.right)
    text_frame.margin_top = Inches(insets.top)
    text_frame.margin_bottom = Inches(insets.bottom)


def _add_textbox(
    slide,
    left: float,
    top: float,
    width: float,
    height: float,
    text: str,
    theme: ResolvedTheme,
    style: TextBoxStyle,
):
    textbox = slide.shapes.add_textbox(Inches(left), Inches(top), Inches(width), Inches(height))
    text_frame = textbox.text_frame
    text_frame.word_wrap = True
    _apply_text_frame_insets(text_frame, style.insets)
    text_frame.vertical_anchor = MSO_VERTICAL_ANCHOR.TOP
    text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE if style.fit_text else MSO_AUTO_SIZE.NONE
    _fill_text_frame(
        text_frame,
        text,
        theme,
        style.font_size,
        color=style.color,
        bold=style.bold,
        align=style.align,
        paragraph_space_after=style.paragraph_space_after,
    )
    return textbox


def _set_table_cell_fill(cell, color: str) -> None:
    cell.fill.solid()
    cell.fill.fore_color.rgb = _hex_to_rgb(color)


def _set_table_cell_border(cell, color: str, width_pt: float = 0.75) -> None:
    tc_pr = cell._tc.get_or_add_tcPr()
    for tag_name in ("a:lnL", "a:lnR", "a:lnT", "a:lnB"):
        _remove_xml_children(tc_pr, tag_name)

    for tag_name in ("a:lnL", "a:lnR", "a:lnT", "a:lnB"):
        line = OxmlElement(tag_name)
        line.set("w", str(_points_to_emu(width_pt)))
        solid_fill = OxmlElement("a:solidFill")
        srgb = OxmlElement("a:srgbClr")
        srgb.set("val", _normalize_hex(color, "#000000").lstrip("#"))
        solid_fill.append(srgb)
        line.append(solid_fill)
        tc_pr.append(line)


def _set_table_cell_text(
    cell,
    text: str,
    theme: ResolvedTheme,
    font_size: int,
    *,
    color: str,
    bold: bool = False,
    align=None,
) -> None:
    text_frame = cell.text_frame
    text_frame.word_wrap = True
    text_frame.vertical_anchor = MSO_VERTICAL_ANCHOR.MIDDLE
    text_frame.margin_left = Inches(0.05)
    text_frame.margin_right = Inches(0.05)
    text_frame.margin_top = Inches(0.03)
    text_frame.margin_bottom = Inches(0.03)
    _fill_text_frame(
        text_frame,
        text or "",
        theme,
        font_size,
        color=color,
        bold=bold,
        align=align,
        paragraph_space_after=2,
    )


def _compute_table_column_width_weights(table: TableSpec) -> list[int]:
    weights: list[int] = []
    for index, header in enumerate(table.columns):
        max_length = len(header.strip())
        for row in table.rows:
            max_length = max(max_length, len((row[index] or "").strip()))
        weights.append(max(1, min(max_length, 24)))
    return weights


def _is_card_table_renderable(table: TableSpec) -> bool:
    return len(table.columns) <= TABLE_CARD_MAX_COLUMNS and len(table.rows) <= TABLE_CARD_MAX_ROWS


def _add_table_card(
    slide,
    left: float,
    top: float,
    width: float,
    height: float,
    table: TableSpec,
    theme: ResolvedTheme,
    type_scale: ResolvedTypeScale,
):
    header_fill = theme.accent_soft_color
    border_color = theme.border_color
    row_fill_a = theme.surface_color
    row_fill_b = theme.surface_alt_color
    row_count = len(table.rows) + 1
    col_count = len(table.columns)
    graphic_frame = slide.shapes.add_table(
        row_count, col_count, Inches(left), Inches(top), Inches(width), Inches(height)
    )
    ppt_table = graphic_frame.table

    weights = _compute_table_column_width_weights(table)
    total_weight = sum(weights)
    for index, weight in enumerate(weights):
        ppt_table.columns[index].width = Inches(width * weight / total_weight)

    header_height = min(0.46, max(0.34, height * 0.16))
    body_row_height = max(0.26, (height - header_height) / max(1, len(table.rows)))
    ppt_table.rows[0].height = Inches(header_height)
    for row_index in range(1, row_count):
        ppt_table.rows[row_index].height = Inches(body_row_height)

    for col_index, header in enumerate(table.columns):
        cell = ppt_table.cell(0, col_index)
        _set_table_cell_fill(cell, header_fill)
        _set_table_cell_border(cell, border_color)
        _set_table_cell_text(
            cell,
            header,
            theme,
            max(10, type_scale.detail),
            color=theme.accent_strong_color,
            bold=True,
            align=PP_ALIGN.CENTER,
        )

    for row_index, row in enumerate(table.rows, start=1):
        fill_color = row_fill_a if row_index % 2 else row_fill_b
        for col_index, value in enumerate(row):
            cell = ppt_table.cell(row_index, col_index)
            _set_table_cell_fill(cell, fill_color)
            _set_table_cell_border(cell, border_color)
            _set_table_cell_text(
                cell,
                value,
                theme,
                max(9, type_scale.detail - 1),
                color=theme.text_color,
            )


def _add_table_block(
    slide,
    left: float,
    top: float,
    width: float,
    height: float,
    title: str,
    body: str | None,
    table: TableSpec,
    theme: ResolvedTheme,
    spacing: ResolvedSpacing,
    type_scale: ResolvedTypeScale,
):
    text_insets = _resolve_text_insets(spacing)
    title_height = 0.62 if len(title.strip()) <= FOUR_CARD_TEXT_TITLE_MAX_CHARS else 0.78
    _add_textbox(
        slide,
        left,
        top,
        width,
        title_height,
        title,
        theme,
        TextBoxStyle(
            type_scale.card_title,
            bold=True,
            insets=text_insets.card_title,
            paragraph_space_after=4,
        ),
    )

    content_top = top + title_height + 0.08
    remaining_height = max(1.2, height - title_height - 0.08)
    if body and body.strip():
        body_height = min(0.72, max(0.34, 0.18 * len(_split_text_lines(body)) + 0.16))
        _add_textbox(
            slide,
            left,
            content_top,
            width,
            body_height,
            body,
            theme,
            TextBoxStyle(
                type_scale.detail,
                color=theme.muted_text_color,
                insets=text_insets.card_body,
                paragraph_space_after=4,
            ),
        )
        content_top += body_height + 0.06
        remaining_height = max(1.0, remaining_height - body_height - 0.06)

    if not _is_card_table_renderable(table):
        return

    _add_table_card(
        slide,
        left + 0.02,
        content_top,
        max(0.6, width - 0.04),
        remaining_height,
        table,
        theme,
        type_scale,
    )


def _add_panel(
    slide,
    left: float,
    top: float,
    width: float,
    height: float,
    theme: ResolvedTheme,
    design: DesignSpec,
    spacing: ResolvedSpacing,
):
    shape_type = MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE
    chrome = _resolve_panel_chrome(design)
    panel = slide.shapes.add_shape(shape_type, Inches(left), Inches(top), Inches(width), Inches(height))
    _soften_round_rect(panel)
    panel_color = theme.surface_alt_color if design.variant in {"accent_left", "cards"} else theme.surface_color
    _set_fill(panel, panel_color)
    _set_line(panel, theme.border_color, width=chrome.border_width)
    _apply_outer_shadow(
        panel,
        theme.text_color,
        blur_pt=chrome.shadow_blur_pt,
        distance_pt=chrome.shadow_distance_pt,
        alpha=chrome.shadow_alpha,
    )

    if design.variant == "accent_left":
        rail = slide.shapes.add_shape(
            MSO_AUTO_SHAPE_TYPE.RECTANGLE,
            Inches(left),
            Inches(top),
            Inches(spacing.accent_width),
            Inches(height),
        )
        _set_fill(rail, theme.accent_color)
        _set_line(rail, None)

    return panel


def _panel_content_left(left: float, design: DesignSpec, spacing: ResolvedSpacing) -> float:
    return left + (spacing.accent_width if design.variant == "accent_left" else 0.0)


def _center_block_vertically(
    *, content_top: float, block_height: float, slide_height: float, bottom_margin: float
) -> float:
    available_height = max(block_height, slide_height - content_top - bottom_margin)
    centered_top = content_top + max(0.0, (available_height - block_height) / 2)
    return min(centered_top, slide_height - bottom_margin - block_height)


def _add_card(
    slide,
    left: float,
    top: float,
    width: float,
    height: float,
    title: str,
    body: str | None,
    table: TableSpec | None,
    theme: ResolvedTheme,
    design: DesignSpec,
    spacing: ResolvedSpacing,
    type_scale: ResolvedTypeScale,
    *,
    fit_title_text: bool = False,
):
    text_insets = _resolve_text_insets(spacing)
    _add_panel(slide, left, top, width, height, theme, design, spacing)
    content_left = _panel_content_left(left, design, spacing)
    content_width = width - (content_left - left)
    title_top = top + 0.28
    title_height = 0.92 if design.tone == "bold" else 0.84
    body_text = body or ""
    _add_textbox(
        slide,
        content_left,
        title_top,
        content_width,
        title_height,
        title,
        theme,
        TextBoxStyle(
            type_scale.card_title,
            bold=True,
            fit_text=fit_title_text,
            insets=text_insets.card_title,
            paragraph_space_after=4,
        ),
    )
    content_top = title_top + title_height - 0.02
    remaining_height = max(0.72, height - title_height - 0.28)

    if table is None:
        _add_textbox(
            slide,
            content_left,
            content_top,
            content_width,
            max(1.0, remaining_height),
            body_text,
            theme,
            TextBoxStyle(
                type_scale.body,
                color=theme.muted_text_color,
                insets=text_insets.card_body,
            ),
        )
        return

    body_height = 0.0
    if body_text.strip():
        body_lines = min(2, len(_split_text_lines(body_text)))
        body_height = min(0.92, 0.3 + 0.22 * body_lines)
        _add_textbox(
            slide,
            content_left,
            content_top,
            content_width,
            body_height,
            body_text,
            theme,
            TextBoxStyle(
                type_scale.detail,
                color=theme.muted_text_color,
                insets=text_insets.card_body,
                paragraph_space_after=4,
            ),
        )
        content_top += body_height + 0.06
        remaining_height = max(0.72, remaining_height - body_height - 0.06)

    if _is_card_table_renderable(table):
        _add_table_card(
            slide,
            content_left + 0.04,
            content_top,
            max(0.4, content_width - 0.08),
            remaining_height,
            table,
            theme,
            type_scale,
        )


def _render_image_placeholder(
    slide,
    left: float,
    top: float,
    width: float,
    height: float,
    label: str,
    theme: ResolvedTheme,
    design: DesignSpec,
    spacing: ResolvedSpacing,
    type_scale: ResolvedTypeScale,
):
    _add_panel(slide, left, top, width, height, theme, design, spacing)

    frame = slide.shapes.add_shape(
        MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE,
        Inches(left + 0.35),
        Inches(top + 0.35),
        Inches(width - 0.7),
        Inches(height - 0.7),
    )
    _soften_round_rect(frame, 0.06)
    _set_fill(frame, theme.background_accent_color, 0.0)
    _set_line(frame, theme.accent_soft_color, width=0.01)

    diagonal = slide.shapes.add_shape(
        MSO_AUTO_SHAPE_TYPE.PARALLELOGRAM,
        Inches(left + 0.62),
        Inches(top + 0.78),
        Inches(width - 1.6),
        Inches(0.9),
    )
    _set_fill(diagonal, theme.accent_soft_color, 0.02)
    _set_line(diagonal, None)

    orb = slide.shapes.add_shape(
        MSO_AUTO_SHAPE_TYPE.OVAL,
        Inches(left + width - 1.65),
        Inches(top + 0.48),
        Inches(0.92),
        Inches(0.92),
    )
    _set_fill(orb, theme.accent_soft_color, 0.04)
    _set_line(orb, None)

    vertical_block = slide.shapes.add_shape(
        MSO_AUTO_SHAPE_TYPE.RECTANGLE,
        Inches(left + 0.72),
        Inches(top + 1.55),
        Inches(0.26),
        Inches(height - 2.45),
    )
    _set_fill(vertical_block, theme.accent_color, 0.1)
    _set_line(vertical_block, None)

    inner_card = slide.shapes.add_shape(
        MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE,
        Inches(left + 1.18),
        Inches(top + 1.62),
        Inches(width - 2.0),
        Inches(height - 2.5),
    )
    _soften_round_rect(inner_card, 0.06)
    _set_fill(inner_card, theme.surface_color)
    _set_line(inner_card, theme.border_color, width=0.01)

    band = slide.shapes.add_shape(
        MSO_AUTO_SHAPE_TYPE.RECTANGLE,
        Inches(left + 1.42),
        Inches(top + height - 1.18),
        Inches(width - 2.34),
        Inches(0.14),
    )
    _set_fill(band, theme.accent_color, 0.0)
    _set_line(band, None)

    _add_textbox(
        slide,
        left + 1.3,
        top + height - 0.7,
        width - 2.1,
        0.34,
        label,
        theme,
        TextBoxStyle(type_scale.caption, color=theme.muted_text_color, align=PP_ALIGN.CENTER, fit_text=False),
    )
