"""Чтение исходных файлов в «сетки» строк плюс метаданные документа.

Каждый читатель возвращает (список сеток, сырой текст). Несколько сеток бывает у
многолистовых книг и многотабличных PDF. Сырой текст нужен для аудита и для
детектора битого текстового слоя.
"""

from __future__ import annotations

import re
from pathlib import Path

from etl.extractors.common import Grid, clean


def year_from_name(name: str) -> int | None:
    """Год прайса из имени файла («Клиника 2 прайс 2025 год» → 2025)."""
    m = re.search(r"(20\d{2})", name)
    return int(m.group(1)) if m else None


def partner_from_name(name: str) -> str:
    """Название клиники из имени файла, очищенное от слов «прайс», года и т. п."""
    base = Path(name).stem
    base = re.sub(r"(?i)\b(прайс|price|прейскурант|год)\b", " ", base)
    base = re.sub(r"20\d{2}", " ", base)
    base = re.sub(r"[_\-]+", " ", base)
    return re.sub(r"\s+", " ", base).strip()


_KZ_CITIES = {
    "алмат": "Алматы", "астан": "Астана", "нур-султан": "Астана", "шымкент": "Шымкент",
    "караганд": "Караганда", "актобе": "Актобе", "павлодар": "Павлодар", "семей": "Семей",
    "усть-камен": "Усть-Каменогорск", "костанай": "Костанай", "атырау": "Атырау", "тараз": "Тараз",
    "уральск": "Уральск", "кокшетау": "Кокшетау", "петропавл": "Петропавловск",
    "кызылорд": "Кызылорда", "талдыкорган": "Талдыкорган", "актау": "Актау",
}


def city_from_text(text: str | None) -> str | None:
    """Город партнёра по тексту документа (БИН в казахстанских прайсах обычно не печатают)."""
    low = (text or "").lower()
    for stem, name in _KZ_CITIES.items():
        if stem in low:
            return name
    return None


def detect_format(path: str) -> str:
    ext = Path(path).suffix.lower()
    return {".xlsx": "xlsx", ".xls": "xls", ".docx": "docx", ".pdf": "pdf"}.get(
        ext, ext.lstrip(".")
    )


def docx_grids(path: str) -> tuple[list[Grid], str]:
    import docx

    document = docx.Document(path)
    grids = [
        [[clean(cell.text) for cell in row.cells] for row in table.rows]
        for table in document.tables
    ]
    text = "\n".join(p.text for p in document.paragraphs)
    return grids, text


def xlsx_grids(path: str) -> tuple[list[Grid], str]:
    from openpyxl import load_workbook

    workbook = load_workbook(path, read_only=True, data_only=True)
    grids = []
    for sheet in workbook.worksheets:
        grid = [[clean(c) for c in row] for row in sheet.iter_rows(values_only=True)]
        if any(any(row) for row in grid):
            grids.append(grid)
    return grids, ""


def xls_grids(path: str) -> tuple[list[Grid], str]:
    import xlrd

    book = xlrd.open_workbook(path)
    grids = []
    for sheet in book.sheets():
        grid = [
            [clean(sheet.cell_value(r, c)) for c in range(sheet.ncols)] for r in range(sheet.nrows)
        ]
        if any(any(row) for row in grid):
            grids.append(grid)
    return grids, ""


def _line_to_cells(words: list[dict], x_gap: float) -> list[str]:
    """Слова одной строки → ячейки: большой горизонтальный разрыв = граница колонки."""
    words.sort(key=lambda w: w["x0"])
    cells = [words[0]["text"]]
    right = words[0]["x1"]
    for w in words[1:]:
        if w["x0"] - right > x_gap:
            cells.append(w["text"])
        else:
            cells[-1] += " " + w["text"]
        right = w["x1"]
    return [clean(c) for c in cells]


def _page_to_grid(page, y_tol: float = 3.0, x_gap: float = 18.0) -> Grid:
    """Восстановить табличную сетку страницы PDF по координатам слов.

    У многих прайсов нет линий таблицы, поэтому pdfplumber.extract_tables ничего не
    находит. Здесь слова группируются в строки по вертикали (top), а внутри строки
    режутся на ячейки по горизонтальным разрывам — так колонки цен (резидент/
    нерезидент/…) не сливаются в одну.
    """
    words = page.extract_words(use_text_flow=False, keep_blank_chars=False)
    if not words:
        return []
    rows: Grid = []
    line: list[dict] = []
    current_band: int | None = None
    for w in sorted(words, key=lambda w: (round(w["top"] / y_tol), w["x0"])):
        band = round(w["top"] / y_tol)
        if current_band is None or band == current_band:
            line.append(w)
        else:
            rows.append(_line_to_cells(line, x_gap))
            line = [w]
        current_band = band
    if line:
        rows.append(_line_to_cells(line, x_gap))
    return rows


def pdf_grids(path: str) -> tuple[list[Grid], str]:
    """Все страницы PDF собираются в одну сетку — так шапка распознаётся один раз."""
    import pdfplumber

    grid: Grid = []
    texts: list[str] = []
    with pdfplumber.open(path) as pdf:
        for page in pdf.pages:
            texts.append(page.extract_text() or "")
            grid.extend(_page_to_grid(page))
    return [grid], "\n".join(texts)


def pdf_page_count(path: str) -> int:
    """Число страниц PDF — нужно для оценки выхода строк и стоимости Vision."""
    import pdfplumber

    with pdfplumber.open(path) as pdf:
        return len(pdf.pages)


def is_garbled(text: str, min_letters: int = 200) -> bool:
    """Битый текстовый слой: мало кириллицы или много латиницы-мусора.

    Русский прайс почти не содержит латинских букв (кроме редких брендов), поэтому
    их обилие — верный признак испорченной кодировки. На таком документе нативный
    парсинг бессмыслен, его добирает путь через Vision.
    """
    letters = [ch for ch in text if ch.isalpha()]
    if len(letters) < min_letters:
        return False
    cyrillic = sum(1 for ch in letters if "а" <= ch.lower() <= "я" or ch.lower() == "ё")
    latin = sum(1 for ch in letters if "a" <= ch.lower() <= "z")
    return cyrillic / len(letters) < 0.55 or latin / len(letters) > 0.18
