"""Накопление истории просмотров постов личного канала.

TGStat отдаёт только текущие цифры, а архив в волте перезаписывается — «сколько
пост набрал в первые сутки» нигде не хранится и задним числом не восстановимо.
Этот модуль — единственный механизм, который копит такую историю: ежедневный
снапшот просмотров/пересылок по свежим постам в JSONL-лог, который растёт
только вперёд.
"""
from __future__ import annotations

import json
import os
import sys
import tempfile
from datetime import date, datetime, timedelta
from pathlib import Path


def _posted_date(post: dict) -> date:
    posted_at = post["posted_at"]
    if posted_at.endswith("Z"):
        posted_at = posted_at[:-1] + "+00:00"
    return datetime.fromisoformat(posted_at).date()


def _collapse_albums(rows: list[dict]) -> list[dict]:
    """Схлопнуть альбом в одну строку на пост.

    Телеграм отдаёт альбом (медиагруппу) несколькими сообщениями с разными
    `msg_id` и одной и той же секундой публикации, и TGStat считает просмотры
    по каждому. В снапшотах это давало 3–5 почти одинаковых строк на один
    пост: 2037–2041 от 2026-07-28 с `posted_at` до секунды одинаковым и
    просмотрами 361/373/373/381/381. Пока сравнения по темпу набора нет, это
    просто шум; как только оно заработает, один пост весил бы как пять и
    перекосил бы и медиану темпа, и любой порог по рубрике.

    Группируем по (`snapshot_date`, `posted_at`): двум разным постам совпасть
    до секунды практически невозможно, а у альбома совпадение гарантировано.
    Представитель группы — минимальный `msg_id`: это голова альбома, на неё
    указывает ссылка на пост, по ней он и опознаётся в архиве. Просмотры и
    пересылки берём максимальные по группе — ближайшая снизу оценка охвата
    поста целиком (человек, посмотревший любой слайд, посмотрел пост).
    `album_size` оставляем в строке: иначе схлопывание — молчаливая правка
    данных, а его видно в git-диффе и понятно тому, кто читает файл.

    Применяется к уже слитому набору строк, а не только к свежим, — поэтому
    чинит и то, что записано в файл раньше.
    """
    groups: dict[tuple[str, str], list[dict]] = {}
    order: list[tuple[str, str]] = []
    for row in rows:
        key = (row["snapshot_date"], row["posted_at"])
        if key not in groups:
            groups[key] = []
            order.append(key)
        groups[key].append(row)

    out: list[dict] = []
    for key in order:
        group = groups[key]
        head = min(group, key=lambda r: r["msg_id"])
        merged = dict(head)
        if len(group) > 1:
            merged["views"] = max((r.get("views") or 0) for r in group)
            merged["forwards"] = max((r.get("forwards") or 0) for r in group)
        # max(), не len(): при повторном схлопывании уже записанной строки
        # (следующий прогон читает файл, где альбом — одна строка) группа
        # снова размером 1, и len() стёр бы записанный ранее album_size.
        merged["album_size"] = max(
            len(group), max((r.get("album_size") or 1) for r in group)
        )
        out.append(merged)
    return out


def write_rows(path: Path, posts: list[dict], today: str, window_days: int = 14) -> int:
    """Дописать в `path` снапшот просмотров по постам не старше `window_days`.

    Повторный вызов в тот же `today` обновляет строку `msg_id`+`snapshot_date`,
    а не дублирует её. Файл — единственная копия истории, поэтому он читается
    целиком, дополняется и перезаписывается атомарно (временный файл рядом +
    `os.replace`), а не дописывается построчно.

    Строки альбома (несколько `msg_id` с одной секундой публикации) схлопываются
    в одну — см. `_collapse_albums`.

    Возвращает число постов, отобранных в этом прогоне (попавших в окно, после
    схлопывания альбомов), а не итоговый размер файла.
    """
    path = Path(path)
    cutoff = date.fromisoformat(today) - timedelta(days=window_days)

    fresh = [p for p in posts if _posted_date(p) >= cutoff]

    existing: dict[tuple[int, str], dict] = {}
    if path.exists():
        for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
            if not line.strip():
                continue
            # Файл — единственная копия истории и синкается волтом между
            # несколькими машинами каждые 15 минут; одна битая строка (после
            # merge-конфликта, оборванной записи и т.п.) не должна навсегда
            # заблокировать накопление. Пропускаем её, но громко пишем в лог —
            # тихий отказ здесь хуже упавшего пула: его никто не заметит,
            # пока не полезет в файл руками.
            try:
                row = json.loads(line)
                key = (row["msg_id"], row["snapshot_date"])
            except (json.JSONDecodeError, KeyError) as e:
                print(
                    f"snapshots: битая строка {lineno} в {path} пропущена ({e}); "
                    "остальная история сохранена",
                    file=sys.stderr,
                )
                continue
            existing[key] = row

    for post in fresh:
        key = (post["msg_id"], today)
        existing[key] = {
            "msg_id": post["msg_id"],
            "posted_at": post["posted_at"],
            "snapshot_date": today,
            "views": post.get("views"),
            "forwards": post.get("forwards"),
        }

    # Порядок — по возрастанию snapshot_date, внутри дня по msg_id: файл
    # читают глазами, и так удобно смотреть в git-диффе.
    rows = sorted(existing.values(), key=lambda r: (r["snapshot_date"], r["msg_id"]))
    rows = _collapse_albums(rows)

    path.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=".snapshots-", suffix=".tmp")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as fh:
            for row in rows:
                fh.write(json.dumps(row, ensure_ascii=False) + "\n")
        os.replace(tmp, str(path))
    except Exception:
        Path(tmp).unlink(missing_ok=True)
        raise

    # Считаем посты, а не сообщения: альбом из пяти картинок — один пост.
    return len({p["posted_at"] for p in fresh})
