#!/usr/bin/env python3
"""Свести контент-план с тем, что реально написано и вышло.

Очередь волны и баланс типов набивались руками 28.08.2026, а потом статьи
выходили, и план остался на цифрах первой недели: в панели пятая статья,
на портале десятая. Скрипт закрывает разрыв в одну сторону, из реестра
текстов в план, и трогает только то, что вычисляется: статус позиции,
счётчики баланса, ноту под ним и полосы недели.

Темы, которых в плане нет вовсе (вышли вне очереди волны), дописываются
в конец очереди с неделей «вне плана»: иначе опубликованная статья не
видна в разделе, где её ищут.

Запуск: python3 панель/данные/обновить-контент-план.py
Перед запуском: python3 завод/реестр-текстов.py
"""

import json
import re
from pathlib import Path

ДАННЫЕ = Path(__file__).resolve().parent
КОРЕНЬ = ДАННЫЕ.parent.parent
ПЛАН = ДАННЫЕ / "contentplan.json"
РЕЕСТР = КОРЕНЬ / "завод" / "реестр-текстов.json"
ПЛОЩАДКИ = КОРЕНЬ / "завод" / "площадки.json"

# площадка считается заведённой по началу статуса: «заведено» сидит подстрокой
# внутри «не заведено», и поиск по вхождению затаскивает в счёт пустые строки
НАЧАЛО_РАБОТЫ = ("работает", "заведено", "аккаунт", "регистрация начата",
                 "отправлено", "домен подключён")


def опубликованные_кластеры(реестр):
    """Кластер → заголовок для всего, что вышло на портале."""
    return {т["кластер"]: т["заголовок"] for т in реестр["тексты"]
            if "опубликовано на портале" in т["статус"].lower() and т.get("кластер")}


def заведено_аккаунтов():
    if not ПЛОЩАДКИ.exists():
        return None
    п = json.loads(ПЛОЩАДКИ.read_text(encoding="utf-8"))["площадки"]
    пл = list(п.values()) if isinstance(п, dict) else п
    счёт = 0
    for площадка in пл:
        низ = (площадка.get("статус") or "").lower()
        if низ.startswith("не заведено") or низ.startswith("не для авторов"):
            continue
        if низ.startswith(НАЧАЛО_РАБОТЫ):
            счёт += 1
    return счёт


def главная():
    план = json.loads(ПЛАН.read_text(encoding="utf-8"))
    реестр = json.loads(РЕЕСТР.read_text(encoding="utf-8"))
    вышли = опубликованные_кластеры(реестр)
    очередь = план["очередь"]
    сообщения = []

    # 1. статусы позиций
    for поз in очередь:
        if поз["кластер"] in вышли and поз["статус"] != "Опубликована":
            сообщения.append("%s %s: %s → Опубликована"
                             % (поз["кластер"], поз["тема"][:34], поз["статус"]))
            поз["статус"] = "Опубликована"

    # 2. вышедшее вне очереди волны
    в_плане = {поз["кластер"] for поз in очередь}
    for кластер, заголовок in вышли.items():
        if кластер in в_плане:
            continue
        очередь.append({"позиция": max(п["позиция"] for п in очередь) + 1,
                        "кластер": кластер, "тема": заголовок,
                        "контур": "спутник · разбор", "тип": "разбор",
                        "приоритет": "P1", "объём": "-", "неделя": "вне плана",
                        "волна": "П1", "статус": "Опубликована", "демо": False})
        сообщения.append("%s %s: дописана в очередь (вне плана)" % (кластер, заголовок[:34]))

    # 3. статусы недель волны. Считаем по позициям недели, а не помним: до
    # 14.09.2026 недели стояли «Не начата» с момента составления плана, хотя
    # статьи по ним давно вышли (замечено руководителем). Неделя закрыта,
    # когда опубликованы все её позиции, и в работе - когда хотя бы одна.
    for неделя in план.get("недели", []):
        # «Закрыта» - решение человека, а не итог подсчёта: так помечена
        # неделя 0, где каталог отложен руководителем. Пересчёт её не трогает.
        if (неделя.get("статус") or "").lower().startswith("закрыт"):
            continue
        код = str(неделя.get("неделя"))
        свои = [п for п in очередь if str(п.get("неделя")) == код]
        if not свои:
            # Очередь перешла с номеров недель на конкретные даты выхода,
            # поэтому по полю «неделя» находятся не все. Тогда ищем позиции
            # по кодам кластеров - они перечислены в описании недели
            # («А6 топ франшиз · А20 как отличить…»). Замечено 14.09.2026.
            коды = set(re.findall(r"\b([АБМ]\d{1,2})\b", неделя.get("что") or ""))
            свои = [п for п in очередь if п.get("кластер") in коды]
        if not свои:
            continue
        готово = sum(1 for п in свои if п["статус"] == "Опубликована")
        было = неделя.get("статус")
        стало = ("Опубликована" if готово == len(свои)
                 else "В работе" if готово else "Не начата")
        неделя["статей"] = len(свои)
        неделя["опубликовано"] = готово
        if стало != было:
            неделя["статус"] = стало
            сообщения.append("неделя %s: %s → %s (%d из %d)"
                             % (код, было, стало, готово, len(свои)))

    # 4. баланс типов: итого и опубликовано считаем по очереди, а не помним
    for строка in план["баланс"]["строки"]:
        свои = [п for п in очередь if п["тип"] == строка["тип"]]
        строка["итого"] = len(свои)
        строка["опубликовано"] = sum(1 for п in свои if п["статус"] == "Опубликована")
    типы = {с["тип"] for с in план["баланс"]["строки"]}
    for поз in очередь:
        if поз["тип"] not in типы:
            план["баланс"]["строки"].append(
                {"тип": поз["тип"], "итого": sum(1 for п in очередь if п["тип"] == поз["тип"]),
                 "опубликовано": sum(1 for п in очередь
                                     if п["тип"] == поз["тип"] and п["статус"] == "Опубликована"),
                 "Франшизы": 0, "Бизнес": 0, "Мосты": 0})
            типы.add(поз["тип"])

    коды = sorted(к for к, поз in ((п["кластер"], п) for п in очередь)
                  if поз["статус"] == "Опубликована")
    вне = sorted(п["кластер"] for п in очередь
                 if п["статус"] == "Опубликована" and str(п["неделя"]) == "вне плана")
    план["баланс"]["нота"] = (
        "Опубликовано %d %s: %s.%s"
        % (len(коды), "статья" if len(коды) == 1 else "статей", ", ".join(коды),
           (" Вне очереди волны вышли %s, поэтому в разбивке по неделям их нет."
            % ", ".join(вне)) if вне else ""))

    # 4. полосы недели
    аккаунтов = заведено_аккаунтов()
    for полоса in план["полосы"]:
        если_имя = полоса["название"]
        if если_имя == "Публикации по плану":
            полоса["факт"] = len(коды)
            полоса["план"] = len(очередь)
            полоса["комментарий"] = ("вышло %d позиций плана из %d; счёт идёт по кластерам, "
                                     "а не по неделям" % (len(коды), len(очередь)))
        elif если_имя == "Аккаунты на площадках" and аккаунтов is not None:
            полоса["факт"] = аккаунтов
            полоса["план"] = max(полоса.get("план", 0), аккаунтов)
            полоса["комментарий"] = ("считается из завод/площадки.json: аккаунт заведён "
                                     "или работа с площадкой начата")
        elif если_имя == "Брифы готовы":
            # брифы отдельным этапом мы не ведём: тема из «Запланирована» уходит
            # сразу в текст. Полоса показывает то, что действительно движется
            в_работе = sum(1 for п in очередь if п["статус"] not in ("Опубликована", "Отложена"))
            полоса["название"] = "Темы в работе"
            полоса["факт"] = в_работе
            полоса["план"] = len(очередь)
            полоса["единица"] = "темы"
            полоса["комментарий"] = "очередь без опубликованных и отложенных позиций"

    ПЛАН.write_text(json.dumps(план, ensure_ascii=False, indent=1), encoding="utf-8")
    print("позиций в очереди:", len(очередь), "| опубликовано:", len(коды),
          "| аккаунтов:", аккаунтов)
    for с in сообщения:
        print("  ", с)


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