"""TUI text utilities — visible width, truncation, ANSI-aware wrapping."""

from __future__ import annotations

import re
import unicodedata

# ANSI escape pattern: CSI sequences (ESC[...m/K/H/J/G), OSC (ESC]...BEL/ST), APC (ESC_...BEL/ST)
_ANSI_RE = re.compile(
    r"\x1b\[[0-9;]*[a-zA-Z]"     # CSI sequences
    r"|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)"  # OSC sequences
    r"|\x1b_[^\x07\x1b]*(?:\x07|\x1b\\)"   # APC sequences
    r"|\x1b\([A-Z0-9]"           # Character set designations
)

_ANSI_STYLE_RE = re.compile(r"\x1b\[[0-9;]*m")


def _char_width(ch: str) -> int:
    """Get display width of a single character."""
    if len(ch) == 0:
        return 0
    cat = unicodedata.category(ch)
    if cat.startswith("M"):  # Combining marks
        return 0
    eaw = unicodedata.east_asian_width(ch)
    if eaw in ("W", "F"):  # Wide or Fullwidth
        return 2
    return 1


def visible_width(text: str) -> int:
    """Calculate terminal display width ignoring ANSI escape codes.

    Handles wide characters (CJK, emojis) correctly.
    """
    stripped = _ANSI_RE.sub("", text)
    width = 0
    for ch in stripped:
        width += _char_width(ch)
    return width


def truncate_to_width(
    text: str,
    max_width: int,
    ellipsis: str = "...",
    pad: bool = False,
) -> str:
    """Truncate text to fit visible width, preserving ANSI codes.

    Args:
        text: Input text (may contain ANSI codes).
        max_width: Maximum visible width.
        ellipsis: String to append when truncated.
        pad: If True, pad with spaces to exact max_width.
    """
    if visible_width(text) <= max_width:
        if pad:
            return text + " " * (max_width - visible_width(text))
        return text

    ellipsis_width = visible_width(ellipsis)
    target_width = max_width - ellipsis_width
    if target_width < 0:
        target_width = 0

    result: list[str] = []
    current_width = 0
    i = 0
    chars = list(text)

    while i < len(chars):
        # Check for ANSI escape at this position
        remaining = text[sum(len(c) for c in chars[:i]):]
        ansi_match = _ANSI_RE.match(remaining)
        if ansi_match:
            ansi_code = ansi_match.group(0)
            result.append(ansi_code)
            # Advance past the ANSI code characters
            code_len = len(ansi_code)
            consumed = 0
            while consumed < code_len and i < len(chars):
                consumed += len(chars[i])
                i += 1
            continue

        ch = chars[i]
        w = _char_width(ch)
        if current_width + w > target_width:
            break
        result.append(ch)
        current_width += w
        i += 1

    # Reset styling before ellipsis
    output = "".join(result) + "\x1b[0m" + ellipsis

    if pad:
        output_width = current_width + ellipsis_width
        if output_width < max_width:
            output += " " * (max_width - output_width)

    return output


def wrap_text_with_ansi(text: str, width: int) -> list[str]:
    """Wrap text at word boundaries, preserving ANSI styling across lines.

    Returns a list of wrapped lines.
    """
    if width <= 0:
        return [text]

    lines: list[str] = []
    # Split on actual newlines first
    for paragraph in text.split("\n"):
        if not paragraph:
            lines.append("")
            continue

        # Simple word-wrap approach
        current_line = ""
        current_width = 0
        active_ansi: list[str] = []

        i = 0
        while i < len(paragraph):
            # Check for ANSI code
            remaining = paragraph[i:]
            ansi_match = _ANSI_STYLE_RE.match(remaining)
            if ansi_match:
                code = ansi_match.group(0)
                current_line += code
                if code == "\x1b[0m":
                    active_ansi.clear()
                else:
                    active_ansi.append(code)
                i += len(code)
                continue

            # Regular character
            ch = paragraph[i]
            w = _char_width(ch)

            if current_width + w > width and current_width > 0:
                # Line break needed
                lines.append(current_line)
                # Start new line with active ANSI codes
                current_line = "".join(active_ansi)
                current_width = 0

            current_line += ch
            current_width += w
            i += 1

        lines.append(current_line)

    return lines
