"""Scraper реестра КРТ-площадок Екатеринбурга (#1060, GG-форсайт).

Источник: https://xn--80acgfbsl1azdqr.xn--p1ai/дляработы/гиз/крт
(екатеринбург.рф, SSR-страница без auth и JS, обычный TLS GET).

На странице ~15 площадок по годам (2022–2025). Каждая площадка = текстовый
блок прозой с нумерованными пунктами. Граница блока — тег
<p>...<strong>N. КРТ (жилой|нежилой) застройки...</strong>...</p>.

Публичный API:
    fetch_krt_page()      -> str  (HTTP GET, сеть)
    parse_krt_page(html)  -> list[KRTSite]  (чистая функция, без сети)
"""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass
from datetime import date
from html import unescape
from typing import Any

import httpx
from bs4 import BeautifulSoup

logger = logging.getLogger(__name__)

KRT_URL = (
    "https://xn--80acgfbsl1azdqr.xn--p1ai"
    "/%D0%B4%D0%BB%D1%8F%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D1%8B"
    "/%D0%B3%D0%B8%D0%B7/%D0%BA%D1%80%D1%82"
)

_HEADERS = {
    "User-Agent": ("Mozilla/5.0 (compatible; GenDesign-SF/1.0; +https://gendsgn.ru/)"),
    "Accept-Language": "ru-RU,ru;q=0.9",
}


# -- dataclass ---------------------------------------------------------------


@dataclass
class KRTSite:
    """Одна КРТ-площадка из реестра екатеринбург.рф/гиз/крт."""

    site_name: str
    district: str | None
    krt_kind: str | None
    developer_name: str | None
    contract_number: str | None
    contract_date: date | None
    auction_start_price: float | None
    auction_final_price: float | None
    gradpotential_sqm: float | None
    area_ha: float | None
    mkd_count: int | None
    paga_decisions: list[dict[str, Any]]
    raw_block: str
    source_url: str = KRT_URL


# -- HTTP fetch ---------------------------------------------------------------


def fetch_krt_page() -> str:
    """Загружает HTML страницы реестра КРТ-площадок ЕКБ.

    Сеть отделена от парсинга для testability.
    Raises: httpx.HTTPError при недоступности сервера.
    """
    with httpx.Client(
        headers=_HEADERS,
        timeout=30,
        follow_redirects=True,
        verify=False,  # CA Минцифры РФ — нет в certifi. Аналог ekburg_permits.py #242.
    ) as client:
        resp = client.get(KRT_URL)
        resp.raise_for_status()
        logger.info(
            "ekb_krt_registry: fetched %s bytes from %s (status=%s)",
            len(resp.content),
            KRT_URL,
            resp.status_code,
        )
        return resp.text


# -- Parse helpers ------------------------------------------------------------


def _normalize_num(raw: str) -> float | None:
    """Нормализует числовую строку: убирает пробелы/NBSP, запятая -> точка."""
    cleaned = raw.replace("\xa0", "").replace(" ", "").replace(",", ".")
    cleaned = re.sub(r"[^\d.\-]", "", cleaned)
    try:
        return float(cleaned)
    except ValueError:
        return None


def _extract_kind(title: str) -> str | None:
    """Извлекает «жилой застройки» / «нежилой застройки» из названия блока."""
    m = re.search(r"((?:не)?жилой\s+застройки)", title, re.IGNORECASE)
    if m:
        return m.group(1).strip().lower()
    return None


def _extract_district(title: str) -> str | None:
    """Извлекает район/квартал из названия блока.

    Примеры:
        «в районе ул. Краснофлотцев» -> «ул. Краснофлотцев»
        «в квартале улицы Донбасской — переулков...» -> «улицы Донбасской...»
    """
    m = re.search(r"(?:в\s+районе|в\s+квартале)\s+(.+?)(?::|$)", title)
    if m:
        return m.group(1).strip().rstrip(":")
    return None


def _extract_developer(block_text: str) -> str | None:
    """Застройщик из текста вида «(застройщик — ООО «Брусника...»)»."""
    m = re.search(
        r"застройщик\s*[–—\-]\s*(.+?)\s*\)",
        block_text,
        re.IGNORECASE,
    )
    if m:
        raw = m.group(1).strip().replace("«", "").replace("»", "").strip()
        return raw or None
    return None


def _extract_contract(block_text: str) -> tuple[str | None, date | None]:
    """Номер и дата договора КРТ.

    Образец: «Заключен договор о комплексном развитии ... от 10.08.2022 N 1-2/21.7-14»

    Ищем «Заключен договор», чтобы не путать с «в соответствии с договором» (родительный
    падеж «договора») — они не содержат «Заключен» и дают ложное совпадение.
    """
    m = re.search(
        r"[Зз]аключен\s+договор[^.]*?\bот\s+(\d{2}\.\d{2}\.\d{4})\s*№\s*([\w/.\-]+)",
        block_text,
        re.IGNORECASE,
    )
    if not m:
        return None, None
    raw_date, number = m.group(1), m.group(2).strip()
    try:
        day, month, year = raw_date.split(".")
        contract_dt: date | None = date(int(year), int(month), int(day))
    except (ValueError, AttributeError):
        contract_dt = None
    return number or None, contract_dt


def _extract_auction_prices(
    block_text: str,
) -> tuple[float | None, float | None]:
    """Начальная стоимость и цена по результатам торгов аукциона.

    Образец:
        «Начальная стоимость: 641 018,64 рублей;
         Цена по результатам торгов: 671 018 рублей»
    Также встречается «Начальная цена:».
    """
    start: float | None = None
    final: float | None = None

    m_start = re.search(
        r"Начальная\s+(?:стоимость|цена)\s*:\s*([\d\s\xa0]+[,.]?\d*)\s*руб",
        block_text,
        re.IGNORECASE,
    )
    if m_start:
        start = _normalize_num(m_start.group(1))

    m_final = re.search(
        r"Цена\s+по\s+результатам\s+торгов\s*:\s*([\d\s\xa0]+[,.]?\d*)\s*руб",
        block_text,
        re.IGNORECASE,
    )
    if m_final:
        final = _normalize_num(m_final.group(1))

    return start, final


def _extract_area_ha(block_text: str) -> float | None:
    """Площадь территории в га."""
    m = re.search(
        r"[Пп]лощадь\s+территории\s*:\s*([\d\s\xa0]+[,.]?\d*)\s*га",
        block_text,
    )
    return _normalize_num(m.group(1)) if m else None


def _extract_gradpotential(block_text: str) -> float | None:
    """Градостроительный потенциал в кв. м."""
    m = re.search(
        r"[Гг]радостроительный\s+потенциал\s*:\s*([\d\s\xa0]+[,.]?\d*)\s*кв",
        block_text,
    )
    return _normalize_num(m.group(1)) if m else None


def _extract_mkd_count(block_text: str) -> int | None:
    """Общее количество МКД в блоке.

    Варианты: «Всего многоквартирных домов: N» или «Количество домов: N».
    """
    m = re.search(
        r"(?:Всего\s+многоквартирных\s+домов|Количество\s+домов)\s*:\s*(\d+)",
        block_text,
        re.IGNORECASE,
    )
    if m:
        try:
            return int(m.group(1))
        except ValueError:
            return None
    return None


_PAGA_PATTERN = re.compile(
    r"Постановление\s+Администрации\s+города\s+Екатеринбурга\s+"
    r"от\s+(\d{2}\.\d{2}\.\d{4})\s+№\s+(\d+)\s*(?:«([^»]*)»)?",
    re.IGNORECASE,
)


def _extract_paga_decisions(block_text: str) -> list[dict[str, Any]]:
    """Собирает все постановления ПАГЕ из блока в список словарей."""
    results: list[dict[str, Any]] = []
    for m in _PAGA_PATTERN.finditer(block_text):
        entry: dict[str, Any] = {
            "date": m.group(1),
            "number": m.group(2),
        }
        if m.group(3):
            entry["title"] = m.group(3).strip()
        results.append(entry)
    return results


# -- Block extractor ----------------------------------------------------------


# Граница блока — <p> с (необязательными) пробелами/&nbsp; перед <strong>,
# в котором первый текст — «N. КРТ» (не img-title). Паттерн НЕ совпадает
# с <p><strong><img ...></strong></p> (img-блоки с фото площадки).
_BLOCK_HEADING_RE = re.compile(
    r"<p[^>]*>\s*(?:&nbsp;)?\s*<strong[^>]*>(?!<img)\s*(?:&nbsp;)?\s*"
    r"(\d+\.(?:&nbsp;)?\s*КРТ(?:(?!<strong).)*?)</strong>",
    re.DOTALL,
)


def _extract_krt_section(soup: BeautifulSoup) -> str:
    """Возвращает HTML содержимого раздела КРТ.

    Ищет первый <h2> с текстом «202X год» и возвращает HTML его родительского
    контейнера (<section> или <div>) — всё содержимое раздела с площадками.
    """
    for tag in soup.find_all("h2"):
        text = tag.get_text()
        if re.search(r"202\d\s*год", text):
            parent = tag.find_parent("section") or tag.find_parent("div")
            if parent:
                return str(parent)
            full_html = str(soup)
            idx = full_html.find(str(tag))
            if idx >= 0:
                end = full_html.find("</section>", idx)
                return full_html[idx : end if end > 0 else idx + 100_000]
    return ""


def parse_krt_page(html: str) -> list[KRTSite]:
    """Парсит HTML страницы реестра КРТ-площадок ЕКБ.

    Чистая функция (без сети). Возвращает список KRTSite.
    Пустой/мусорный HTML -> [].
    """
    if not html or not html.strip():
        return []

    soup = BeautifulSoup(html, "html.parser")
    section_html = _extract_krt_section(soup)
    if not section_html:
        logger.warning("ekb_krt_registry: КРТ-раздел не найден в HTML")
        return []

    heading_matches = list(_BLOCK_HEADING_RE.finditer(section_html))
    if not heading_matches:
        logger.warning("ekb_krt_registry: не найдено ни одного блока площадки")
        return []

    sites: list[KRTSite] = []
    block_starts = [m.start() for m in heading_matches]
    block_ends = [*block_starts[1:], len(section_html)]

    for match, block_end in zip(heading_matches, block_ends, strict=True):
        block_html = section_html[match.start() : block_end]
        raw_title_html = match.group(1)
        title = unescape(re.sub(r"<[^>]+>", " ", raw_title_html))
        title = title.replace("\xa0", " ").strip().rstrip(":")

        block_text = unescape(re.sub(r"<[^>]+>", " ", block_html))
        block_text = re.sub(r"\s+", " ", block_text).strip()

        developer = _extract_developer(block_text)
        contract_number, contract_date = _extract_contract(block_text)
        start_price, final_price = _extract_auction_prices(block_text)
        area_ha = _extract_area_ha(block_text)
        gradpotential = _extract_gradpotential(block_text)
        mkd_count = _extract_mkd_count(block_text)
        paga = _extract_paga_decisions(block_text)
        krt_kind = _extract_kind(title)
        district = _extract_district(title)

        site = KRTSite(
            site_name=title.strip(),
            district=district,
            krt_kind=krt_kind,
            developer_name=developer,
            contract_number=contract_number,
            contract_date=contract_date,
            auction_start_price=start_price,
            auction_final_price=final_price,
            gradpotential_sqm=gradpotential,
            area_ha=area_ha,
            mkd_count=mkd_count,
            paga_decisions=paga,
            raw_block=block_text[:4000],
        )
        sites.append(site)
        logger.debug(
            "ekb_krt_registry: parsed '%s' developer=%s contract=%s",
            title[:60],
            developer,
            contract_number,
        )

    logger.info("ekb_krt_registry: parsed %d sites", len(sites))
    return sites
