"""SQLite → md-архивы в волте.

Формат повторяет TGStat-архив канала: планёрка уже умеет его читать, и учить
её новому синтаксису незачем. Медианы считаются здесь и кладутся готовыми в
шапку — чтобы у планёрки была одна точка истины, а не свой пересчёт.
"""
from __future__ import annotations

import statistics
from datetime import datetime, timedelta, timezone
from pathlib import Path

import db

MSK = timezone(timedelta(hours=3))

# Пост младше двух суток ещё активно добирает охват: в медиане он занижал бы
# планку, по которой планёрка судит остальные.
MEDIAN_MIN_AGE_DAYS = 2
MEDIAN_WINDOW_DAYS = 90
MEDIAN_MIN_POSTS = 5

PLATFORM_TITLES = {"instagram": "Instagram", "threads": "Threads"}

# Тип контента для человека: у рилса и карусели несопоставимые охваты, и в
# архиве это должно быть видно с первого взгляда.
IG_KINDS = {
    ("REELS", None): "рилс",
    ("FEED", "CAROUSEL_ALBUM"): "карусель",
    ("FEED", "VIDEO"): "видео",
    ("FEED", "IMAGE"): "фото",
}

IG_METRIC_LABELS = (
    ("views", "Просмотры"), ("reach", "Охват"), ("likes", "Лайки"),
    ("comments", "Комменты"), ("saved", "Сохранения"), ("shares", "Шеры"),
    ("follows", "Подписки"), ("profile_visits", "Заходы в профиль"),
)
TH_METRIC_LABELS = (
    ("views", "Просмотры"), ("likes", "Лайки"), ("replies", "Ответы"),
    ("reposts", "Репосты"), ("quotes", "Цитаты"), ("shares", "Шеры"),
)


def _parse(moment: str) -> datetime:
    return datetime.fromisoformat(moment.replace("Z", "+00:00"))


def _msk(moment: str) -> str:
    return _parse(moment).astimezone(MSK).strftime("%Y-%m-%d %H:%M")


def ig_kind(row) -> str:
    product = row["media_product_type"]
    if product == "REELS":
        return IG_KINDS[("REELS", None)]
    return IG_KINDS.get((product, row["media_type"]), "пост")


def _live_posts(db_path, platform: str) -> list:
    with db.connect(db_path) as conn:
        return conn.execute(
            "SELECT * FROM {} WHERE is_deleted = 0 "
            "ORDER BY timestamp DESC".format(db.TABLES[platform])
        ).fetchall()


def medians(db_path, platform: str, now: str) -> dict:
    """Медиана просмотров по типам контента (для Threads — единственный ключ)."""
    newest_allowed = _parse(now) - timedelta(days=MEDIAN_MIN_AGE_DAYS)
    oldest_allowed = _parse(now) - timedelta(days=MEDIAN_WINDOW_DAYS)

    buckets: dict[str, list] = {}
    for row in _live_posts(db_path, platform):
        posted = _parse(row["timestamp"])
        if not (oldest_allowed <= posted <= newest_allowed):
            continue
        if row["views"] is None:
            continue
        key = row["media_product_type"] if platform == "instagram" else "ALL"
        buckets.setdefault(key, []).append(row["views"])

    return {key: (int(statistics.median(values))
                  if len(values) >= MEDIAN_MIN_POSTS else None)
            for key, values in buckets.items()}


def _median_line(platform: str, values: dict) -> str:
    if platform == "threads":
        median = values.get("ALL")
        return ("Медиана просмотров за 90 дней: {}.".format(median) if median
                else "Медиана просмотров: пока мало постов для расчёта.")
    named = {"REELS": "рилсы", "FEED": "лента"}
    parts = ["{} — {} просмотров".format(named.get(key, key), value)
             for key, value in sorted(values.items()) if value]
    return ("Медианы просмотров за 90 дней: {}.".format(", ".join(parts)) if parts
            else "Медианы просмотров: пока мало постов для расчёта.")


def _header(db_path, platform: str, now: str, username: str, total: int) -> list:
    state = db.account_state(db_path, platform, today=now[:10])
    followers = "—" if state["followers"] is None else str(state["followers"])
    if state["growth"] is not None:
        followers += " ({:+d} за {} дн.)".format(state["growth"], state["growth_days"])
    return [
        "# {} архив постов @{}".format(PLATFORM_TITLES[platform], username),
        "",
        "Обновлено: {}. Всего постов: {}. Подписчики: {}.".format(
            now[:10], total, followers),
        "",
        _median_line(platform, medians(db_path, platform, now)),
        "",
    ]


def _post_block(platform: str, row) -> list:
    labels = IG_METRIC_LABELS if platform == "instagram" else TH_METRIC_LABELS
    metrics = ["{}: {}".format(label, row[field])
               for field, label in labels if row[field] is not None]

    title = _msk(row["timestamp"])
    if platform == "instagram":
        title += " — " + ig_kind(row)
    elif row["is_quote_post"]:
        title += " — цитата"

    link = row["permalink"] or ""
    text = ((row["caption"] if platform == "instagram" else row["text"]) or "").strip()
    return [
        "### {} — [{}]({})".format(title, link, link),
        "**" + " · ".join(metrics) + "**" if metrics else "_(метрики ещё не собраны)_",
        text or "_(без текста)_",
        "",
    ]


def render_archive(db_path, platform: str, out_path: Path, now: str,
                   username: str) -> None:
    """Пишет архив ровно в out_path — корень волта решает вызывающий код.

    Путь приходит аргументом намеренно: в соседнем tgstat-puller рендер брал
    его из переменной окружения и в тестах уехал в боевой волт, обнулив там
    архив канала.
    """
    rows = _live_posts(db_path, platform)
    lines = _header(db_path, platform, now, username, total=len(rows))
    lines += ["## Все посты (новые сверху)", ""]
    for row in rows:
        lines += _post_block(platform, row)

    out_path = Path(out_path)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text("\n".join(lines), encoding="utf-8")
