#!/usr/bin/env python3
"""Очередь ручной публикации и история завершённых - из реестра текстов.

Зачем. Вкладка стояла пустой, и это выглядело как «нам нечего публиковать
руками». На деле наоборот: руками у нас уходит почти всё, а файл
`панель/данные/manualqueue.json` никто не вёл. Он был написан руками
27.08.2026, все его строки помечены демо и после снятия демо-режима
16.09.2026 перестали показываться вовсе.

Что кладём:
  очередь  - тексты для внешних площадок, которые ещё не вышли: готовые
             к отправке и отправленные, по которым ждём ответа редакции;
  история  - то, что уже опубликовано руками, со ссылкой и датой.

Причина ручной публикации берётся из `завод/площадки.json`, поле
«автопостинг»: панель не гадает, почему материал не ушёл сам.

Порядок: после обновить-статьи.py, до обновить-фолбэк.py.
Запуск: python3 панель/данные/обновить-очередь-ручную.py [--сухой]
"""

import json
import re
import sys
from pathlib import Path

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

# Площадки, куда мы публикуем сами скриптом с сервера. В базе площадок у них
# стоит «автопостинг: нет» - и это правда про официальное API, но неправда про
# нашу работу: форму заполняет скрипт. Из-за этого панель писала «нет
# автопостинга» про Spark, куда с сервера ушли два материала (17.09.2026).
# Источник правды - сами файлы: убрали скрипт, площадка вернулась в ручные.
СКРИПТЫ_ПУБЛИКАЦИИ = {
    "Spark": "опубликовать-spark.py",
    "Smart-Lab": "опубликовать-smartlab.py",
    "Работаем!": "опубликовать-rabotaem.py",
    "LiveJournal": "публикация-lj.py",
}

ЖДЁМ = ("отправлено", "модерац", "ждём ответа", "на проверке", "премодерац")
ГОТОВ = ("черновик", "готов", "к публикации")


def дата(текст):
    м = re.search(r"(\d{2})\.(\d{2})\.(\d{4})", текст or "")
    return f"{м.group(3)}-{м.group(2)}-{м.group(1)}" if м else None


def имя_базы(площадка):
    return (площадка or "").split("(")[0].strip()


def ключ(имя):
    """Имя площадки для сверки: без доменного хвоста, регистра и знаков.

    17.09.2026 панель писала «не заведены в базе: E-xecutive.ru», хотя профиль
    заведён 11.09 и статья лежала на модерации. Вся разница была в хвосте
    «.ru»: в реестре текстов человек написал с доменом, в базе площадок без.
    Имя в реестре поправлен, но сверку заодно делаем устойчивой - иначе
    та же история повторится на первом же «vc.ru» или «Pikabu.ru».
    """
    н = имя_базы(имя).lower().replace("ё", "е")
    for хвост in (".ru", ".com", ".net", ".app", ".io"):
        if н.endswith(хвост):
            н = н[: -len(хвост)]
    return "".join(с for с in н if с.isalnum())


def автопостинг():
    """Имя площадки -> чем публикуем и почему не сама."""
    if not ПЛОЩАДКИ.exists():
        return {}
    б = json.loads(ПЛОЩАДКИ.read_text(encoding="utf-8"))
    сп = б.get("площадки") if isinstance(б, dict) else б
    сп = сп.values() if isinstance(сп, dict) else сп
    итог = {}
    for п in сп:
        итог[ключ(п.get("имя"))] = {
            "автопостинг": п.get("автопостинг"),
            "как": п.get("как_публикуем") or "",
        }
    return итог


def уходит_скриптом():
    """Имя площадки -> файл скрипта, если он на месте."""
    папка = КОРЕНЬ / "завод" / "публикации"
    return {и: ф for и, ф in СКРИПТЫ_ПУБЛИКАЦИИ.items() if (папка / ф).exists()}


def причина(площадка, база):
    """Почему материал не ушёл с сервера. Не знаем - так и пишем."""
    свои = {ключ(и): ф for и, ф in уходит_скриптом().items()}
    имя = имя_базы(площадка)
    if ключ(имя) in свои:
        return "уходит скриптом с сервера", f"публикует завод/публикации/{свои[ключ(имя)]}"
    п = база.get(ключ(имя))
    if not п:
        return "нет в базе площадок", ""
    авто = str(п.get("автопостинг") or "").strip().lower()
    если_нет = авто in ("", "нет", "нельзя", "не умеет", "-", "none")
    return ("нет автопостинга" if если_нет else "публикуем руками"), п.get("как", "")


def главная():
    сухой = "--сухой" in sys.argv
    р = json.loads(РЕЕСТР.read_text(encoding="utf-8"))
    база = автопостинг()
    д = json.loads(ОЧЕРЕДЬ.read_text(encoding="utf-8"))

    очередь, история = [], []
    for т in р["тексты"]:
        if т.get("вид") != "внешняя площадка":
            continue
        статус = str(т.get("статус") or "")
        низ = статус.lower()
        общее = {
            "статья": т.get("заголовок") or "",
            "кластер": т.get("кластер") or т.get("код") or "",
            "площадка": имя_базы(т.get("площадка")),
            "когда": дата(статус) or "",
            # Файл текста и черновик на витрине - для кнопки «Открыть»: без них
            # у неопубликованного материала кнопке некуда вести (30.09.2026).
            "файл": т.get("файл") or "",
            "черновик": ("https://smm.prfabrika.ru/" + т["на_витрине"][0]) if т.get("на_витрине") else None,
        }
        if "опубликовано" in низ and "не видно читателям" not in низ:
            история.append({**общее, "№": len(история) + 1,
                            "результат": "Опубликовано",
                            "ссылка": т.get("ссылка") or None,
                            "комментарий": статус[:160],
                            "демо": False})
            continue
        if "отклон" in низ:
            история.append({**общее, "№": len(история) + 1,
                            "результат": "Отклонено", "ссылка": None,
                            "комментарий": статус[:160], "демо": False})
            continue
        ждём = any(с in низ for с in ЖДЁМ)
        готов = any(с in низ for с in ГОТОВ)
        if не_нужен(низ) or not (ждём or готов):
            continue
        поч, как = причина(т.get("площадка"), база)
        очередь.append({**общее, "№": len(очередь) + 1,
                        "причина": поч,
                        "комментарий": как or статус[:120],
                        "статус": "Ждём ответа редакции" if ждём else "Готов к отправке",
                        "ссылка": т.get("ссылка") or None,
                        "демо": False})

    # свежее сверху: оператору важно последнее, а не первое
    очередь.sort(key=lambda о: (о["статус"] != "Готов к отправке", о["когда"] or ""), reverse=False)
    история.sort(key=lambda о: о["когда"] or "", reverse=True)
    for и, о in enumerate(очередь, 1):
        о["№"] = и
    for и, о in enumerate(история, 1):
        о["№"] = и

    ждут = sum(1 for о in очередь if о["статус"] == "Ждём ответа редакции")
    вышло = sum(1 for о in история if о["результат"] == "Опубликовано")
    д["очередь"], д["история"] = очередь, история
    # Вкладка вернулась в панель, а поле осталось и уверяло, что её сняли.
    д.pop("вкладка_снята", None)
    д.get("мета", {}).pop("вкладка_снята", None)
    д.setdefault("мета", {})["состояние_очереди"] = (
        f"в очереди {len(очередь)}: готовых к отправке {len(очередь) - ждут}, "
        f"ждут ответа редакции {ждут}. Опубликовано на внешних площадках {вышло}")
    д["мета"]["источник"] = ("завод/реестр-текстов.json (что в работе) и "
                             "завод/площадки.json (почему не уходит само)")
    # Пояснение раздела было написано словами 27.08.2026 и устарело дважды:
    # вкладку вернули 14.09, а с сервера мы с тех пор публикуем в три площадки.
    # Пояснение собираем из чисел, а не из слов: прежний текст был написан
    # руками 27.08.2026 и к 17.09 врал трижды - обещал шесть площадок,
    # называл строки придуманными и уверял, что вкладку сняли.
    свои = уходит_скриптом()
    площадки_очереди = sorted({о["площадка"] for о in очередь})
    без_базы = sorted({о["площадка"] for о in очередь
                       if о["причина"] == "нет в базе площадок"})
    руками = sorted({о["площадка"] for о in история
                     if о["результат"] == "Опубликовано"
                     and ключ(о["площадка"]) not in {ключ(и) for и in свои}})
    д["мета"]["пояснение"] = (
        f"Что ждёт отправки на внешние площадки и что уже ушло. В очереди "
        f"{len(очередь)} материалов на {len(площадки_очереди)} площадок. "
        f"Скриптом с сервера публикуем {len(свои)}: {', '.join(sorted(свои))} - "
        "официального API у них нет, форму заполняет скрипт. "
        f"Руками уже публиковали на {len(руками)} площадках: у части нет API "
        "вовсе, у vc.ru токен выдают не всем, у TenChat и Дзена нет публичного "
        "API публикации, Pikabu закрыт антиботом. Причина по каждой строке "
        "берётся из базы площадок и из наличия скрипта публикации."
        + (f" Не заведены в базе площадок: {', '.join(без_базы)}." if без_базы else ""))

    if сухой:
        print(f"сухой прогон: очередь {len(очередь)}, история {len(история)}")
        return 0
    ОЧЕРЕДЬ.write_text(json.dumps(д, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
    print(f"очередь: {len(очередь)} (ждут ответа {ждут}), история: {len(история)} "
          f"(опубликовано {вышло})")
    return 0


def не_нужен(низ):
    """Материалы, которые оператору передавать не надо."""
    return "не публиковать" in низ or "снят" in низ


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