#!/usr/bin/env python3
"""Воронка SEO для обзора: от опубликованной статьи до перехода из поиска.

Собрана по образцу PR Factory. Смысл: три карточки и строка под ними, где
видно, на какой ступени теряется результат. Статью можно написать, но не
попасть в индекс; попасть в индекс, но не попасть в топ; попасть в топ,
но не получить переходов. Пока ступени стоят порознь, узкое место незаметно.

Ничего не считаем заново - складываем то, что уже выгружено:
  статей портала      - реестр текстов
  в индексе Яндекса   - KPI из выгрузки Вебмастера
  запросов в топ-10   - оттуда же
  переходов из поиска - Метрика, источник Search engine traffic за месяц
  визиты и посетители - Метрика, блок посещаемости

Порядок: после выгрузок Вебмастера и Метрики, после обновить-сводку-seo.py,
до обновить-фолбэк.py.
Запуск: python3 панель/данные/обновить-воронку-seo.py
"""

import json
from pathlib import Path

ПАПКА = Path(__file__).resolve().parent
КОРЕНЬ = ПАПКА.parent.parent
ОТЧЁТ = ПАПКА / "report.json"
РЕЕСТР = КОРЕНЬ / "завод" / "реестр-текстов.json"
ИСТОРИЯ = КОРЕНЬ / "завод" / "вебмастер" / "история"


def склонение(н, один, два, много):
    о, е = abs(н) % 100, abs(н) % 10
    if 11 <= о <= 14:
        return много
    return один if е == 1 else два if 2 <= е <= 4 else много


def статей_портала():
    if not РЕЕСТР.exists():
        return None
    д = json.loads(РЕЕСТР.read_text(encoding="utf-8"))
    n = 0
    for т in д["тексты"]:
        if т.get("вид") == "внешняя площадка":
            continue
        if "опубликован" in str(т.get("статус", "")).lower():
            n += 1
    return n or None


def статей_в_индексе():
    """Сколько статей портала реально в поиске.

    Считаем по списку адресов из снимка Вебмастера, а не по сводному числу
    searchable_pages_count: оно обновляется с задержкой (23.09.2026 сводка
    отдавала 9, а список - 11) и считает все страницы подряд, включая главную,
    «Об авторе» и ленту статей. В карточке написано «статей», значит и считать
    надо статьи (23.09.2026).
    """
    снимки = sorted(ИСТОРИЯ.glob("*.json")) if ИСТОРИЯ.exists() else []
    for путь in reversed(снимки):
        try:
            д = json.loads(путь.read_text(encoding="utf-8"))
        except (OSError, ValueError):
            continue
        адреса = д.get("адреса_в_поиске")
        if адреса is not None:
            статей = [а for а in адреса if "/statya/" in а]
            return len(статей), len(адреса)
    return None, None


def kpi(seo, метка):
    for к in seo.get("kpi", []):
        if к.get("метка") == метка:
            зн = к.get("значение")
            return None if зн is None else int(зн)
    return None


def из_поиска(seo):
    """Визиты из поисковых систем за месяц. None - Метрика не отвечала."""
    м = seo.get("метрика") or {}
    for и in м.get("источники_месяц") or []:
        if "Search" in str(и.get("источник")):
            return и.get("визиты")
    return 0 if м.get("источники_месяц") else None


def главная():
    отчёт = json.loads(ОТЧЁТ.read_text(encoding="utf-8"))
    seo = отчёт.setdefault("seo", {})
    пос = seo.get("посещаемость") or {}

    статей = статей_портала()
    индекс, страниц_в_индексе = статей_в_индексе()
    if индекс is None:            # снимка со списком ещё нет
        индекс = kpi(seo, "страниц в индексе")
        страниц_в_индексе = индекс
    топ10 = kpi(seo, "запросов в топ-10")
    показов = kpi(seo, "показов в выдаче за две недели")
    поиск = из_поиска(seo)

    # Узкое место - первая ступень, где теряется больше всего. Пишем словами,
    # потому что число само по себе не говорит, что с ним делать.
    узкое = None
    if статей and индекс is not None and индекс < статей:
        не_в_индексе = статей - индекс
        узкое = (f"{не_в_индексе} {склонение(не_в_индексе, 'статья', 'статьи', 'статей')} "
                 f"{склонение(не_в_индексе, 'не в индексе', 'не в индексе', 'не в индексе')} "
                 "Яндекса - до поиска они не дошли вовсе. Отправляются на переобход "
                 "по суточной квоте.")
    elif индекс and not топ10:
        узкое = ("в индексе есть, а в топ-10 нет ни одного запроса: страницы "
                 "показываются, но ниже десятого места.")

    ступени = [
        {"метка": склонение(статей or 0, "статья опубликована", "статьи опубликованы",
                            "статей опубликовано"), "значение": статей},
        {"метка": "в индексе Яндекса", "значение": индекс},
        {"метка": "запросов в топ-10", "значение": топ10},
        {"метка": "переходов из поиска", "значение": поиск},
    ]

    seo["воронка"] = {
        "заголовок": "От статьи до перехода из поиска",
        "ступени": ступени,
        "узкое_место": узкое,
        "карточки": [
            {"метка": "Визитов за месяц", "значение": пос.get("визиты"),
             "подпись": (f"{пос.get('посетители')} "
                         f"{склонение(пос.get('посетители') or 0, 'посетитель', 'посетителя', 'посетителей')}"
                         " · Яндекс Метрика, с 01.09.2026")},
            {"метка": "Запросов в топ-10 Яндекса", "значение": топ10,
             "подпись": (f"показов за две недели {показов}" if показов is not None
                         else "Вебмастер ещё не отдал показы")},
            {"метка": "Статей в индексе",
             "значение": индекс,
             "подпись": ((f"из {статей} опубликованных на портале" if статей
                          else "число статей портала не посчитано")
                         + (f". Всего страниц в поиске {страниц_в_индексе}: "
                            "плюс главная, «Об авторе» и лента статей"
                            if страниц_в_индексе and страниц_в_индексе != индекс else ""))},
        ],
        "google": seo.get("google") or {"статус": "не подключено"},
        "как_считается": (
            "Статьи - по реестру текстов, всё, что помечено опубликованным. "
            "Индекс и топ-10 - выгрузка Яндекс Вебмастера, она ходит в 05:30 "
            "и отдаёт скользящее окно в две недели. Переходы из поиска - "
            "Яндекс Метрика, источник «поисковые системы» за месяц; свои "
            "заходы она не отделяет, поэтому число скорее завышено. "
            "Google Search Console подключён 24.09.2026, его цифры идут "
            "отдельной строкой: у поисковиков разные индексы и разные окна "
            "данных (Яндекс отдаёт две недели, Google - шестнадцать месяцев), "
            "складывать их нельзя."),
    }
    ОТЧЁТ.write_text(json.dumps(отчёт, ensure_ascii=False, indent=1) + "\n",
                     encoding="utf-8")
    print("воронка SEO: " + " → ".join(
        f"{с['метка']} {с['значение'] if с['значение'] is not None else '?'}"
        for с in ступени))
    return 0


if __name__ == "__main__":
    raise SystemExit(главная())
