#!/usr/bin/env python3
"""Сбор заголовков и видимых ссылок со страниц из маршруты/адреса-страниц.json.

Запуск:
    python3 сборка/веб-данные-страниц.py

Читает список адресов, загружает каждую страницу по HTTP, извлекает:
  * заголовок страницы — содержимое первого тега <title>;
  * видимые ссылки — теги <a href="..."> с непустым видимым текстом;
  * число ссылок — длину списка видимых ссылок.

Результат: результаты/веб-данные.json
"""

from __future__ import annotations

import json
import re
import unicodedata
import urllib.error
import urllib.request
from datetime import datetime, timezone
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urljoin

KOREN = Path(__file__).resolve().parent.parent
ADRESA_FAJL = KOREN / "маршруты" / "адреса-страниц.json"
REZULTAT_FAJL = KOREN / "результаты" / "веб-данные.json"

OPISANIE_SPOSOBA = (
    "Страница загружается по исходному адресу методом GET (urllib, User-Agent Mozilla/5.0), "
    "учитываются серверные перенаправления: источником записи указан исходный адрес, "
    "дополнительно сохраняется конечный адрес после перенаправлений. "
    "Заголовок страницы — текст первого тега <title>, пробелы нормализованы. "
    "Видимой ссылкой считается тег <a>, у которого есть непустой атрибут href и непустой "
    "видимый текст (текстовые узлы внутри <a>, включая вложенные строчные элементы); "
    "пробелы в тексте нормализованы, текст приведён к Unicode NFC. "
    "Не учитываются: теги <a> без href и с пустым href=\"\"; теги внутри <script>, <style> "
    "и <template>; текст <script>/<style> внутри ссылки. Относительные адреса разрешаются "
    "в абсолютные относительно конечного адреса (urljoin). "
    "Число ссылок — количество записей в списке видимых ссылок; в поле "
    "\"ссылок_в_разметке\" приведено общее число тегов <a> с непустым href до фильтра по тексту."
)


class RazborshhikStranicy(HTMLParser):
    """Собирает <title> и видимые ссылки, пропуская script/style/template."""

    NESODERZHIMYE = {"script", "style", "template"}

    def __init__(self) -> None:
        super().__init__(convert_charrefs=True)
        self.zagolovok: str | None = None
        self.ssylki: list[dict[str, str]] = []
        self.ssylok_v_razmetke = 0
        self._glubina_title = 0
        self._teksty_title: list[str] = []
        self._glubina_skrytyh = 0
        self._ssylka: dict | None = None
        self._teksty_ssylki: list[str] = []
        self._glubina_vlozhennosti_ssylki = 0

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        if tag in self.NESODERZHIMYE:
            self._glubina_skrytyh += 1
            return
        if self._glubina_skrytyh:
            return
        if tag == "title":
            self._glubina_title += 1
            return
        if tag == "a":
            if self._ssylka is not None:
                self._glubina_vlozhennosti_ssylki += 1
                return
            atributy = {imya: (znachenie or "") for imya, znachenie in attrs}
            href = atributy.get("href", "")
            if href:
                self.ssylok_v_razmetke += 1
                self._ssylka = {"href": href}
                self._teksty_ssylki = []

    def handle_endtag(self, tag: str) -> None:
        if tag in self.NESODERZHIMYE:
            if self._glubina_skrytyh:
                self._glubina_skrytyh -= 1
            return
        if self._glubina_skrytyh:
            return
        if tag == "title":
            if self._glubina_title:
                self._glubina_title -= 1
            return
        if tag == "a" and self._ssylka is not None:
            if self._glubina_vlozhennosti_ssylki:
                self._glubina_vlozhennosti_ssylki -= 1
                return
            tekst = normalizovat(" ".join(self._teksty_ssylki))
            if tekst:
                self._ssylka["tekst"] = tekst
                self.ssylki.append(self._ssylka)
            self._ssylka = None
            self._teksty_ssylki = []

    def handle_data(self, data: str) -> None:
        if self._glubina_skrytyh:
            return
        if self._glubina_title:
            self._teksty_title.append(data)
            return
        if self._ssylka is not None:
            self._teksty_ssylki.append(data)

    def rezultat(self) -> tuple[str, list[dict[str, str]], int]:
        zagolovok = normalizovat(" ".join(self._teksty_title))
        return zagolovok, self.ssylki, self.ssylok_v_razmetke


def normalizovat(tekst: str) -> str:
    """Схлопывает пробельные последовательности и приводит текст к Unicode NFC."""
    return unicodedata.normalize("NFC", re.sub(r"\s+", " ", tekst)).strip()


def zagruzit(url: str) -> tuple[str, str, bytes, str]:
    """Возвращает (исходный адрес, конечный адрес, тело ответа, кодировку)."""
    zapros = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    with urllib.request.urlopen(zapros, timeout=30) as otvet:
        konechnyj = otvet.geturl()
        kodirovka = otvet.headers.get_content_charset() or "utf-8"
        return url, konechnyj, otvet.read(), kodirovka


def obrabotat(url: str) -> dict:
    _, konechnyj, telo, kodirovka = zagruzit(url)
    html = telo.decode(kodirovka, errors="replace")
    parser = RazborshhikStranicy()
    parser.feed(html)
    parser.close()
    zagolovok, ssylki, ssylok_v_razmetke = parser.rezultat()

    vidimye = [
        {"текст": element["tekst"], "адрес": urljoin(konechnyj, element["href"])}
        for element in ssylki
    ]
    return {
        "источник": url,
        "конечный_адрес": konechnyj,
        "дата_получения": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"),
        "заголовок": zagolovok,
        "число_ссылок": len(vidimye),
        "ссылок_в_разметке": ssylok_v_razmetke,
        "видимые_ссылки": vidimye,
    }


def main() -> None:
    adresa = json.loads(ADRESA_FAJL.read_text(encoding="utf-8"))
    stranicy: list[dict] = []
    ne_polucheno: list[dict[str, str]] = []

    for zapis in adresa:
        url = zapis["url"]
        try:
            stranicy.append(obrabotat(url))
            print(f"ок: {url} — ссылок: {stranicy[-1]['число_ссылок']}")
        except (urllib.error.URLError, UnicodeError, OSError) as oshibka:
            ne_polucheno.append(
                {
                    "источник": url,
                    "ошибка": f"{type(oshibka).__name__}: {oshibka}",
                    "дата_получения": datetime.now(timezone.utc)
                    .astimezone()
                    .isoformat(timespec="seconds"),
                }
            )
            print(f"ошибка: {url} — {oshibka}")

    rezultat = {
        "источник": "маршруты/адреса-страниц.json",
        "дата_получения": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"),
        "способ_подсчёта": OPISANIE_SPOSOBA,
        "получено": len(stranicy),
        "не_получено": len(ne_polucheno),
        "страницы": stranicy,
        "не_полученные_адреса": ne_polucheno,
    }
    REZULTAT_FAJL.write_text(
        json.dumps(rezultat, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
    )
    print(f"записано: {REZULTAT_FAJL}")


if __name__ == "__main__":
    main()
