"""
warmup_diary.py — личные каналы-дневники для прогрева.

Каждый аккаунт ведёт свой приватный канал на тему (поиск работы, обучение,
путешествия, спорт, фриланс). Посты идут по нарастающей story arc.
Другие аккаунты подписываются по invite-ссылке (которую владелец постит в
общий чат прогрева) и ставят реакции на новые посты.

Это полная имитация активности живого человека:
- создание канала
- регулярные посты по теме (1-2 в день в зависимости от дня прогрева)
- шейринг ссылки в чат (естественный путь подписки)
- подписка через клик по ссылке
- реакции на новые посты подписанных каналов

Хранение: ctx.user_settings["warmup_diaries"] = {
    phone: {
        "channel_id": int,           # id созданного канала
        "access_hash": int,          # access_hash канала
        "invite_link": str,          # t.me/+xxx
        "theme": "work"|"learning"|...,
        "title": str,                # фактическое название канала
        "post_idx": int,             # сколько постов уже опубликовано
        "subscribed_to": [phone, ...],  # на кого этот акк уже подписан
        "created_at": isoformat,
    }, ...
}
"""
from __future__ import annotations

import asyncio
import logging
import random
from datetime import datetime
from typing import Dict, List, Optional, Tuple

from core.warmup_defaults import DIARY_THEMES, REACTION_EMOJIS

logger = logging.getLogger("telepush.warmup")


def _spintax(text: str) -> str:
    """Раскрывает {a|b|c} → a/b/c рандомно."""
    import re
    def pick(m):
        return random.choice(m.group(1).split('|'))
    return re.sub(r'\{([^{}]+)\}', pick, text)


def pick_theme_for_account(phone: str, used_themes: Dict[str, int], max_per_theme: int = 8) -> str:
    """Выбирает тему для нового дневника. Балансирует распределение —
    не больше N акков на одну тему чтобы каналы не выглядели одинаковыми."""
    available = [
        t for t in DIARY_THEMES.keys()
        if used_themes.get(t, 0) < max_per_theme
    ]
    if not available:
        available = list(DIARY_THEMES.keys())
    return random.choice(available)


def pick_channel_title(theme: str) -> str:
    """Возвращает рандомное название канала для темы.
    Приоритет: AI-пул (если сгенерирован) → статичные NEUTRAL_CHANNEL_NAMES."""
    try:
        from core.ai_generator import pool_chat_names as _pool_chat_names
        ai_names = _pool_chat_names()
        if ai_names:
            return random.choice(ai_names)
    except Exception:
        pass
    return random.choice(DIARY_THEMES[theme]["channel_names"])


async def create_diary_channel(client, phone: str, theme: str, title: str) -> Optional[Dict]:
    """Создаёт приватный broadcast-канал, генерирует invite-ссылку,
    публикует первый пост.

    Возвращает dict с метаданными канала или None при ошибке.
    """
    try:
        from telethon.tl.functions.channels import (
            CreateChannelRequest, GetFullChannelRequest,
        )
        from telethon.tl.functions.messages import ExportChatInviteRequest

        # Создаём канал (broadcast=True значит channel, не megagroup)
        result = await client(CreateChannelRequest(
            title=title,
            about="",
            broadcast=True,
            megagroup=False,
        ))
        channel = None
        for ch in getattr(result, "chats", []):
            channel = ch
            break
        if not channel:
            logger.warning("[diary] %s: канал создан но chats пуст", phone)
            return None

        channel_id = channel.id
        access_hash = getattr(channel, "access_hash", 0)

        # Получаем invite-ссылку
        invite_link = ""
        try:
            invite = await client(ExportChatInviteRequest(channel))
            invite_link = getattr(invite, "link", "") or ""
        except Exception as e:
            logger.debug("[diary] %s: ExportChatInvite: %s", phone, e)

        # Публикуем первый пост
        first_post = _spintax(DIARY_THEMES[theme]["posts"][0])
        try:
            await client.send_message(channel, first_post)
        except Exception as e:
            logger.warning("[diary] %s: первый пост не отправился: %s", phone, e)

        return {
            "channel_id": channel_id,
            "access_hash": access_hash,
            "invite_link": invite_link,
            "theme": theme,
            "title": title,
            "post_idx": 1,
            "subscribed_to": [],
            "created_at": datetime.utcnow().isoformat(),
        }
    except Exception as e:
        logger.warning("[diary] create_diary_channel %s: %s", phone, e)
        return None


async def post_diary_entry(client, phone: str, diary: Dict) -> Optional[str]:
    """Публикует следующий пост из story arc темы. Возвращает текст поста или None.
    Приоритет: AI-пул постов под эту тему → статичный DIARY_THEMES[theme]['posts'] + spintax."""
    try:
        theme = diary.get("theme", "work")
        idx = int(diary.get("post_idx", 0))
        # AI-пул генерится через /api/ai/regenerate_pools — story arc уникальный
        # для каждой инсталляции TelePush (минус общая шаблонность).
        ai_posts: List[str] = []
        try:
            from core.ai_generator import pool_diary_posts as _pool_diary_posts
            ai_posts = _pool_diary_posts(theme) or []
        except Exception:
            ai_posts = []
        posts = ai_posts if ai_posts else DIARY_THEMES.get(theme, {}).get("posts", [])
        if not posts:
            return None
        # Если посты кончились — циклимся с лёгкими вариациями последних
        if idx >= len(posts):
            idx = len(posts) - 1 - random.randint(0, min(2, len(posts) - 1))

        # AI-посты идут как есть, статичные — через spintax (содержат {a|b}).
        text = posts[idx] if ai_posts else _spintax(posts[idx])

        # Получаем сущность канала
        from telethon.tl.types import InputPeerChannel
        channel_id = int(diary["channel_id"])
        access_hash = int(diary.get("access_hash", 0))
        peer = InputPeerChannel(channel_id=channel_id, access_hash=access_hash)
        await client.send_message(peer, text)

        diary["post_idx"] = idx + 1
        return text
    except Exception as e:
        logger.warning("[diary] post_diary_entry %s: %s", phone, e)
        return None


async def share_diary_link_in_chat(client, phone: str, chat_id: int, invite_link: str) -> bool:
    """Постит invite-ссылку дневника в общий чат прогрева с естественной фразой."""
    if not invite_link:
        return False
    try:
        intros = [
            "ребят, завёл канал — буду писать что у меня происходит",
            "решил вести свой канал, кому интересно — заходите",
            "тут запустил канал, делюсь мыслями. подпишитесь если что",
            "канал свой завёл, наверное буду туда что-то писать",
            "запустил канал, посмотрите если интересно",
            "ребят, вот мой канал — заходите",
        ]
        text = f"{random.choice(intros)}\n{invite_link}"
        # Получаем сущность чата
        entity = await client.get_input_entity(int(chat_id))
        await client.send_message(entity, text)
        return True
    except Exception as e:
        logger.warning("[diary] share_link %s: %s", phone, e)
        return False


async def subscribe_to_diary_via_link(client, phone: str, invite_link: str) -> bool:
    """Подписывается на чужой дневник по invite-ссылке (как реальный человек —
    кликнул и зашёл). Возвращает True при успехе."""
    try:
        # Парсим хеш из t.me/+ABC или t.me/joinchat/ABC
        invite_hash = None
        if "+" in invite_link:
            invite_hash = invite_link.split("+", 1)[1].split("?")[0].split("/")[0]
        elif "joinchat/" in invite_link:
            invite_hash = invite_link.split("joinchat/", 1)[1].split("?")[0].split("/")[0]
        if not invite_hash:
            return False

        from telethon.tl.functions.messages import ImportChatInviteRequest
        await client(ImportChatInviteRequest(invite_hash))
        return True
    except Exception as e:
        msg = str(e).lower()
        if "user_already_participant" in msg or "already" in msg:
            return True  # уже подписаны — не ошибка
        logger.debug("[diary] subscribe %s: %s", phone, e)
        return False


async def react_on_recent_diary_post(client, phone: str, diary: Dict) -> bool:
    """Ставит реакцию на последний пост в чужом дневнике."""
    try:
        from telethon.tl.types import InputPeerChannel, ReactionEmoji
        from telethon.tl.functions.messages import SendReactionRequest

        channel_id = int(diary["channel_id"])
        access_hash = int(diary.get("access_hash", 0))
        peer = InputPeerChannel(channel_id=channel_id, access_hash=access_hash)

        # Берём последние 3 поста
        msgs = await client.get_messages(peer, limit=3)
        if not msgs:
            return False

        msg = random.choice(msgs)
        emoji = random.choice(REACTION_EMOJIS)
        await client(SendReactionRequest(
            peer=peer,
            msg_id=msg.id,
            reaction=[ReactionEmoji(emoticon=emoji)],
        ))
        return True
    except Exception as e:
        logger.debug("[diary] react %s: %s", phone, e)
        return False


def get_post_quota_for_day(day: int) -> int:
    """Возвращает сколько постов в день делать с учётом дня прогрева.

    День 1-3: 0 (дневники ещё не созданы — только знакомство)
    День 4-6: 1 пост в 1-2 дня → 0 или 1
    День 7-13: 1 пост в день
    День 14+: 1-2 поста в день
    """
    if day < 4:
        return 0
    if day < 7:
        return random.choice([0, 1])  # 50% дней — пост, 50% — пауза
    if day < 14:
        return 1
    return random.choice([1, 1, 2])


MAX_DIARY_SUBSCRIPTIONS_PER_ACCOUNT = 6
"""Максимум на сколько ЧУЖИХ дневников один акк может подписаться за весь
цикл прогрева. Раньше было unlimited → 21 акк × 20 = полносвязный граф,
TG детектил это как бот-сеть и репортил/морозил (incident 2026-05-11: 16/21).
6 — компромисс: достаточно для редкой реакции на чужие посты, не выдаёт сети."""


def find_unsubscribed_diaries(my_phone: str, my_diary: Dict, all_diaries: Dict[str, Dict]) -> List[Tuple[str, Dict]]:
    """Возвращает список (phone, diary_data) дневников на которые этот акк ещё не подписан.

    Если акк уже подписан на MAX_DIARY_SUBSCRIPTIONS_PER_ACCOUNT — возвращает [],
    чтобы граф подписок оставался разреженным."""
    subscribed = list(my_diary.get("subscribed_to") or [])
    if len(subscribed) >= MAX_DIARY_SUBSCRIPTIONS_PER_ACCOUNT:
        return []
    subscribed_set = set(subscribed)
    return [
        (phone, d) for phone, d in all_diaries.items()
        if phone != my_phone and phone not in subscribed_set and d.get("invite_link")
    ]
