"""Эвристики извлечения, общие для всех форматов.

Все табличные источники (xlsx, xls, docx, таблицы из PDF) сводятся к «сетке» —
списку строк из строковых ячеек — и обрабатываются единым кодом. Здесь собраны
функции, понимающие реальные особенности казахстанских прайсов: разрядные пробелы
в числах, многотарифные колонки цен, строки-заголовки секций и шапку не в первой
строке таблицы.
"""

from __future__ import annotations

import re

Grid = list[list[str]]

# Ячейка-цена целиком: «14 400», «12600.0», «3 980», «9000 тг», «от 12000».
# Требуем, чтобы вся ячейка была числом (плюс валюта или «от»), иначе цифра внутри
# текста («Раздел 1.Консультации») ошибочно сошла бы за цену.
_PRICE_CELL_RE = re.compile(
    r"^(?:от\s+)?(\d[\d .,]*\d|\d)\s*(?:тг|тенге|₸|kzt|руб|\$)?\.?$",
    re.IGNORECASE,
)
# Похоже на код услуги: «U1.1», «A02.004.000», «1.0», «В02.110.002».
_CODE_RE = re.compile(r"^[A-ZА-Я]?\.?\d+([.\-]\d+)*[A-ZА-Я]?$")

_NAME_KEYS = ("наименование", "услуг", "название", "перечень")
_PRICE_KEYS = ("цена", "стоимость", "тариф", "тенге")
_UNIT_KEYS = ("ед.", "единица", "ед изм", "ед. изм")
_CODE_KEYS = ("код", "№", "тарификатор")

# Заголовок ценовой колонки → тип тарифа. Порядок важен: частные случаи раньше общих.
_TIER_PATTERNS: list[tuple[str, tuple[str, ...]]] = [
    ("nonresident", ("нерезидент", "без гражданства", "иностран")),
    ("far", ("дальнего", "дальнее")),
    ("cis", ("снг", "ближнего", "оралман")),
    ("insurance", ("страхов",)),
    ("partner", ("партнер", "партнён", "партнёр")),
    ("resident", ("резидент", "республики казахстан", " рк", "граждан республики")),
]


def clean(text) -> str:
    """Строковое значение ячейки без хвостовых пробелов и переводов строк."""
    if text is None:
        return ""
    return re.sub(r"\s+", " ", str(text)).strip()


def parse_price(text) -> float | None:
    """Вытащить цену из ячейки, если ВСЯ ячейка — число (плюс валюта или «от»).

    Возвращает положительный float или None. Намеренно строгая: «Раздел 1.…» или
    «приём (30 минут)» ценой не считаются — это защищает детектор секций.
    """
    s = clean(text)
    if not s:
        return None
    m = _PRICE_CELL_RE.match(s)
    if not m:
        return None
    raw = m.group(1).replace(" ", "")
    # Есть и точка, и запятая — запятая разрядная; иначе запятая = десятичная.
    if "," in raw and "." in raw:
        raw = raw.replace(",", "")
    else:
        raw = raw.replace(",", ".")
    try:
        value = float(raw)
    except ValueError:
        return None
    return value if value > 0 else None


def looks_like_code(text) -> bool:
    """Похоже ли значение на код услуги (внутренний или тарификатора)."""
    s = clean(text)
    return bool(s) and len(s) <= 16 and bool(_CODE_RE.match(s))


def is_real_name(text) -> bool:
    """Похоже ли значение на настоящее название услуги, а не на код или число.

    Защищает нормализацию от мусора: коды («E04.000.012»), номера и цены, по ошибке
    попавшие в колонку названия, не должны уходить в сопоставление со справочником.
    Требуем минимум три кириллические буквы и не код-подобную форму.
    """
    s = clean(text)
    cyrillic = sum(1 for ch in s if "а" <= ch.lower() <= "я" or ch.lower() == "ё")
    if cyrillic < 3 or looks_like_code(s):
        return False
    # Код-мнемоника: цифры + только заглавные буквы + без пробела (напр. «МОЗО.15») — не услуга.
    if any(c.isdigit() for c in s) and " " not in s and not any(c.islower() for c in s):
        return False
    return True


def classify_price_tier(header_text) -> str | None:
    """По тексту заголовка ценовой колонки определить тип тарифа."""
    h = clean(header_text).lower()
    if not h:
        return None
    for tier, keys in _TIER_PATTERNS:
        if any(k in h for k in keys):
            return tier
    if any(k in h for k in _PRICE_KEYS):
        return "resident"  # обобщённая «Цена»/«Стоимость» — основной тариф
    return None


def _has_key(cells: list[str], keys: tuple[str, ...]) -> bool:
    joined = " | ".join(c.lower() for c in cells)
    return any(k in joined for k in keys)


def find_header_row(grid: Grid, scan: int = 30) -> int:
    """Найти индекс строки заголовков (она не всегда первая).

    Заголовок — строка, где есть и колонка названия услуги, и хотя бы одна
    ценовая колонка. Если явной шапки нет, берётся первая строка, под которой
    идут пары «текст + число».
    """
    best_idx, best_score = -1, 0
    for i, row in enumerate(grid[:scan]):
        cells = [clean(c) for c in row]
        if not any(cells):
            continue
        score = 0
        if _has_key(cells, _NAME_KEYS):
            score += 2
        score += sum(1 for c in cells if classify_price_tier(c))
        if _has_key(cells, _UNIT_KEYS):
            score += 1
        if score > best_score:
            best_idx, best_score = i, score
    if best_idx >= 0 and best_score >= 2:
        return best_idx
    # Фолбэк: первая строка, под которой есть пара «непустой текст + число».
    for i in range(min(scan, len(grid))):
        for j in range(i + 1, min(i + 6, len(grid))):
            cells = [clean(c) for c in grid[j]]
            texts = [c for c in cells if c and not parse_price(c)]
            nums = [c for c in cells if parse_price(c)]
            if texts and nums:
                return i
    return 0
