"""Pure utility functions — no COM dependency.

Address conversion, range parsing, color helpers.
Mirrors client/shared/src/lib/spreadsheet/utils/excel-address-utils.ts.
"""

import re

_CELL_RE = re.compile(r'^([A-Z]+)(\d+)$')


# ---------------------------------------------------------------------------
# Column letter <-> 0-based index
# ---------------------------------------------------------------------------


def col_letter(index: int) -> str:
    """Convert 0-based column index to Excel column letters (0->A, 25->Z, 26->AA)."""
    name = ''
    dividend = index + 1
    while dividend > 0:
        mod = (dividend - 1) % 26
        name = chr(65 + mod) + name
        dividend = (dividend - mod) // 26
    return name


def col_index(letter: str) -> int:
    """Convert Excel column letters to 0-based index (A->0, Z->25, AA->26)."""
    idx = 0
    for ch in letter.upper():
        idx = idx * 26 + (ord(ch) - 64)
    return idx - 1


# ---------------------------------------------------------------------------
# Address / range conversion (all 0-based)
# ---------------------------------------------------------------------------


def index_to_address(row: int, col: int) -> str:
    """Convert 0-based (row, col) to Excel address like 'A1'."""
    if row < 0 or col < 0:
        raise ValueError(f'Row and col must be >= 0, got ({row}, {col})')
    return f'{col_letter(col)}{row + 1}'


def address_to_index(address: str) -> tuple[int, int]:
    """Parse 'A1' into (row, col) 0-based. Raises ValueError on bad input.

    Handles absolute references like '$A$1', 'A$1', '$A1'.
    """
    m = _CELL_RE.match(address.upper().strip().replace('$', ''))
    if not m:
        raise ValueError(f'Invalid cell address: {address}')
    return int(m.group(2)) - 1, col_index(m.group(1))


def parse_range(range_str: str) -> tuple[int, int, int, int]:
    """Parse 'A1:C10' into (start_row, start_col, end_row, end_col), 0-based.

    Also accepts a single cell like 'A1' (returns start==end).
    """
    # Strip optional sheet prefix
    rest = _strip_sheet(range_str)
    parts = rest.split(':')
    sr, sc = address_to_index(parts[0])
    if len(parts) == 2:
        er, ec = address_to_index(parts[1])
    else:
        er, ec = sr, sc
    if sr > er or sc > ec:
        raise ValueError(f'Invalid range: {range_str}')
    return sr, sc, er, ec


def range_to_excel(start_row: int, start_col: int, end_row: int, end_col: int) -> str:
    """Convert 0-based coords to Excel range like 'A1:C10'."""
    return f'{index_to_address(start_row, start_col)}:{index_to_address(end_row, end_col)}'


def _strip_sheet(ref: str) -> str:
    """Strip optional 'Sheet1!' or \"'My Sheet'!\" prefix, return cell/range part."""
    _, range_part = extract_sheet_and_range(ref)
    return range_part


def build_cell_ref(workbook: str | None, sheet: str, address: str) -> str:
    """Build a full cell reference like '[Book1]Sheet1!A1' from components.

    This is the inverse of parse_cell_ref().
    """
    prefix = f'[{workbook}]' if workbook else ''
    return f'{prefix}{sheet}!{address}'


def parse_cell_ref(cell_ref: str) -> tuple[str | None, str | None, str | None]:
    """Parse a full cell reference into (workbook, sheet, address).

    Supports single cells and ranges:
      "[Book1]Sheet1!A1"   → ("Book1", "Sheet1", "A1")
      "Sheet1!A1:E1"       → (None,    "Sheet1", "A1:E1")

    Returns (None, None, None) if the reference is invalid.
    This is the inverse of build_cell_ref().
    """
    rest = cell_ref
    workbook = None

    if rest.startswith('['):
        close = rest.find(']')
        if close <= 1:  # missing or empty workbook name
            return None, None, None
        workbook = rest[1:close]
        rest = rest[close + 1 :]

    sheet, address = extract_sheet_and_range(rest)
    if not sheet or not address:
        return None, None, None

    return workbook, sheet, address


def extract_sheet_and_range(ref: str) -> tuple[str | None, str]:
    """Split 'Sheet1!A1:B2' into ('Sheet1', 'A1:B2'). Returns (None, ref) if no sheet."""
    if '!' not in ref:
        return None, ref
    if ref.startswith("'"):
        i = 1
        while i < len(ref):
            if ref[i] == "'":
                if i + 1 < len(ref) and ref[i + 1] == "'":
                    i += 2
                elif i + 1 < len(ref) and ref[i + 1] == '!':
                    sheet = ref[1:i].replace("''", "'")
                    return sheet, ref[i + 2 :]
                else:
                    break
            else:
                i += 1
        return None, ref
    bang = ref.index('!')
    return ref[:bang], ref[bang + 1 :]


# ---------------------------------------------------------------------------
# Color helpers
# ---------------------------------------------------------------------------


def rgb_to_bgr(r: int, g: int, b: int) -> int:
    """Convert RGB components to BGR integer for Excel COM."""
    return r + (g << 8) + (b << 16)


def hex_to_bgr(hex_str: str) -> int:
    """Convert '#FF0000' (red) to BGR integer for Excel COM."""
    h = hex_str.lstrip('#')
    if len(h) != 6:
        raise ValueError(f'Invalid hex color: {hex_str}')
    r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
    return rgb_to_bgr(r, g, b)


def bgr_to_hex(bgr: int) -> str:
    """Convert BGR integer from Excel COM to '#RRGGBB' hex string."""
    bgr = int(bgr)
    r = bgr & 0xFF
    g = (bgr >> 8) & 0xFF
    b = (bgr >> 16) & 0xFF
    return f'#{r:02X}{g:02X}{b:02X}'
