"""Парсер постановлений об изъятии/резервировании ЗУ из PDF/текста (#1091, #1062, #1067).

Источник — PDF-постановления Правительства Свердловской области (pravo.gov66.ru / ПАГЕ).
Геометрии зон изъятия/резервирования нет ни в одном spatial-источнике; перечень ЗУ существует
ТОЛЬКО текстом. Этот модуль — чистая функция от bytes/text: извлекает кад-номера, тип
(изъятие/резервирование), реквизиты акта, цель. Без сети; тестируемо без PDF-файлов.

Seed-документ: 509-ПП (Правительство СО, изъятие под реконструкцию ул. Татищева, ЕКБ).

Зависимость pdfplumber: добавлена в backend/pyproject.toml. Если недоступна — модуль падает
с ImportError на уровне extract_from_pdf (parse-функции работают без неё).
"""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass
from datetime import date
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    pass

logger = logging.getLogger(__name__)

# ── Константы / регулярки ──────────────────────────────────────────────────────

# Полный кадастровый номер ЗУ: 66:41:1234567:12 (6 или 7 цифр в квартале, любой номер ЗУ).
_RE_CAD_NUM = re.compile(r"\b(\d{2}:\d{2}:\d{6,7}:\d+)\b")

# Номер постановления: 509-ПП, 1234-ПА, 55-ПП, NNN-РП и т.п.
# Паттерн намеренно широкий — постановления РФ имеют разные суффиксы.
_RE_ACT_NUMBER = re.compile(
    r"№\s*(\d+[\s\-–—]?(?:[А-Яа-яA-Za-z]+[\-–—]?)*(?:ПП|ПА|РП|ПГ|ГП|МО))\b",
    re.UNICODE,
)

# Дата акта: «от 12.03.2024» (приоритетно) или «12 марта 2024 г.»
_RE_DATE_DMY = re.compile(r"от\s+(\d{2})\.(\d{2})\.(\d{4})")
_RE_DATE_WORD = re.compile(
    r"(\d{1,2})\s+"
    r"(январ[яь]|феврал[яь]|март[аа]?|апрел[яь]|ма[йя]|июн[яь]|"
    r"июл[яь]|август[аа]?|сентябр[яь]|октябр[яь]|ноябр[яь]|декабр[яь])"
    r"\s+(\d{4})",
    re.UNICODE | re.IGNORECASE,
)

_MONTH_MAP: dict[str, int] = {
    "январ": 1,
    "феврал": 2,
    "март": 3,
    "апрел": 4,
    "ма": 5,
    "июн": 6,
    "июл": 7,
    "август": 8,
    "сентябр": 9,
    "октябр": 10,
    "ноябр": 11,
    "декабр": 12,
}

# Слова-маркеры типа документа.
_RE_REZERV = re.compile(r"резервир", re.IGNORECASE | re.UNICODE)
_RE_IZYAT = re.compile(r"изъят", re.IGNORECASE | re.UNICODE)

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

_EXCERPT_RADIUS = 120  # символов в каждую сторону от первого кад-номера


# ── Dataclass результата ───────────────────────────────────────────────────────


@dataclass(frozen=True)
class ReservationRecord:
    """Одна запись кад-номера ЗУ, извлечённая из постановления."""

    cad_num: str
    reservation_kind: str  # 'изъятие' | 'резервирование'
    act_number: str | None
    act_date: date | None
    purpose: str | None
    raw_excerpt: str | None


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


def _parse_act_number(text: str) -> str | None:
    """Извлекает номер постановления из текста (первое вхождение)."""
    m = _RE_ACT_NUMBER.search(text)
    if not m:
        return None
    # Нормализуем пробелы внутри номера.
    return re.sub(r"\s+", "", m.group(1))


def _parse_act_date(text: str) -> date | None:
    """Извлекает дату акта: сначала «от DD.MM.YYYY», потом «DD <месяц-словом> YYYY»."""
    m = _RE_DATE_DMY.search(text)
    if m:
        try:
            return date(int(m.group(3)), int(m.group(2)), int(m.group(1)))
        except ValueError:
            logger.debug("_parse_act_date: некорректная числовая дата %s", m.group(0))

    m2 = _RE_DATE_WORD.search(text)
    if m2:
        day_s, month_s, year_s = m2.group(1), m2.group(2).lower(), m2.group(3)
        month_num = next(
            (v for k, v in _MONTH_MAP.items() if month_s.startswith(k)),
            None,
        )
        if month_num is not None:
            try:
                return date(int(year_s), month_num, int(day_s))
            except ValueError:
                logger.debug("_parse_act_date: некорректная словесная дата %s", m2.group(0))

    return None


def _detect_kind(text: str, default_kind: str) -> str:
    """Определяет тип операции: 'резервирование' | 'изъятие' | default_kind."""
    if _RE_REZERV.search(text):
        return "резервирование"
    if _RE_IZYAT.search(text):
        return "изъятие"
    return default_kind


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:
    """±120 символов вокруг позиции первого кад-номера."""
    start = max(0, pos - _EXCERPT_RADIUS)
    end = min(len(text), pos + _EXCERPT_RADIUS)
    return text[start:end]


# ── Основные публичные функции ─────────────────────────────────────────────────


def extract_reservations(
    text: str,
    *,
    default_kind: str = "изъятие",
) -> list[ReservationRecord]:
    """Извлекает записи ЗУ из текста постановления об изъятии/резервировании.

    Args:
        text: полный текст постановления (конкатенация всех страниц PDF или plain-text).
        default_kind: тип по умолчанию, если ни «изъят», ни «резервир» не встретились.

    Returns:
        Список ReservationRecord, дедуплицированный по cad_num внутри документа.
        Пустой список если кад-номеров нет.
    """
    if not text or not text.strip():
        return []

    kind = _detect_kind(text, default_kind)
    act_number = _parse_act_number(text)
    act_date = _parse_act_date(text)
    purpose = _extract_purpose(text)

    seen: set[str] = set()
    records: list[ReservationRecord] = []
    first_pos: int | None = None

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

        if first_pos is None:
            first_pos = m.start()

        records.append(
            ReservationRecord(
                cad_num=cad,
                reservation_kind=kind,
                act_number=act_number,
                act_date=act_date,
                purpose=purpose,
                raw_excerpt=_build_excerpt(text, m.start()) if first_pos == m.start() else None,
            )
        )

    # Ставим raw_excerpt только первой записи (выше он уже записан через first_pos == m.start()).
    # Для остальных строк оставляем None (достаточно для верификации).
    logger.debug(
        "extract_reservations: kind=%s act=%s date=%s cad_count=%d",
        kind,
        act_number,
        act_date,
        len(records),
    )
    return records


def extract_from_pdf(pdf_bytes: bytes) -> list[ReservationRecord]:
    """Извлекает записи ЗУ из байт PDF-постановления.

    Args:
        pdf_bytes: сырые байты PDF-файла.

    Returns:
        Список ReservationRecord. Пустой список при пустом/нечитаемом PDF.

    Raises:
        ImportError: если pdfplumber не установлен (добавить в backend/pyproject.toml).
    """
    try:
        import pdfplumber  # type: ignore[import-untyped]
    except ImportError as exc:
        raise ImportError(
            "pdfplumber не установлен. Добавь 'pdfplumber>=0.10.0' в backend/pyproject.toml "
            "и выполни 'uv lock'."
        ) from exc

    import io

    text_parts: list[str] = []
    try:
        with pdfplumber.open(io.BytesIO(pdf_bytes)) as pdf:
            for page in pdf.pages:
                page_text = page.extract_text() or ""
                text_parts.append(page_text)
    except Exception as exc:
        logger.warning("extract_from_pdf: не удалось прочитать PDF: %s", exc)
        return []

    full_text = "\n".join(text_parts)
    return extract_reservations(full_text)


__all__ = ["ReservationRecord", "extract_from_pdf", "extract_reservations"]
