"""Range formatting — featurized getCellRange output.

Replicates the TypeScript processCellRange / RangeFormatter logic:
- Formula abbreviation (frequent R1C1 patterns aliased as F1, F2, …)
- Style pattern grouping (cells grouped by style → S1, S2, …)
- Left context (row labels from cells to the left)
- Above context (column headers via voting)
"""

from __future__ import annotations

from shortcut_xl.api.style import read_style
from shortcut_xl.api.utils.helpers import index_to_address
from shortcut_xl.api.utils.numerical import NUMERIC_TYPES, is_numerical
from shortcut_xl.api.utils.ranges import consolidate_ranges
from shortcut_xl.api.utils.style_utils import get_style_description, get_style_key

# ---------------------------------------------------------------------------
# Constants (match TypeScript range-processing/types.ts)
# ---------------------------------------------------------------------------

CONTEXT_ROW_SEARCH_DEPTH = 20
CONTEXT_COLUMN_SEARCH_DEPTH = 20
MAX_LEFT_CONTEXT_COLUMNS = 2
HEADER_ROW_VOTING_PERCENT = 30
MIN_FORMULA_FREQUENCY = 10  # alias formulas appearing > this many times
MAX_STYLE_ENTRIES = 10


# ---------------------------------------------------------------------------
# Style analysis
# ---------------------------------------------------------------------------


def _analyze_styles(
    ws,
    sr: int,
    sc: int,
    rows: int,
    cols: int,
    values: list[list] | None = None,
) -> list[str]:
    """Analyze styles and return documentation lines.

    Groups cells by their style key, then generates range descriptions.
    *values* is an optional pre-read grid from ``rng.Value`` to avoid
    redundant per-cell COM reads.
    """
    style_map: dict[str, dict] = {}  # key → {cells, style, description}

    for r in range(rows):
        for c in range(cols):
            cell = ws.Cells(sr + r + 1, sc + c + 1)

            # Skip empty cells
            val = values[r][c] if values is not None else cell.Value
            if val is None and not cell.Text:
                continue

            style = read_style(cell)
            if not style:
                continue

            key = get_style_key(style)
            if key not in style_map:
                style_map[key] = {
                    'cells': [],
                    'style': style,
                    'description': get_style_description(style),
                }
            style_map[key]['cells'].append((sr + r, sc + c, val))

    if not style_map:
        return []

    lines: list[str] = []
    entries = list(style_map.values())[:MAX_STYLE_ENTRIES]

    for info in entries:
        cell_coords = [(r, c) for r, c, _ in info['cells']]
        range_desc = ', '.join(consolidate_ranges(cell_coords))
        count = len(info['cells'])

        # Content type hint
        sample = info['cells'][0][2]
        content_type = ''
        if isinstance(sample, NUMERIC_TYPES):
            content_type = ' (numbers)'
        elif isinstance(sample, str):
            content_type = ' (text)'

        lines.append(f'{range_desc}: {count} cells{content_type}')
        desc = info['description']
        if desc not in ('Default style', 'No style'):
            lines.append(f'  → {desc}')

    if len(style_map) > MAX_STYLE_ENTRIES:
        lines.append('')
        lines.append(f'... and {len(style_map) - MAX_STYLE_ENTRIES} more style patterns')

    return lines


# ---------------------------------------------------------------------------
# Left context
# ---------------------------------------------------------------------------


def _build_left_context(
    ws,
    sr: int,
    sc: int,
    rows: int,
) -> list[str] | None:
    """Scan leftward for row labels.

    For each row, collect up to MAX_LEFT_CONTEXT_COLUMNS textual (non-numeric)
    cells to the left of the range.
    """
    if sc == 0:
        return None

    left_bound = max(0, sc - CONTEXT_ROW_SEARCH_DEPTH)
    if left_bound >= sc:
        return None

    lines: list[str] = []
    any_found = False

    for r in range(rows):
        found: list[tuple[int, str, int]] = []  # (col, text, indent)
        collected = 0

        for c in range(sc - 1, left_bound - 1, -1):
            cell = ws.Cells(sr + r + 1, c + 1)
            val = cell.Value
            text = cell.Text
            if val is None and not text:
                continue
            display = str(text or val or '')
            if not display.strip():
                continue
            if is_numerical(display):
                continue

            indent = 0
            try:
                indent = cell.IndentLevel or 0
            except Exception:
                pass

            found.append((c, display, indent))
            collected += 1
            if collected >= MAX_LEFT_CONTEXT_COLUMNS:
                break

        if not found:
            lines.append('')
            continue

        any_found = True
        # Render left-to-right (ascending col)
        found.sort(key=lambda x: x[0])
        tokens = [f'{"→" * max(0, indent)}{index_to_address(sr + r, col)}:{text}' for col, text, indent in found]
        lines.append(' | '.join(tokens))

    return lines if any_found else None


# ---------------------------------------------------------------------------
# Above context (header row voting)
# ---------------------------------------------------------------------------


def _build_above_context(
    ws,
    sr: int,
    sc: int,
    ec: int,
) -> str | None:
    """Find the best header row above the range using voting, then build chain."""
    if sr == 0:
        return None

    total_cols = ec - sc + 1
    num_voting_cols = max(1, int((HEADER_ROW_VOTING_PERCENT / 100) * total_cols + 0.5))
    voting_start = max(sc, ec - num_voting_cols + 1)

    votes: dict[int, int] = {}

    for c in range(ec, voting_start - 1, -1):
        scanned = 0
        for r in range(sr - 1, -1, -1):
            scanned += 1
            if scanned > CONTEXT_COLUMN_SEARCH_DEPTH:
                break
            cell = ws.Cells(r + 1, c + 1)
            val = cell.Value
            text = cell.Text
            display = str(text or val or '')
            if not display.strip():
                continue
            if is_numerical(display):
                continue
            votes[r] = votes.get(r, 0) + 1
            break

    if not votes:
        return None

    # Pick row with most votes; tie-break by textual density
    best_row = max(votes, key=lambda r: (votes[r], _count_textual_in_row(ws, r, sc, ec)))

    # Build horizontal chain
    return _build_header_chain(ws, best_row, sc, ec)


def _count_textual_in_row(ws, row: int, sc: int, ec: int) -> int:
    """Count non-empty, non-numerical cells in a row span."""
    count = 0
    for c in range(sc, ec + 1):
        cell = ws.Cells(row + 1, c + 1)
        val = cell.Value
        text = cell.Text
        display = str(text or val or '')
        if not display.strip():
            continue
        if is_numerical(display):
            continue
        count += 1
    return count


def _build_header_chain(ws, row: int, sc: int, ec: int) -> str | None:
    """Build a pipe-separated chain of header cells, with '...' for numeric gaps."""
    tokens: list[str] = []
    saw_numeric = False

    for c in range(sc, ec + 1):
        cell = ws.Cells(row + 1, c + 1)
        val = cell.Value
        text = cell.Text
        display = str(text or val or '')
        if not display.strip():
            continue

        if is_numerical(display):
            saw_numeric = True
            continue

        addr = index_to_address(row, c)
        if tokens and saw_numeric:
            tokens.append('...')
        tokens.append(f'{addr}:{display}')
        saw_numeric = False

    return ' | '.join(tokens) if tokens else None


# ---------------------------------------------------------------------------
# Formula alias helpers
# ---------------------------------------------------------------------------


def _build_formula_aliases(
    formulas_r1c1: list[list],
    rows: int,
    cols: int,
) -> tuple[dict[str, str], dict[str, list[str]], list[list[str]]]:
    """Build R1C1 frequency map and alias map for frequent formulas.

    Returns (alias_map, alias_examples, r1c1_grid).
    """
    r1c1_freq: dict[str, int] = {}
    r1c1_grid: list[list[str]] = []

    for r in range(rows):
        row_r1c1: list[str] = []
        for c in range(cols):
            r1c1 = formulas_r1c1[r][c]
            if isinstance(r1c1, str) and r1c1.startswith('='):
                r1c1_freq[r1c1] = r1c1_freq.get(r1c1, 0) + 1
                row_r1c1.append(r1c1)
            else:
                row_r1c1.append('')
        r1c1_grid.append(row_r1c1)

    alias_map: dict[str, str] = {}
    alias_examples: dict[str, list[str]] = {}
    frequent = [f for f, cnt in r1c1_freq.items() if cnt > MIN_FORMULA_FREQUENCY]
    for i, f in enumerate(frequent):
        alias_map[f] = f'=F{i + 1}'
        alias_examples[f] = []

    return alias_map, alias_examples, r1c1_grid


def _build_data_lines(
    ws,
    sr: int,
    sc: int,
    rows: int,
    cols: int,
    values: list[list],
    formulas_a1: list[list],
    r1c1_grid: list[list[str]],
    alias_map: dict[str, str],
    alias_examples: dict[str, list[str]],
) -> list[str]:
    """Build per-row display lines with formula aliases.

    Mutates *alias_examples* to collect up to 2 A1 examples per pattern.
    """
    data_lines: list[str] = []
    for r in range(rows):
        parts: list[str] = []
        has_content = False
        for c in range(cols):
            addr = index_to_address(sr + r, sc + c)
            cell = ws.Cells(sr + r + 1, sc + c + 1)
            text = cell.Text
            display = '' if (not text and values[r][c] is None) else str(text or '')

            if display:
                has_content = True

            # Append formula (or alias)
            a1 = formulas_a1[r][c] if formulas_a1[r] is not None else None
            if a1 and isinstance(a1, str) and a1.startswith('='):
                r1c1 = r1c1_grid[r][c]
                alias = alias_map.get(r1c1)
                if alias:
                    display += f'({alias[1:]})'  # strip leading '='
                    examples = alias_examples[r1c1]
                    if len(examples) < 2:
                        examples.append(f'{addr}: {a1}')
                else:
                    display += f'({a1})'

            parts.append(f'{addr}:{display}')

        if has_content:
            data_lines.append(' | '.join(parts))

    return data_lines


# ---------------------------------------------------------------------------
# Main formatter
# ---------------------------------------------------------------------------

_MAX_RANGE_CELLS = 3000


def format_cell_range(
    ws,
    range_addr: str,
    include_style: bool = True,
) -> str:
    """Return featurized range representation matching TypeScript processCellRange.

    Output sections:
    1. Optional formula alias explanation
    2. Main data: ``A1:value(formula) | B1:value``
    3. Formula definitions (if any frequent patterns)
    4. Style patterns (if included)
    5. Context from cells to the left
    6. Context from cells above
    """
    from shortcut_xl.api.utils.com_utils import make_range, normalize_2d
    from shortcut_xl.api.utils.helpers import parse_range

    sr, sc, er, ec = parse_range(range_addr)
    rows = er - sr + 1
    cols = ec - sc + 1

    if rows * cols > _MAX_RANGE_CELLS:
        raise ValueError(f'Range {range_addr} has {rows * cols} cells; limit is {_MAX_RANGE_CELLS}.')

    rng = make_range(ws, sr, sc, er, ec)
    values = normalize_2d(rng.Value, rows, cols)
    formulas_a1 = normalize_2d(rng.Formula, rows, cols)
    formulas_r1c1 = normalize_2d(rng.FormulaR1C1, rows, cols)

    alias_map, alias_examples, r1c1_grid = _build_formula_aliases(
        formulas_r1c1,
        rows,
        cols,
    )
    data_lines = _build_data_lines(
        ws,
        sr,
        sc,
        rows,
        cols,
        values,
        formulas_a1,
        r1c1_grid,
        alias_map,
        alias_examples,
    )

    # Assemble output
    output_parts: list[str] = []

    if alias_map:
        output_parts.append('Common formulas are abbreviated like F1, F2, etc. and the definitions are below')
        output_parts.append('')

    output_parts.extend(data_lines)

    if alias_map:
        output_parts.append('')
        for r1c1_pattern, alias in alias_map.items():
            examples = alias_examples.get(r1c1_pattern, [])
            if examples:
                output_parts.append(f'{alias} -> {r1c1_pattern}  (e.g. {", ".join(examples)})')
            else:
                output_parts.append(f'{alias} -> {r1c1_pattern}')

    if include_style:
        style_lines = _analyze_styles(ws, sr, sc, rows, cols, values)
        if style_lines:
            output_parts.append('')
            output_parts.append('--- Style patterns ---')
            output_parts.extend(style_lines)

    left = _build_left_context(ws, sr, sc, rows)
    if left:
        output_parts.append('')
        output_parts.append('--- Context from cells to the left ---')
        output_parts.extend(left)

    above = _build_above_context(ws, sr, sc, ec)
    if above:
        output_parts.append('')
        output_parts.append('--- Context from cells above ---')
        output_parts.append(above)

    return '\n'.join(output_parts)
