"""Paint pass — write IR nodes into the CharGrid as characters.

Depth-first traversal: each node writes its content at its assigned
rect position. Containers recurse into children after drawing their
own structure (borders, etc.).
"""

from __future__ import annotations

import re
import textwrap

from uframe.chars import (
    BOX_CHARS, DIVIDER_CHARS, sparkline_chars,
)
from uframe.grid import CharGrid, CellStyle
from uframe.ir import (
    IRNode, Page, Box, Row, Col, Spacer, Pad,
    Heading, Text, Label, Divider, Link, ListNode, ListItem,
    Gauge, Sparkline, Status, Table,
    Form, Field, Password, Radio, Checkbox, FormButton,
    BigTitle, ImageNode, HNav, VNav,
    HeadingLevel, DividerStyle, ListStyle, Align, BorderWeight,
)
from uframe.themes import ThemeDef, THEME_DEFAULT
from uframe.fonts import render_big_text, get_text_width, FONT_HEIGHTS
from uframe.imaging import convert_image


def _align_text(text: str, width: int, align: Align) -> str:
    """Align text within a field of the given width."""
    if len(text) >= width:
        return text[:width]
    if align == Align.CENTER:
        return text.center(width)
    elif align == Align.RIGHT:
        return text.rjust(width)
    return text.ljust(width)


def _style_from_node(node: IRNode) -> CellStyle:
    """Create a CellStyle from a node's style attributes."""
    return CellStyle(
        fg=node.style.fg,
        bg=node.style.bg,
        bold=node.style.bold,
        italic=node.style.italic,
        underline=node.style.underline,
    )


def paint(node: IRNode, grid: CharGrid, theme: ThemeDef | None = None) -> None:
    """Recursively paint an IR node and its children into the grid."""
    th = theme or THEME_DEFAULT
    x, y, w = node.rect.x, node.rect.y, node.rect.w

    # Ensure grid is tall enough
    grid.grow_height(y + node.rect.h)

    if isinstance(node, Page):
        for child in node.children:
            paint(child, grid, th)

    elif isinstance(node, Box):
        # Draw the border with themed characters
        title_style = CellStyle(bold=True, fg=node.style.fg or th.palette.accent)
        grid.draw_border(x, y, w, node.rect.h,
                         weight=node.weight,
                         title=node.title,
                         title_style=title_style,
                         border_chars=th.border_dict(node.weight.name.lower()),
                         title_caps=(th.title_caps.left, th.title_caps.right))
        # Propagate box alignment/color to children that don't have their own
        for child in node.children:
            if node.style.align != Align.LEFT and child.style.align == Align.LEFT:
                child.style.align = node.style.align
            if node.style.fg and not child.style.fg:
                child.style.fg = node.style.fg
            paint(child, grid, th)

    elif isinstance(node, Row):
        for child in node.children:
            paint(child, grid, th)

    elif isinstance(node, Col):
        for child in node.children:
            paint(child, grid, th)

    elif isinstance(node, Spacer):
        pass  # Just empty space

    elif isinstance(node, Pad):
        for child in node.children:
            paint(child, grid, th)

    elif isinstance(node, Heading):
        style = CellStyle(bold=True)
        if node.level == HeadingLevel.H1:
            style.fg = th.palette.accent
        elif node.level == HeadingLevel.H2:
            style.fg = th.palette.accent2
        elif node.level == HeadingLevel.H3:
            style.fg = th.palette.accent3

        # Underline-style heading
        grid.put_text(x, y, node.text[:w], style=style)

    elif isinstance(node, Text):
        style = _style_from_node(node)

        if node.spans and any(
            s.bold or s.italic or s.underline or s.fg or s.bg
            for s in node.spans
        ):
            # Render with inline spans
            col = x
            row = y
            for span in node.spans:
                span_style = CellStyle(
                    fg=span.fg or style.fg,
                    bg=span.bg or style.bg,
                    bold=span.bold or style.bold,
                    italic=span.italic or style.italic,
                    underline=span.underline or style.underline,
                )
                for ch in span.text:
                    if col >= x + w:
                        col = x
                        row += 1
                    if grid.in_bounds(col, row):
                        grid.put(col, row, ch, style=span_style)
                    col += 1
        else:
            # Simple text with word wrapping
            wrapped = textwrap.wrap(node.content, width=w) if node.content else [""]
            for i, line in enumerate(wrapped):
                if y + i < grid.height:
                    text = _align_text(line, w, node.style.align)
                    grid.put_text(x, y + i, text, style=style)

    elif isinstance(node, Label):
        style = _style_from_node(node)
        key_style = CellStyle(bold=True, fg=style.fg, bg=style.bg)
        # Key: value layout with padding
        key_text = f"{node.key}:"
        pad = max(1, 16 - len(key_text))
        grid.put_text(x, y, key_text, style=key_style)
        grid.put_text(x + len(key_text) + pad, y, node.value, style=style)

    elif isinstance(node, Divider):
        ds = node.divider_style
        char = getattr(th.dividers, ds.name.lower(), th.dividers.light)
        style = CellStyle(fg=th.palette.muted)
        for col in range(x, x + w):
            grid.put(col, y, char, style=style)

    elif isinstance(node, Link):
        style = CellStyle(fg=th.palette.info, underline=True)
        grid.put_text(x, y, node.display[:w], style=style, link=node.dest)

    elif isinstance(node, ListNode):
        for child in node.children:
            paint(child, grid, th)

    elif isinstance(node, ListItem):
        style = _style_from_node(node)
        # Bullet to the left of the content (safe: put_text clips to bounds)
        bullet_x = max(0, x - 2)
        grid.put_text(bullet_x, y, f"{th.ornaments.bullet} ", style=CellStyle(fg=th.palette.label))
        # Wrap content
        wrapped = textwrap.wrap(node.content, width=w) if node.content else [""]
        for i, line in enumerate(wrapped):
            if y + i < grid.height:
                grid.put_text(x, y + i, line, style=style)

    elif isinstance(node, Gauge):
        style = _style_from_node(node)
        # Label
        label_text = f"{node.label}  "
        grid.put_text(x, y, label_text, style=CellStyle(bold=True))

        bar_x = x + len(label_text)
        bar_w = min(node.bar_width, w - len(label_text) - 6)

        if bar_w > 0:
            pct = min(node.value / node.max_val, 1.0) if node.max_val > 0 else 0
            filled = int(bar_w * pct)

            # Determine color based on thresholds
            fg = th.palette.success
            if node.crit is not None and node.value >= node.crit:
                fg = th.palette.danger
            elif node.warn is not None and node.value >= node.warn:
                fg = th.palette.warning

            for i in range(bar_w):
                if i < filled:
                    grid.put(bar_x + i, y, th.gauge.filled, style=CellStyle(fg=fg))
                else:
                    grid.put(bar_x + i, y, th.gauge.empty, style=CellStyle(fg=th.palette.muted))

            # Percentage
            pct_text = f"  {int(pct * 100)}%"
            grid.put_text(bar_x + bar_w, y, pct_text, style=CellStyle(fg=fg))

    elif isinstance(node, Sparkline):
        style = _style_from_node(node)
        label_text = f"{node.label}  "
        grid.put_text(x, y, label_text, style=CellStyle(bold=True))

        spark_x = x + len(label_text)
        chars = sparkline_chars(node.values, node.spark_width)
        spark_style = CellStyle(fg=th.palette.info)
        for i, ch in enumerate(chars):
            grid.put(spark_x + i, y, ch, style=spark_style)

    elif isinstance(node, Status):
        char = getattr(th.indicators, node.state, th.indicators.unknown)
        color_map = {"online": th.palette.success, "offline": th.palette.danger,
                     "degraded": th.palette.warning, "unknown": th.palette.label,
                     "alert": th.palette.danger}
        color = color_map.get(node.state, th.palette.label)
        grid.put(x, y, char, style=CellStyle(fg=color))
        grid.put_text(x + 2, y, node.label)

    elif isinstance(node, HNav):
        _paint_hnav(node, grid, x, y, w, th)

    elif isinstance(node, VNav):
        _paint_vnav(node, grid, x, y, w, th)

    elif isinstance(node, ImageNode):
        style = CellStyle(fg=node.style.fg or th.palette.accent)
        try:
            lines = convert_image(node.path, node.mode, node.img_width,
                                  node.dither, node.invert, node.use_color)
            # Center if aligned
            offset = 0
            actual_w = len(lines[0]) if lines else 0
            if node.style.align == Align.CENTER:
                offset = max(0, (w - actual_w) // 2)
            elif node.style.align == Align.RIGHT:
                offset = max(0, w - actual_w)

            for row_i, line in enumerate(lines):
                if y + row_i < grid.height:
                    grid.put_text(x + offset, y + row_i, line, style=style)

            # Caption
            if node.caption and y + len(lines) < grid.height:
                cap_style = CellStyle(fg=th.palette.label, italic=True)
                grid.put_text(x + offset, y + len(lines), node.caption, style=cap_style)
        except (ImportError, FileNotFoundError) as e:
            # Render placeholder if image can't be loaded
            grid.put_text(x, y, f"[image: {node.path}]", style=CellStyle(fg=th.palette.muted))

    elif isinstance(node, BigTitle):
        style = CellStyle(fg=node.style.fg or th.palette.accent, bold=True)

        # Determine which font fits
        font = node.font
        tw = get_text_width(node.text, font)
        if tw > w:
            # Try fallback cascade
            for fallback in ["pixel", "thin"]:
                if get_text_width(node.text, fallback) <= w:
                    font = fallback
                    tw = get_text_width(node.text, fallback)
                    break
            else:
                # Final fallback: styled single line
                styled = f"═══ {node.text.upper()} ═══"
                grid.put_text(x, y, styled[:w], style=style)
                return

        lines = render_big_text(node.text, font)

        # Center if align is set
        offset = 0
        if node.style.align == Align.CENTER:
            offset = max(0, (w - tw) // 2)
        elif node.style.align == Align.RIGHT:
            offset = max(0, w - tw)

        for row_i, line in enumerate(lines):
            if y + row_i < grid.height:
                grid.put_text(x + offset, y + row_i, line, style=style)

    elif isinstance(node, Table):
        _paint_table(node, grid, x, y, w)

    elif isinstance(node, Form):
        for child in node.children:
            paint(child, grid, th)

    elif isinstance(node, Field):
        label_style = CellStyle(fg=th.palette.label)
        field_style = CellStyle(fg=th.palette.form)
        label_text = f"{node.field_name}: "
        grid.put_text(x, y, label_text, style=label_style)
        fl = th.form.field_l
        fr = th.form.field_r
        fx = x + len(label_text)
        fw = min(node.field_width, w - len(label_text) - len(fl) - len(fr))
        grid.put_text(fx, y, fl, style=field_style)
        placeholder = node.placeholder or node.field_name
        inner = placeholder.ljust(fw)[:fw]
        grid.put_text(fx + len(fl), y, inner, style=CellStyle(fg=th.palette.muted))
        grid.put_text(fx + len(fl) + fw, y, fr, style=field_style)

    elif isinstance(node, Password):
        label_style = CellStyle(fg=th.palette.label)
        field_style = CellStyle(fg=th.palette.form)
        label_text = f"{node.field_name}: "
        grid.put_text(x, y, label_text, style=label_style)
        fl = th.form.field_l
        fr = th.form.field_r
        fx = x + len(label_text)
        fw = min(node.field_width, w - len(label_text) - len(fl) - len(fr))
        grid.put_text(fx, y, fl, style=field_style)
        inner = "•" * fw
        grid.put_text(fx + len(fl), y, inner[:fw], style=CellStyle(fg=th.palette.muted))
        grid.put_text(fx + len(fl) + fw, y, fr, style=field_style)

    elif isinstance(node, Radio):
        label_style = CellStyle(fg=th.palette.label)
        label_text = f"{node.group}: "
        grid.put_text(x, y, label_text, style=label_style)
        rx = x + len(label_text)
        for i, opt in enumerate(node.options):
            dot = th.form.radio_on if i == 0 else th.form.radio_off
            opt_style = CellStyle(fg=th.palette.form if i == 0 else th.palette.label)
            grid.put_text(rx, y, dot, style=opt_style)
            rx += len(dot) + 1
            grid.put_text(rx, y, opt, style=CellStyle())
            rx += len(opt) + 2

    elif isinstance(node, Checkbox):
        check_style = CellStyle(fg=th.palette.form)
        box_char = th.form.check_on if node.checked else th.form.check_off
        grid.put_text(x, y, box_char, style=check_style)
        grid.put_text(x + len(box_char) + 1, y, node.checkbox_label)

    elif isinstance(node, FormButton):
        btn_style = CellStyle(bold=True, fg=th.palette.button)
        btn_text = f"[ {node.button_label} ]"
        grid.put_text(x, y, btn_text, style=btn_style, link=node.dest)

    else:
        # Generic: paint children
        for child in node.children:
            paint(child, grid, th)


def _paint_hnav(node: HNav, grid: CharGrid, x: int, y: int, w: int, th: ThemeDef) -> None:
    """Paint a horizontal navigation bar."""
    active_style = CellStyle(bold=True, fg=th.palette.accent)
    link_style = CellStyle(fg=th.palette.info)
    sep_style = CellStyle(fg=th.palette.muted)
    border_style = CellStyle()

    items = [it for it in node.items if it.kind in ("item", "separator")]
    marker = "▸ "

    if node.nav_style in ("bar", "tabs", "pills"):
        # Bordered bar
        bc = th.border_dict("light")
        grid.draw_border(x, y, w, 3, border_chars=bc,
                         title_caps=(th.title_caps.left, th.title_caps.right))
        col = x + 2
        for it in items:
            if it.kind == "separator":
                grid.put(col, y + 1, "│", style=sep_style)
                col += 2
                continue
            if it.active:
                grid.put_text(col, y + 1, marker, style=active_style)
                col += len(marker)
                grid.put_text(col, y + 1, it.label, style=active_style)
            else:
                grid.put_text(col, y + 1, it.label, style=link_style, link=it.dest)
            col += len(it.label) + 2
            if col < x + w - 2:
                grid.put(col, y + 1, "│", style=sep_style)
                col += 2

    elif node.nav_style == "breadcrumb":
        col = x + 2
        sep = " ▸ "
        for i, it in enumerate(items):
            if it.kind == "separator":
                continue
            if i > 0:
                grid.put_text(col, y, sep, style=sep_style)
                col += len(sep)
            if it.active:
                grid.put_text(col, y, it.label, style=active_style)
            else:
                grid.put_text(col, y, it.label, style=link_style, link=it.dest)
            col += len(it.label)

    elif node.nav_style == "underline":
        col = x + 2
        active_start = 0
        active_len = 0
        for i, it in enumerate(items):
            if it.kind == "separator":
                continue
            if it.active:
                active_start = col
                active_len = len(it.label)
                grid.put_text(col, y, it.label, style=active_style)
            else:
                grid.put_text(col, y, it.label, style=link_style, link=it.dest)
            col += len(it.label) + 5
        # Underline beneath active
        if active_len > 0:
            for c in range(active_start, active_start + active_len):
                grid.put(c, y + 1, "━", style=CellStyle(fg=th.palette.accent))


def _paint_vnav(node: VNav, grid: CharGrid, x: int, y: int, w: int, th: ThemeDef) -> None:
    """Paint a vertical navigation panel."""
    active_style = CellStyle(bold=True, fg=th.palette.accent)
    link_style = CellStyle(fg=th.palette.info)
    heading_style = CellStyle(bold=True, fg=th.palette.muted)
    sep_style = CellStyle(fg=th.palette.muted)
    marker = "▸ "

    items = node.items
    row_y = y

    if node.nav_style == "boxed":
        # Draw a box and render items inside
        bc = th.border_dict("light")
        h = node.rect.h
        grid.draw_border(x, y, w, h, border_chars=bc,
                         title_caps=(th.title_caps.left, th.title_caps.right))
        row_y = y + 1
        for it in items:
            if it.kind == "separator":
                # Draw internal separator
                for c in range(x + 1, x + w - 1):
                    grid.put(c, row_y, bc.get("h", "─"), style=sep_style,
                             is_border=True)
                grid.put(x, row_y, "├", style=sep_style, is_border=True)
                grid.put(x + w - 1, row_y, "┤", style=sep_style, is_border=True)
                row_y += 1
            elif it.kind == "heading":
                grid.put_text(x + 2, row_y, it.label.upper(), style=heading_style)
                row_y += 1
            elif it.kind == "item":
                if it.active:
                    grid.put_text(x + 2, row_y, marker, style=active_style)
                    grid.put_text(x + 2 + len(marker), row_y, it.label, style=active_style)
                else:
                    grid.put_text(x + 4, row_y, it.label, style=link_style, link=it.dest)
                row_y += 1

    elif node.nav_style == "tree":
        for idx, it in enumerate(items):
            if it.kind == "separator":
                for c in range(x, x + w):
                    grid.put(c, row_y, "─", style=sep_style)
                row_y += 1
            elif it.kind == "heading":
                grid.put_text(x, row_y, it.label, style=heading_style)
                row_y += 1
            elif it.kind == "item":
                # Determine connector
                remaining = [i for i in items[idx+1:] if i.kind == "item"]
                connector = "└── " if not remaining else "├── "
                grid.put_text(x, row_y, connector, style=sep_style)
                if it.active:
                    grid.put_text(x + len(connector), row_y, it.label, style=active_style)
                    grid.put_text(x + len(connector) + len(it.label) + 2, row_y, "◀",
                                  style=CellStyle(fg=th.palette.accent))
                else:
                    grid.put_text(x + len(connector), row_y, it.label,
                                  style=link_style, link=it.dest)
                row_y += 1

    else:
        # list, sidebar, minimal
        for it in items:
            if it.kind == "separator":
                for c in range(x, min(x + w, x + 16)):
                    grid.put(c, row_y, "─", style=sep_style)
                row_y += 1
            elif it.kind == "heading":
                grid.put_text(x, row_y, it.label.upper(), style=heading_style)
                row_y += 1
            elif it.kind == "item":
                if it.active:
                    grid.put_text(x, row_y, marker, style=active_style)
                    grid.put_text(x + len(marker), row_y, it.label, style=active_style)
                else:
                    grid.put_text(x + 2, row_y, it.label, style=link_style, link=it.dest)
                row_y += 1


def _paint_table(node: Table, grid: CharGrid, x: int, y: int, w: int) -> None:
    """Paint a box-drawn table with header and data rows."""
    if not node.columns:
        return

    ch = BOX_CHARS[BorderWeight.LIGHT]
    border_style = CellStyle()
    header_style = CellStyle(bold=True)

    num_cols = len(node.columns)

    # Calculate column widths
    # If columns have explicit widths, use them. Otherwise distribute evenly.
    col_widths: list[int] = []
    total_explicit = 0
    auto_count = 0
    for _, cw in node.columns:
        if cw > 0:
            col_widths.append(cw)
            total_explicit += cw
        else:
            col_widths.append(0)
            auto_count += 1

    # Available inner width = total - borders (num_cols + 1 border chars)
    inner_w = w - (num_cols + 1)
    if auto_count > 0:
        auto_each = max(1, (inner_w - total_explicit) // auto_count)
        for i in range(len(col_widths)):
            if col_widths[i] == 0:
                col_widths[i] = auto_each

    # Compute column x positions (after each left border)
    col_x: list[int] = []
    cx = x + 1  # after left border
    for cw in col_widths:
        col_x.append(cx)
        cx += cw + 1  # +1 for separator

    table_w = cx - x  # total table width including right border

    row_y = y

    # ── Top border ──
    grid.put(x, row_y, ch["tl"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
    grid.put(x + table_w - 1, row_y, ch["tr"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
    for ci, cw in enumerate(col_widths):
        for j in range(cw):
            grid.put(col_x[ci] + j, row_y, ch["h"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
        # Column separator on top border
        if ci < num_cols - 1:
            grid.put(col_x[ci] + cw, row_y, ch["t_down"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
    row_y += 1

    # ── Header row ──
    grid.put(x, row_y, ch["v"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
    for ci, (col_name, _) in enumerate(node.columns):
        text = col_name[:col_widths[ci]].ljust(col_widths[ci])
        grid.put_text(col_x[ci], row_y, text, style=header_style)
        sep_x = col_x[ci] + col_widths[ci]
        grid.put(sep_x, row_y, ch["v"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
    row_y += 1

    # ── Header separator ──
    grid.put(x, row_y, ch["t_right"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
    grid.put(x + table_w - 1, row_y, ch["t_left"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
    for ci, cw in enumerate(col_widths):
        for j in range(cw):
            grid.put(col_x[ci] + j, row_y, ch["h"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
        if ci < num_cols - 1:
            grid.put(col_x[ci] + cw, row_y, ch["cross"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
    row_y += 1

    # ── Data rows ──
    cell_style = CellStyle()
    for row_data in node.rows:
        grid.put(x, row_y, ch["v"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
        for ci in range(num_cols):
            cell_text = row_data[ci] if ci < len(row_data) else ""
            text = cell_text[:col_widths[ci]].ljust(col_widths[ci])

            # Check for @color{hex}{text} modifiers in cell content
            if "@" in cell_text:
                pattern = re.compile(r"@color\{([0-9a-fA-F]{3})\}\{([^}]*)\}")
                pos = 0
                styled_parts: list[tuple[str, CellStyle]] = []
                for m in pattern.finditer(cell_text):
                    if m.start() > pos:
                        styled_parts.append((cell_text[pos:m.start()], cell_style))
                    styled_parts.append((m.group(2), CellStyle(fg=m.group(1))))
                    pos = m.end()
                if pos < len(cell_text):
                    styled_parts.append((cell_text[pos:], cell_style))

                col_pos = col_x[ci]
                for part_text, part_style in styled_parts:
                    for pch in part_text:
                        if col_pos < col_x[ci] + col_widths[ci]:
                            grid.put(col_pos, row_y, pch, style=part_style)
                            col_pos += 1
                # Pad remaining
                while col_pos < col_x[ci] + col_widths[ci]:
                    grid.put(col_pos, row_y, " ")
                    col_pos += 1
            else:
                grid.put_text(col_x[ci], row_y, text, style=cell_style)

            sep_x = col_x[ci] + col_widths[ci]
            grid.put(sep_x, row_y, ch["v"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
        row_y += 1

    # ── Bottom border ──
    grid.put(x, row_y, ch["bl"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
    grid.put(x + table_w - 1, row_y, ch["br"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
    for ci, cw in enumerate(col_widths):
        for j in range(cw):
            grid.put(col_x[ci] + j, row_y, ch["h"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
        if ci < num_cols - 1:
            grid.put(col_x[ci] + cw, row_y, ch["t_up"], border_style, is_border=True, border_weight=BorderWeight.LIGHT)
