"""OCR-пайплайн извлечения кадастровых номеров из PDF-сканов изъятия ЕКБ (#1062).

PDF-документы «Сообщений о планируемом изъятии…» с екатеринбург.рф — растровые сканы:
pdfplumber даёт пустой текст. Используем PyMuPDF для растеризации (без poppler) +
Tesseract rus для распознавания.

OCR-шум: сканы содержат артефакты распознавания:
  - «О» вместо «0» в кад-номере: «66:41:07О2048:26»
  - пробелы внутри кад-паттерна: «66: 41 :0702048 :26»
  - «б» вместо «6» в первых цифрах: «6б:41:...»
Нормализация выполняется ДО поиска регуляркой кад-номеров.
"""

from __future__ import annotations

import io
import logging
import re
from typing import Any

logger = logging.getLogger(__name__)

# ── Константы ──────────────────────────────────────────────────────────────────

_DPI_DEFAULT = 300
_MAX_PAGES_DEFAULT = 30
_EXCERPT_RADIUS = 100  # символов вокруг первого кад-номера

# Строгий паттерн кад-номера ПОСЛЕ нормализации текста.
# 66:41:NNNNNNN:NN — регион:район:квартал:номер. Квартал 6 или 7 цифр.
_RE_CAD_NUM = re.compile(r"\b(66:\d{2}:\d{6,7}:\d+)\b")

# Паттерн номера акта: «№ NNN от DD.MM.YYYY» или «№ NNN-ПП».
_RE_ACT_NUMBER = re.compile(
    r"№\s*(\d[\d\s]*[\-–—]?\s*(?:ПП|ПА|РП|ПГ|ГП|МО))\b",
    re.UNICODE | re.IGNORECASE,
)
_RE_ACT_DATE = re.compile(r"от\s+(\d{2})\.(\d{2})\.(\d{4})")

# Паттерн цели: «в целях…», «для …», «под строительство …» — best-effort.
_RE_PURPOSE = re.compile(
    r"(?:для|в целях?|под)\s+([^.;,\n]{10,120})",
    re.IGNORECASE | re.UNICODE,
)


# ── Нормализация OCR-шума ──────────────────────────────────────────────────────


def _normalize_ocr_text(text: str) -> str:
    """Нормализует OCR-шум в кадастровых номерах перед поиском по регулярке.

    Стратегия:
    1. Убираем пробелы внутри цифро-двоеточных последовательностей вида «66: 41 :».
       Паттерн: пробелы после цифры или двоеточия перед цифрой или двоеточием.
    2. Заменяем «О» (кириллическая) → «0» (ноль) в позициях, окружённых цифрами/«:».
    3. Заменяем «б» → «6» только в начале кад-паттерна (если предшествует граница слова
       и после стоит «6:» или цифра с двоеточием).
    4. Убираем мягкий дефис (U+00AD) и другие невидимые разделители.

    Нормализация намеренно консервативна: применяется только в контексте,
    где риск ложной замены минимален.
    """
    # Шаг 1: убрать пробелы/переносы строк внутри «66: 41 :0702048 :26» →
    # используем итеративную замену пробелов между [\d:] и [\d:].
    # Итерируем до схождения (максимум 5 проходов — запас против вложенных пробелов).
    for _ in range(5):
        new_text = re.sub(r"(?<=[\d:])[ \t]+(?=[\d:])", "", text)
        if new_text == text:
            break
        text = new_text

    # Шаг 2: «О» (кириллическая, U+041E) → «0» между цифрами/двоеточиями.
    # Контекст: цифра/«:» ПЕРЕД и цифра/«:» ПОСЛЕ замены.
    text = re.sub(r"(?<=[\d:])О(?=[\d:])", "0", text)
    # Также в начале кад-паттерна «6б:41» → «66:41»:
    # б (кирилл.) → 6, если справа стоит «6:» (т.е. первые два символа кад-номера).
    text = re.sub(r"\bб(?=6:)", "6", text, flags=re.IGNORECASE)
    # Симметрично «6б:» → «66:» (OCR съел второй символ кад-номера МСК-66). См. #1228.
    text = re.sub(r"(?<=6)б(?=:)", "6", text, flags=re.IGNORECASE)

    # Шаг 3: мягкий дефис и нулевая ширина.
    text = text.replace("­", "").replace("​", "")

    return text


# ── OCR PDF ────────────────────────────────────────────────────────────────────


def ocr_pdf_text(
    pdf_bytes: bytes,
    *,
    dpi: int = _DPI_DEFAULT,
    max_pages: int = _MAX_PAGES_DEFAULT,
) -> str:
    """Растеризует PDF и выполняет OCR каждой страницы через Tesseract rus.

    Использует PyMuPDF (fitz) для рендера без poppler.
    При отсутствии tesseract или битом PDF возвращает пустую строку (graceful).

    Args:
        pdf_bytes: сырые байты PDF-файла.
        dpi: разрешение рендера (300 dpi — баланс качество/скорость для OCR).
        max_pages: максимальное число страниц для обработки.

    Returns:
        Конкатенированный OCR-текст всех страниц (разделитель «\\n\\n»).
        Пустая строка при ошибке.
    """
    try:
        import fitz  # type: ignore[import-untyped]  # PyMuPDF
    except ImportError:
        logger.error("ocr_pdf_text: PyMuPDF не установлен — добавь pymupdf в pyproject.toml")
        return ""

    try:
        import pytesseract  # type: ignore[import-untyped]
        from PIL import Image
    except ImportError:
        logger.error(
            "ocr_pdf_text: pytesseract или PIL не установлен — "
            "добавь pytesseract, pillow в pyproject.toml"
        )
        return ""

    try:
        doc = fitz.open(stream=pdf_bytes, filetype="pdf")
    except Exception as exc:
        logger.warning("ocr_pdf_text: не удалось открыть PDF (%d байт): %s", len(pdf_bytes), exc)
        return ""

    pages_total = min(len(doc), max_pages)
    logger.info(
        "ocr_pdf_text: PDF %d стр. → обрабатываем %d стр. при dpi=%d",
        len(doc),
        pages_total,
        dpi,
    )

    text_parts: list[str] = []
    mat = fitz.Matrix(dpi / 72, dpi / 72)  # масштаб: 72 dpi → target dpi

    for page_num in range(pages_total):
        page = doc[page_num]
        try:
            pix = page.get_pixmap(matrix=mat, alpha=False)
            img_bytes = pix.tobytes("png")
            img = Image.open(io.BytesIO(img_bytes))
        except Exception as exc:
            logger.warning("ocr_pdf_text: страница %d — ошибка рендера: %s", page_num + 1, exc)
            text_parts.append("")
            continue

        try:
            page_text: str = pytesseract.image_to_string(img, lang="rus")
        except pytesseract.pytesseract.TesseractNotFoundError:
            logger.error(
                "ocr_pdf_text: Tesseract не найден в системе. "
                "Установи tesseract-ocr + tesseract-ocr-rus (apt-get)."
            )
            return ""
        except Exception as exc:
            logger.warning("ocr_pdf_text: страница %d — OCR ошибка: %s", page_num + 1, exc)
            page_text = ""

        text_parts.append(page_text)

    full_text = "\n\n".join(text_parts)
    logger.info(
        "ocr_pdf_text: OCR завершён: %d стр. → %d символов",
        pages_total,
        len(full_text),
    )
    return full_text


# ── Извлечение записей из OCR-текста ──────────────────────────────────────────


def extract_izyatie_records(
    ocr_text: str,
    doc_title: str,
    doc_url: str,
) -> list[dict[str, Any]]:
    """Извлекает записи об изъятии ЗУ из OCR-текста документа.

    Нормализует OCR-шум (пробелы в кад-номерах, «О»→«0», «б»→«6»), находит
    кад-номера 66:41:NNNNNNN:NN, дедуплицирует, извлекает реквизиты акта и цель.

    Args:
        ocr_text: сырой текст от OCR (может содержать артефакты).
        doc_title: заголовок документа (из HTML-страницы раздела).
        doc_url: URL PDF-файла (сохраняется в поле doc_url).

    Returns:
        Список словарей — по одному на каждый уникальный кад-номер. Поля:
          cad_num, reservation_kind, basis_act, act_number, act_date,
          purpose, doc_url, source, raw_excerpt.
        Пустой список если кад-номеров нет.
    """
    if not ocr_text or not ocr_text.strip():
        return []

    # Нормализуем OCR-шум ДО поиска кад-номеров.
    normalized = _normalize_ocr_text(ocr_text)

    # Реквизиты акта из заголовка или текста.
    act_number = _extract_act_number(doc_title) or _extract_act_number(normalized)
    act_date = _extract_act_date(doc_title) or _extract_act_date(normalized)
    purpose = _extract_purpose(doc_title) or _extract_purpose(normalized)

    # Поиск кад-номеров.
    seen: set[str] = set()
    records: list[dict[str, Any]] = []
    first_pos: int | None = None

    for m in _RE_CAD_NUM.finditer(normalized):
        cad = m.group(1)
        if cad in seen:
            continue
        seen.add(cad)

        if first_pos is None:
            first_pos = m.start()
            raw_excerpt = _build_excerpt(normalized, m.start())
        else:
            raw_excerpt = None

        records.append(
            {
                "cad_num": cad,
                "reservation_kind": "изъятие",
                "basis_act": doc_title,
                "act_number": act_number,
                "act_date": act_date,
                "purpose": purpose,
                "doc_url": doc_url,
                "source": "izyatie_ekb_ocr",
                "raw_excerpt": raw_excerpt,
            }
        )

    if records:
        logger.info(
            "extract_izyatie_records: документ %r → %d кад-номеров (act=%s)",
            doc_title[:60],
            len(records),
            act_number,
        )
    else:
        logger.debug(
            "extract_izyatie_records: документ %r → кад-номеров не найдено",
            doc_title[:60],
        )

    return records


# ── Вспомогательные функции ────────────────────────────────────────────────────


def _extract_act_number(text: str) -> str | None:
    """Извлекает номер акта из текста (первое вхождение «№ NNN-ПП»)."""
    m = _RE_ACT_NUMBER.search(text)
    if not m:
        return None
    return re.sub(r"\s+", "", m.group(1))


def _extract_act_date(text: str) -> str | None:
    """Извлекает дату акта «от DD.MM.YYYY» → строка «YYYY-MM-DD» для SQL DATE."""
    m = _RE_ACT_DATE.search(text)
    if not m:
        return None
    day, month, year = m.group(1), m.group(2), m.group(3)
    try:
        # Валидируем диапазоны.
        d, mo, y = int(day), int(month), int(year)
        if 1 <= d <= 31 and 1 <= mo <= 12 and 2000 <= y <= 2100:
            return f"{y:04d}-{mo:02d}-{d:02d}"
    except ValueError:
        pass
    return None


def _extract_purpose(text: str) -> str | None:
    """Извлекает цель изъятия из текста (best-effort)."""
    m = _RE_PURPOSE.search(text)
    if m:
        return re.sub(r"\s+", " ", m.group(1)).strip().rstrip(".,;")
    return None


def _build_excerpt(text: str, pos: int) -> str:
    """Возвращает ±100 символов вокруг позиции первого кад-номера."""
    start = max(0, pos - _EXCERPT_RADIUS)
    end = min(len(text), pos + _EXCERPT_RADIUS)
    return text[start:end]


__all__ = ["_normalize_ocr_text", "extract_izyatie_records", "ocr_pdf_text"]
