"""Display rendering utilities.

Static helper functions for formatting and measuring terminal output.
"""

import re
import shutil
import unicodedata
from typing import List, Optional

from slopmop.core.result import ScopeInfo
from slopmop.reporting.display import config

# Regex to match ANSI escape sequences (colors, cursor movement, etc.)
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")


def get_terminal_width() -> int:
    """Get current terminal width, with fallback.

    Returns:
        Terminal width in columns.
    """
    try:
        return shutil.get_terminal_size().columns
    except (ValueError, OSError):
        return config.DEFAULT_TERMINAL_WIDTH


def display_width(text: str) -> int:
    """Calculate terminal display width of a string.

    Wide characters (emoji, CJK) take 2 columns.  ANSI escape sequences
    are stripped before measuring so they don't inflate the count.

    Args:
        text: String to measure

    Returns:
        Number of terminal columns the text occupies
    """
    stripped = _ANSI_RE.sub("", text)
    width = 0
    for ch in stripped:
        cp = ord(ch)
        # Zero-width characters: variation selectors, ZWJ, ZWSP, BOM/ZWNBSP
        if cp in (0xFE0E, 0xFE0F, 0x200B, 0x200D, 0xFEFF):
            continue
        cat = unicodedata.east_asian_width(ch)
        if cat in ("W", "F"):
            width += 2
        else:
            width += 1
    return width


def strip_ansi(text: str) -> str:
    """Remove all ANSI escape sequences from text.

    Args:
        text: Text possibly containing ANSI codes

    Returns:
        Clean text with no ANSI sequences.
    """
    return _ANSI_RE.sub("", text)


def truncate_to_width(text: str, max_width: int) -> str:
    """Truncate a string to fit within *max_width* visible columns.

    ANSI escape sequences are preserved but don't count toward the
    width budget, so color codes are never cut in half.  A trailing
    reset (``\\033[0m``) is appended when the string contains any
    ANSI code to guarantee no color leaks.

    Args:
        text: Text to truncate (may contain ANSI codes)
        max_width: Maximum display columns allowed

    Returns:
        Truncated string that fits in *max_width* columns.
    """
    result: List[str] = []
    col = 0
    i = 0
    has_ansi = False

    while i < len(text):
        # Check for ANSI escape at current position
        m = _ANSI_RE.match(text, i)
        if m:
            result.append(m.group())
            has_ansi = True
            i = m.end()
            continue

        ch = text[i]
        cat = unicodedata.east_asian_width(ch)
        w = 2 if cat in ("W", "F") else 1

        if col + w > max_width:
            break

        result.append(ch)
        col += w
        i += 1

    out = "".join(result)
    # Ensure we reset color at the end to prevent bleed
    if has_ansi:
        out += "\033[0m"
    return out


def format_time(seconds: float, *, allow_fast_label: bool = True) -> str:
    """Format seconds as fixed-width, right-aligned time string.

    Produces consistent-width output so timing columns align
    vertically in the display.  Sub-60s values are formatted as
    ``Xs`` or ``X.Xs`` (always includes one decimal for < 100s).
    Near-zero values (< 0.05s) are shown as ``" fast"`` to avoid
    the meaningless ``"  0.0s"`` label — unless ``allow_fast_label``
    is False, in which case the numeric value is always shown.

    Use ``allow_fast_label=False`` for *actual* durations (the check
    really did run, show how long it took) and the default ``True``
    for *expected* / ETA durations where "fast" is more informative.

    Args:
        seconds: Time in seconds
        allow_fast_label: If True (default), sub-0.05s values render
            as ``" fast"``.  If False, they render as ``"  0.0s"``.

    Returns:
        Formatted string like "  5.2s", " 42.1s", "1m 30s", " fast" etc.
        Width is 6 characters for sub-60s values.
    """
    if seconds < 0.05 and allow_fast_label:
        return "  fast"
    if seconds < 60:
        return f"{seconds:5.1f}s"
    elif seconds < 3600:
        mins = int(seconds // 60)
        secs = seconds % 60
        return f"{mins}m {secs:.1f}s"
    else:
        hours = int(seconds // 3600)
        mins = int((seconds % 3600) // 60)
        secs = seconds % 60
        return f"{hours}h {mins}m {secs:.1f}s"


def align_columns(time_str: str, eta_str: str) -> str:
    """Right-align time and ETA into fixed-width columns.

    Args:
        time_str: Elapsed/duration string
        eta_str: ETA string

    Returns:
        Combined right-side string with consistent column widths
    """
    return (
        f"{time_str:>{config.TIME_COLUMN_WIDTH}}  "
        f"{eta_str:>{config.ETA_COLUMN_WIDTH}}"
    )


def right_justify(left: str, right: str, term_width: Optional[int] = None) -> str:
    """Right-justify a line with left and right parts.

    Uses display width (accounting for wide/emoji characters) to
    calculate padding correctly.

    Args:
        left: Left-aligned content
        right: Right-aligned content
        term_width: Terminal width (auto-detected if None)

    Returns:
        Formatted line with proper padding
    """
    if term_width is None:
        term_width = get_terminal_width()
    left_w = display_width(left)
    right_w = display_width(right)
    padding = max(1, term_width - left_w - right_w)
    return left + (" " * padding) + right


def build_dot_leader(
    left: str,
    right: str,
    term_width: int,
    animation_tick: int,
) -> str:
    """Build a line with an animated dot leader between left and right.

    A subtle pulse (brighter dot) travels through the dot leader to
    indicate activity, giving visual feedback even when no timing
    data is available.

    Args:
        left: Left-aligned content
        right: Right-aligned content
        term_width: Terminal width in columns
        animation_tick: Monotonic counter for pulse position

    Returns:
        Formatted line with animated dot leader
    """
    left_w = display_width(left)
    right_w = display_width(right)
    gap = term_width - left_w - right_w - 2  # 1 space padding each side

    if gap <= 0:
        return right_justify(left, right, term_width)

    # Build dot leader with traveling pulse
    dots = list(config.DOT_CHAR * gap)
    # Pulse travels the full width using monotonic tick counter
    pulse_pos = (animation_tick * 2) % max(gap, 1)
    for i in range(config.PULSE_WIDTH):
        idx = (pulse_pos + i) % gap
        dots[idx] = config.PULSE_CHAR

    leader = "".join(dots)
    return f"{left} {leader} {right}"


def build_progress_bar(
    left: str,
    right: str,
    term_width: int,
    pct: float,
    colors_enabled: Optional[bool] = None,
    bar_color: Optional[str] = None,
    pct_label: Optional[str] = None,
) -> str:
    """Build a line with a progress bar between left and right.

    Used for checks with timing estimates to show completion percentage.

    Args:
        left: Left-aligned content (spinner + name)
        right: Right-aligned content (time columns)
        term_width: Terminal width in columns
        pct: Completion percentage (0.0 to 1.0)
        colors_enabled: Whether to colorize the filled portion
        bar_color: ANSI color code to use for the filled portion (e.g. "\033[32m").
            Defaults to cyan if None.
        pct_label: Override the percentage label (e.g. "|+13%" for overrun).
            May contain ANSI codes; display width is measured correctly.

    Returns:
        Formatted line with progress bar
    """
    left_w = display_width(left)
    right_w = display_width(right)
    gap = term_width - left_w - right_w - 2  # 1 space padding each side

    if gap < config.MIN_PROGRESS_BAR_WIDTH:
        return right_justify(left, right, term_width)

    if pct_label is None:
        pct_label = f"{int(pct * 100):>3}%"
    pct_label_width = display_width(pct_label)
    bar_width = gap - pct_label_width - 3  # [] + space before pct
    if bar_width < config.MIN_BAR_CONTENT_WIDTH:
        return right_justify(left, right, term_width)

    filled = int(pct * bar_width)
    filled_str = config.PROGRESS_FILL * filled
    empty_str = config.PROGRESS_EMPTY * (bar_width - filled)

    # Colorize the filled portion in the category color when colors are enabled
    if colors_enabled is None:
        from slopmop.reporting.display.colors import supports_color

        colors_enabled = supports_color()
    if colors_enabled and filled > 0:
        color_code = bar_color if bar_color else "\033[36m"  # default cyan
        filled_str = f"{color_code}{filled_str}\033[0m"

    middle = f"[{filled_str}{empty_str}] {pct_label}"

    return f"{left} {middle} {right}"


def build_overall_progress(
    completed: int,
    total: int,
    elapsed: float,
    term_width: Optional[int] = None,
    colors_enabled: Optional[bool] = None,
    category_sequence: Optional[List[str]] = None,
) -> str:
    """Build the overall progress line with bar and stats.

    When *category_sequence* is provided, the filled portion of the
    bar becomes a domino-line of category colors — each completed
    check is one segment, colored by its category, in the order
    checks finished.  This gives a striped barber-pole effect rather
    than grouping like categories together.

    Args:
        completed: Number of completed checks
        total: Total number of checks
        elapsed: Elapsed time in seconds
        term_width: Terminal width (auto-detected if None)
        colors_enabled: Whether to colorize the filled portion
        category_sequence: Ordered list of category keys, one per
            completed check, in completion order.  Falls back to
            solid green if omitted.

    Returns:
        Formatted progress line
    """
    if term_width is None:
        term_width = get_terminal_width()

    # Right side: count + elapsed
    elapsed_str = format_time(elapsed, allow_fast_label=False)
    right_side = f"{completed}/{total} · {elapsed_str} elapsed"

    # Calculate bar width from remaining space
    # "Progress: [" + bar + "]  " + right_side
    chrome_len = len("Progress: []  ") + len(right_side)
    bar_width = max(10, term_width - chrome_len)

    pct = completed / total if total > 0 else 0
    filled = int(pct * bar_width)
    empty_str = config.PROGRESS_EMPTY * (bar_width - filled)

    # Colorize filled portion
    if colors_enabled is None:
        from slopmop.reporting.display.colors import supports_color

        colors_enabled = supports_color()

    if colors_enabled and filled > 0 and category_sequence:
        # Domino-line: each check gets proportional width in completion order
        from slopmop.reporting.display.colors import category_header_color

        n = len(category_sequence)
        reset = "\033[0m"
        segments: List[str] = []
        chars_used = 0

        for i, cat in enumerate(category_sequence):
            # Distribute bar chars proportionally; last segment absorbs rounding
            if i == n - 1:
                seg_w = filled - chars_used
            else:
                seg_w = max(1, round(filled * (i + 1) / n) - chars_used)
            if seg_w <= 0:
                continue
            color = category_header_color(cat, True)
            seg_str = config.PROGRESS_FILL * seg_w
            if color:
                segments.append(f"{color}{seg_str}{reset}")
            else:
                segments.append(seg_str)
            chars_used += seg_w

        filled_str = "".join(segments)
    elif colors_enabled and filled > 0:
        filled_str = f"\033[32m{config.PROGRESS_FILL * filled}\033[0m"  # green
    else:
        filled_str = config.PROGRESS_FILL * filled

    left_side = f"Progress: [{filled_str}{empty_str}]"
    return right_justify(left_side, right_side, term_width)


def truncate_for_inline(text: str, max_width: int) -> str:
    """Truncate text for inline failure preview.

    Args:
        text: Text to truncate
        max_width: Maximum display width

    Returns:
        Truncated text with ellipsis if needed
    """
    if not text:
        return ""

    # Get first non-empty line
    lines = text.strip().split("\n")
    first_line = ""
    for line in lines:
        stripped = line.strip()
        if stripped:
            first_line = stripped
            break

    if not first_line:
        return ""

    # Truncate if needed
    if display_width(first_line) <= max_width:
        return first_line

    # Truncate to fit with ellipsis
    result: List[str] = []
    width = 0
    for char in first_line:
        char_width = 2 if unicodedata.east_asian_width(char) in ("W", "F") else 1
        if width + char_width + 1 > max_width:  # +1 for ellipsis
            break
        result.append(char)
        width += char_width

    return "".join(result) + "…"


def build_column_header_line(term_width: Optional[int] = None) -> str:
    """Build a single column-header line for the report.

    Uses the same width constants and separators as the data rows so
    column edges align vertically.

    Layout::

        <padding>     avg    time  history

    Args:
        term_width: Terminal width (auto-detected if None)

    Returns:
        Right-justified header-label line
    """
    if term_width is None:
        term_width = get_terminal_width()

    right = config.TIMING_HEADER
    # Pad to right edge
    padding = max(0, term_width - display_width(right))
    return " " * padding + right


def build_category_header(
    label: str,
    completed: int,
    total: int,
    term_width: Optional[int] = None,
    scope: Optional[ScopeInfo] = None,
    elapsed: Optional[float] = None,
) -> str:
    """Build a category header line filled with dashes.

    Produces a line like::

        ── Python [3/6] · 6.8s ──────────────────────

    Args:
        label: Category display label (e.g. "🐍 Python")
        completed: Completed checks in this category
        total: Total checks in this category
        term_width: Terminal width (auto-detected if None)
        scope: Optional scope metrics (unused; kept for API compat)
        elapsed: Optional aggregate time for the category

    Returns:
        Formatted header line
    """
    if term_width is None:
        term_width = get_terminal_width()

    dash = config.HEADER_DASH
    progress = f"[{completed}/{total}]"
    if elapsed is not None and elapsed > 0:
        time_str = format_time(elapsed, allow_fast_label=False)
        left_part = f"{dash * 2} {label} {progress} · {time_str} "
    else:
        left_part = f"{dash * 2} {label} {progress} "

    left_w = display_width(left_part)
    fill = max(0, term_width - left_w)
    return f"{left_part}{dash * fill}"


def strip_category_prefix(check_name: str) -> str:
    """Strip the category prefix from a check name.

    'laziness:sloppy-formatting.py' → 'lint-format'
    'myopia:code-sprawl'    → 'code-sprawl'
    'some-check'         → 'some-check' (no prefix)

    Args:
        check_name: Full check name (possibly with category prefix)

    Returns:
        Check name without category prefix
    """
    if ":" in check_name:
        return check_name.split(":", 1)[1]
    return check_name
