#!/usr/bin/env python3
"""Пересобрать PR-срез сводного отчёта из реестра текстов и базы площадок.

Зачем. Раздел «Сводный отчёт» показывал состояние внешнего контура руками:
цифры вписывались в report.json один раз и отставали на несколько дней.
07.09.2026 панель говорила «две публикации на внешних площадках», когда их
было семь, а Smart-Lab, Audit-it и LiveJournal в таблице отсутствовали вовсе.

Источники правды:
    завод/реестр-текстов.json - сколько материалов вышло и на какой площадке
    завод/площадки.json       - заведён ли аккаунт, что с ним не так, dofollow

Что скрипт трогает: у каждой площадки поля «опубликовано», «статус»,
«ограничение», «ссылки», плюс KPI и строку состояния. Демо-поля (всё,
что с суффиксом _демо) не трогает: они нарисованные и показывают, как
раздел выглядит с данными за четыре месяца.

Порядок: после завод/реестр-текстов.py, до обновить-фолбэк.py.

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

import json
import re
from collections import Counter
from pathlib import Path

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


def ручные_просмотры():
    """Просмотры на площадках из таблички ручного ввода `просмотры-площадок.csv`.

    Возвращает {имя площадки: (просмотры, дата)}. Пустое поле просмотров -
    площадка ещё не мерилась, её не добавляем: пустое честнее нуля.
    Числа площадки отдают только в своих кабинетах (API почти нет, Дзен -
    руками раз в неделю), поэтому их вписывают руками сюда.
    """
    из_файла = {}
    if not ПРОСМОТРЫ_CSV.exists():
        return из_файла
    for строка in ПРОСМОТРЫ_CSV.read_text(encoding="utf-8").splitlines():
        строка = строка.strip()
        if not строка or строка.startswith("#") or строка.startswith("площадка;"):
            continue
        части = [ч.strip() for ч in строка.split(";")]
        имя = части[0]
        число = части[1] if len(части) > 1 else ""
        дата = части[2] if len(части) > 2 else ""
        if not имя or not число:
            continue
        try:
            из_файла[имя] = (int(число.replace(" ", "")), дата or None)
        except ValueError:
            print(f"  просмотры: не число у «{имя}»: {число!r}, пропускаю")
    return из_файла

# в реестре площадка пишется так, как её называет шапка материала,
# в базе - так, как она называет себя сама
СИНОНИМЫ = {"audit-it.ru": "Audit-it", "смарт-лаб": "Smart-Lab",
            "живой журнал": "LiveJournal", "вс.ру": "vc.ru"}

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


def имя_базы(площадка):
    return СИНОНИМЫ.get(площадка.lower().strip(), площадка.strip())


def короткий_статус(строка):
    """Первое слово статуса из базы: «работает», «заведено», «отложено»."""
    низ = строка.lower()
    if низ.startswith("работает") or "опубликован" in низ[:40]:
        return "работает"
    if "не заведено" in низ:
        return "не заведено"
    if "отложено" in низ:
        return "отложено"
    return "заведено"


def причина(площадка):
    """Одна строка о том, что мешает: её читает оператор, а не машина.

    Из статуса срезается только начальное слово-метка («работает - …»):
    оно уже стоит в своей колонке. Резать по первому дефису нельзя,
    в статусах живут адреса публикаций, и smart-lab.ru рвётся пополам.
    """
    статус = (площадка.get("статус") or "").strip()
    хвост = re.sub(r"^(работает|заведено|отложено|отправлено|отклонено)\s*[-\u2013\u2014:]\s*",
                   "", статус, flags=re.IGNORECASE)
    хвост = хвост.strip(" .")
    if len(хвост) <= 200:
        return хвост
    # режем по границе слова: иначе обрывается адрес публикации,
    # а он в этой колонке единственная кликабельная зацепка
    return хвост[:200].rsplit(" ", 1)[0] + "..."


def ссылки(площадка):
    """Передаёт ли площадка вес.

    Три состояния, а не два: «не проверено» это честный ответ, а поставить
    там nofollow значит соврать в отчёте. Метку ставим только после ручной
    проверки на своей публикации, и она записана в поле «ссылки_seo».
    """
    текст = (площадка.get("ссылки_seo") or "").lower()
    if not текст:
        return "не проверено"
    if "dofollow" in текст:
        return "dofollow"
    if "nofollow" in текст:
        return "nofollow"
    return "не проверено"


def главная():
    реестр = json.loads(РЕЕСТР.read_text(encoding="utf-8"))
    база = {п["имя"]: п for п in json.loads(ПЛОЩАДКИ.read_text(encoding="utf-8"))["площадки"]}
    отчёт = json.loads(ОТЧЁТ.read_text(encoding="utf-8"))
    pr = отчёт["pr"]
    просмотры_ручные = ручные_просмотры()

    вышло = Counter(имя_базы(т["площадка"]) for т in реестр["тексты"]
                    if т["вид"] == "внешняя площадка" and "опубликовано" in т["статус"])

    # кого показываем: где есть публикации плюс где заведён аккаунт
    показываем = set(вышло)
    for имя, п in база.items():
        начало = (п.get("статус") or "").lower()
        if начало.startswith(НАЧАЛО_РАБОТЫ) and имя != "Портал frantlas.ru":
            показываем.add(имя)

    строки = {с["площадка"]: с for с in pr["площадки"]}
    образец = {к: (0 if isinstance(v, int) else None) for к, v in pr["площадки"][0].items()}
    добавлено, изменено = [], []

    for имя in sorted(показываем, key=lambda и: (-вышло[и], и)):
        п = база.get(имя, {})
        строка = строки.get(имя)
        if строка is None:
            строка = dict(образец)
            строка.update({к: v for к, v in pr["площадки"][0].items() if к.endswith("_демо")})
            строка.update({"площадка": имя, "демо": False, "просмотры": None,
                           "со_ссылкой": 0})
            pr["площадки"].append(строка)
            строки[имя] = строка
            добавлено.append(имя)

        было = (строка.get("опубликовано"), строка.get("статус"))
        строка["опубликовано"] = вышло[имя]
        строка["статус"] = короткий_статус(п.get("статус", "заведено"))
        строка["автопостинг"] = ("есть, публикуем скриптом" if п.get("автопостинг")
                                 else строка.get("автопостинг") or "нет API, только руками")
        строка["ограничение"] = причина(п) or строка.get("ограничение")
        строка["ссылки"] = ссылки(п)
        # Просмотры отражают ровно табличку: вписано - ставим, нет - прочерк.
        # Убрал(а) строку из CSV - число уходит из панели, а не залипает.
        строка["просмотры"], строка["замерено"] = просмотры_ручные.get(имя, (None, None))
        if было != (строка["опубликовано"], строка["статус"]) and имя not in добавлено:
            изменено.append(f'{имя}: {было[0]} → {строка["опубликовано"]}, {было[1]} → {строка["статус"]}')

    # разведанные, но не заведённые площадки из отчёта убираем: они попадали
    # сюда при широком отборе и создавали ряд пустых строк
    лишние = [с["площадка"] for с in pr["площадки"]
              if not с.get("демо") and с["площадка"] not in показываем]
    pr["площадки"] = [с for с in pr["площадки"]
                      if с.get("демо") or с["площадка"] in показываем]
    pr["площадки"].sort(key=lambda с: (-(с.get("опубликовано") or 0), с["площадка"]))

    всего = sum(вышло.values())
    аккаунтов = len(показываем)
    вес = sorted(и for и in вышло if ссылки(база.get(и, {})) == "dofollow")
    для_kpi = {"статей опубликовано": (всего, ", ".join(f"{и}: {к}" for и, к in вышло.most_common())),
               "аккаунтов заведено": (аккаунтов, ", ".join(sorted(показываем)))}
    for k in pr["kpi"]:
        if k["метка"] in для_kpi:
            k["значение"], k["комментарий"] = для_kpi[k["метка"]]

    # текст пустого рейтинга тоже с цифрой: стояло «две публикации есть»,
    # когда их было семь
    pr["рейтинг_пусто"] = (
        f"Рейтинг строится по просмотрам, а счётчиков на площадках у нас нет: "
        f"публикаций {всего}, но цифры по ним площадки отдают только в своём "
        f"интерфейсе. Порядок площадок в списке ниже задан числом публикаций, "
        f"а не результатом.")
    # про «остальных» пишем раздельно: закрытая ссылка и незамеренная это
    # разные вещи, и сваливать их в одну фразу значит выдавать догадку за замер
    закрыто = sorted(и for и in вышло if ссылки(база.get(и, {})) == "nofollow")
    без_замера = sorted(и for и in вышло if ссылки(база.get(и, {})) == "не проверено")
    хвост = []
    if вес:
        хвост.append(f"Вес порталу передаёт только {', '.join(вес)}")
    if закрыто:
        хвост.append(f"закрыты nofollow: {', '.join(закрыто)}")
    if без_замера:
        хвост.append(f"не замерены: {', '.join(без_замера)}")
    pr["состояние"] = (f"публикаций на внешних площадках: {всего} на {len(вышло)} площадках. "
                       + ("; ".join(хвост) + "." if хвост else "Площадок с dofollow пока нет."))
    ОТЧЁТ.write_text(json.dumps(отчёт, ensure_ascii=False, indent=1), encoding="utf-8")

    print(f"публикаций: {всего}, площадок в отчёте: {аккаунтов}")
    for и in добавлено:
        print("  добавлена:", и)
    for и in лишние:
        print("  убрана:", и)
    for с in изменено:
        print("  ", с)


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