"""
boost.py — модуль накрутки (массовые действия пачкой аккаунтов).

6 типов:
1. subscribers — подписчики в канал/чат (JoinChannel / ImportChatInvite)
2. reactions — реакции на пост (SendReaction)
3. polls — голос в опросе (SendVote)
4. buttons — нажатие inline-кнопок (GetBotCallbackAnswer)
5. bots — запуск бота (/start)
6. views — просмотры поста (GetMessagesViews)

Запуск: run_boost(ctx, job, log_fn) — асинхронный воркер с потоками.
Состояние: BoostJob в models.py.
"""
from __future__ import annotations

import asyncio
import logging
import random
import re
from dataclasses import dataclass, field
from typing import List, Optional, Tuple

from core.client_builder import ensure_connected, release_after_task

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


# ── Парсинг целей ───────────────────────────────────────────────────────────

def parse_target(target: str) -> dict:
    """Разбирает любую форму ссылки/юзернейма на компоненты.

    Возвращает {"kind": "channel"|"invite"|"post"|"user_post", "username": str|None,
                "invite_hash": str|None, "msg_id": int|None}
    """
    s = (target or "").strip()
    if not s:
        return {"kind": "unknown"}

    # Invite-link: t.me/+xxx или t.me/joinchat/xxx
    m = re.search(r't\.me/(\+|joinchat/)([A-Za-z0-9_-]+)', s)
    if m:
        return {"kind": "invite", "invite_hash": m.group(2)}

    # Пост в канале: t.me/channel/123 или t.me/c/123456/789
    m = re.search(r't\.me/(c/(-?\d+)|([A-Za-z0-9_]+))/(\d+)', s)
    if m:
        if m.group(2):  # private channel: c/CHANNEL_ID/MSG_ID
            return {"kind": "post", "channel_id": int(m.group(2)), "msg_id": int(m.group(4))}
        return {"kind": "post", "username": m.group(3), "msg_id": int(m.group(4))}

    # Просто канал/юзер: t.me/channel или @channel или channel
    m = re.search(r'(?:t\.me/|@)?([A-Za-z0-9_]{4,})$', s)
    if m:
        return {"kind": "channel", "username": m.group(1)}

    return {"kind": "unknown"}


async def _resolve_post(client, parsed: dict):
    """Возвращает (peer, msg_id) для пост-цели."""
    if parsed.get("kind") != "post":
        return None, None
    msg_id = int(parsed.get("msg_id", 0))
    if parsed.get("username"):
        try:
            entity = await client.get_entity(parsed["username"])
            return entity, msg_id
        except Exception as e:
            logger.debug("[boost] resolve %s: %s", parsed.get("username"), e)
            return None, None
    if parsed.get("channel_id"):
        # Приватный канал — нужен access_hash, но если акк уже подписан — get_entity
        try:
            entity = await client.get_entity(int(parsed["channel_id"]))
            return entity, msg_id
        except Exception:
            return None, None
    return None, None


async def _resolve_channel(client, parsed: dict):
    """Возвращает entity канала/чата для подписки."""
    if parsed.get("kind") == "invite":
        return None  # invite-link обрабатываем отдельно через ImportChatInvite
    if parsed.get("username"):
        try:
            return await client.get_entity(parsed["username"])
        except Exception as e:
            logger.debug("[boost] resolve channel %s: %s", parsed.get("username"), e)
            return None
    return None


# ── Функции накрутки ───────────────────────────────────────────────────────

async def boost_subscribe(client, target_str: str) -> Tuple[bool, str]:
    """Подписывает аккаунт на канал/чат."""
    parsed = parse_target(target_str)
    try:
        if parsed["kind"] == "invite":
            from telethon.tl.functions.messages import ImportChatInviteRequest
            await client(ImportChatInviteRequest(parsed["invite_hash"]))
            return True, "joined invite"
        if parsed["kind"] == "channel":
            from telethon.tl.functions.channels import JoinChannelRequest
            entity = await _resolve_channel(client, parsed)
            if not entity:
                return False, "не удалось найти канал"
            await client(JoinChannelRequest(entity))
            return True, f"подписан на @{parsed.get('username', '?')}"
        return False, f"неподдерживаемый формат: {target_str}"
    except Exception as e:
        msg = str(e).lower()
        if "user_already_participant" in msg or "already" in msg:
            return True, "уже подписан"
        return False, str(e)[:80]


async def boost_react(client, target_str: str, emojis: List[str]) -> Tuple[bool, str]:
    """Ставит реакцию на пост (рандомная из списка)."""
    parsed = parse_target(target_str)
    if parsed["kind"] != "post":
        return False, "укажите ссылку на пост: t.me/channel/123"
    if not emojis:
        return False, "укажите хотя бы один эможди"
    try:
        from telethon.tl.functions.messages import SendReactionRequest
        from telethon.tl.types import ReactionEmoji
        peer, msg_id = await _resolve_post(client, parsed)
        if not peer:
            return False, "пост не найден"
        emoji = random.choice(emojis)
        await client(SendReactionRequest(
            peer=peer, msg_id=msg_id,
            reaction=[ReactionEmoji(emoticon=emoji)],
        ))
        return True, f"поставил {emoji}"
    except Exception as e:
        return False, str(e)[:80]


async def boost_vote(client, target_str: str, option_idx: Optional[int],
                    allow_revote: bool) -> Tuple[bool, str]:
    """Голосует в опросе. option_idx=None → рандом."""
    parsed = parse_target(target_str)
    if parsed["kind"] != "post":
        return False, "укажите ссылку на пост-опрос"
    try:
        from telethon.tl.functions.messages import SendVoteRequest
        peer, msg_id = await _resolve_post(client, parsed)
        if not peer:
            return False, "пост не найден"

        msg = await client.get_messages(peer, ids=msg_id)
        if not msg or not getattr(msg, "poll", None):
            return False, "это не опрос"

        poll = msg.poll
        answers = poll.poll.answers
        if not answers:
            return False, "в опросе нет вариантов"

        # Проверка переголосования
        if not allow_revote:
            try:
                results = poll.results
                if results and getattr(results, "results", None):
                    for r in results.results:
                        if getattr(r, "chosen", False):
                            return False, "уже голосовал"
            except Exception:
                pass

        if option_idx is None or option_idx < 0 or option_idx >= len(answers):
            chosen = random.choice(answers)
        else:
            chosen = answers[option_idx]

        await client(SendVoteRequest(peer=peer, msg_id=msg_id, options=[chosen.option]))
        return True, f"проголосовал за вариант {chosen.text.text if hasattr(chosen.text, 'text') else chosen.text}"
    except Exception as e:
        return False, str(e)[:80]


async def boost_button(client, target_str: str, button_text_or_idx: str) -> Tuple[bool, str]:
    """Жмёт inline-кнопку по тексту или индексу (формат '0' или 'Принять')."""
    parsed = parse_target(target_str)
    if parsed["kind"] != "post":
        return False, "укажите ссылку на сообщение с кнопкой"
    try:
        peer, msg_id = await _resolve_post(client, parsed)
        if not peer:
            return False, "сообщение не найдено"
        msg = await client.get_messages(peer, ids=msg_id)
        if not msg or not getattr(msg, "reply_markup", None):
            return False, "у сообщения нет кнопок"

        # Собираем все кнопки в плоский список
        rows = msg.reply_markup.rows or []
        flat = []
        for row in rows:
            for btn in (row.buttons or []):
                flat.append(btn)
        if not flat:
            return False, "кнопок нет"

        # Поиск по индексу или тексту
        chosen = None
        if button_text_or_idx.isdigit():
            idx = int(button_text_or_idx)
            if 0 <= idx < len(flat):
                chosen = flat[idx]
        else:
            t = button_text_or_idx.lower().strip()
            for btn in flat:
                if t in (getattr(btn, "text", "") or "").lower():
                    chosen = btn
                    break

        if not chosen:
            return False, f"кнопка '{button_text_or_idx}' не найдена"

        # Callback или URL
        from telethon.tl.types import KeyboardButtonCallback, KeyboardButtonUrl
        if isinstance(chosen, KeyboardButtonCallback):
            from telethon.tl.functions.messages import GetBotCallbackAnswerRequest
            await client(GetBotCallbackAnswerRequest(peer=peer, msg_id=msg_id, data=chosen.data))
            return True, f"клик '{chosen.text}'"
        if isinstance(chosen, KeyboardButtonUrl):
            return True, f"URL-кнопка ('{chosen.text}') — открыта (без клика, это просто ссылка)"
        return False, f"тип кнопки {type(chosen).__name__} не поддержан"
    except Exception as e:
        return False, str(e)[:80]


async def boost_start_bot(client, target_str: str, allow_restart: bool) -> Tuple[bool, str]:
    """Запускает бота /start. Если уже есть диалог и allow_restart=False — пропускает."""
    parsed = parse_target(target_str)
    if parsed["kind"] != "channel" or not parsed.get("username"):
        return False, "укажите @username бота"
    try:
        bot = await client.get_entity(parsed["username"])
        # Проверка существующего диалога
        if not allow_restart:
            try:
                msgs = await client.get_messages(bot, limit=1)
                if msgs:
                    return False, f"уже есть диалог с @{parsed['username']}"
            except Exception:
                pass
        await client.send_message(bot, "/start")
        return True, f"запустил @{parsed['username']}"
    except Exception as e:
        return False, str(e)[:80]


async def boost_views(client, target_str: str) -> Tuple[bool, str]:
    """Накручивает 1 просмотр на пост. Один акк = +1 view."""
    parsed = parse_target(target_str)
    if parsed["kind"] != "post":
        return False, "укажите ссылку на пост"
    try:
        from telethon.tl.functions.messages import GetMessagesViewsRequest
        peer, msg_id = await _resolve_post(client, parsed)
        if not peer:
            return False, "пост не найден"
        await client(GetMessagesViewsRequest(peer=peer, id=[msg_id], increment=True))
        return True, "+1 view"
    except Exception as e:
        return False, str(e)[:80]


# ── Главный воркер ─────────────────────────────────────────────────────────

@dataclass
class BoostConfig:
    boost_type: str            # "subscribers" | "reactions" | "polls" | "buttons" | "bots" | "views"
    target: str                # ссылка/юзернейм
    count: int = 50            # сколько акков использовать (0 = все)
    delay_min: float = 5.0
    delay_max: float = 15.0
    threads: int = 1
    folder: str = ""           # пустая = все живые акки
    # Тип-специфичные:
    emojis: List[str] = field(default_factory=list)  # для reactions
    poll_option: Optional[int] = None  # для polls (None = рандом)
    button_text: str = ""      # для buttons
    allow_restart: bool = False
    allow_revote: bool = False


async def run_boost(ctx, job, log_fn):
    """Главный воркер — берёт пачку аккаунтов и параллельно крутит выбранное действие.

    job — BoostJob с config: BoostConfig, status: str ("running"/"stopped"), counters.
    """
    cfg = job.config
    boost_type = cfg.boost_type
    target = cfg.target

    # Отбираем аккаунты
    accs = []
    folder_filter = (cfg.folder or "").strip()
    for phone, acc in list(ctx.accounts.items()):
        # Только подтверждённо мёртвые исключаются — soft-dead (транзиентный сбой)
        # пускаем дальше, акку дадут шанс подключиться внутри ensure_connected.
        from core.warmup import _is_confirmed_dead
        if _is_confirmed_dead(acc) or acc.frozen:
            continue
        if folder_filter and getattr(acc, "folder_name", "") != folder_filter:
            continue
        accs.append(acc)

    if cfg.count and cfg.count > 0:
        accs = accs[:cfg.count]

    if not accs:
        log_fn("Нет подходящих аккаунтов для накрутки")
        job.status = "stopped"
        return

    log_fn(f"Накрутка [{boost_type}] на цели «{target}» — {len(accs)} акк, потоков: {cfg.threads}")

    sem = asyncio.Semaphore(max(1, int(cfg.threads)))
    job.total = len(accs)
    job.done = 0
    job.ok = 0
    job.fail = 0

    async def _process(acc):
        async with sem:
            if job.status != "running":
                return
            phone = acc.phone
            if not await ensure_connected(acc):
                job.fail += 1
                job.done += 1
                log_fn(f"❌ {phone} не подключился")
                return
            try:
                if boost_type == "subscribers":
                    ok, msg = await boost_subscribe(acc.client, target)
                elif boost_type == "reactions":
                    ok, msg = await boost_react(acc.client, target, cfg.emojis or ["👍"])
                elif boost_type == "polls":
                    ok, msg = await boost_vote(acc.client, target, cfg.poll_option, cfg.allow_revote)
                elif boost_type == "buttons":
                    ok, msg = await boost_button(acc.client, target, cfg.button_text)
                elif boost_type == "bots":
                    ok, msg = await boost_start_bot(acc.client, target, cfg.allow_restart)
                elif boost_type == "views":
                    ok, msg = await boost_views(acc.client, target)
                else:
                    ok, msg = False, f"неизвестный тип: {boost_type}"

                if ok:
                    job.ok += 1
                    log_fn(f"✓ {phone}: {msg}")
                else:
                    job.fail += 1
                    log_fn(f"✗ {phone}: {msg}")
            except Exception as e:
                job.fail += 1
                log_fn(f"⚠ {phone}: {str(e)[:60]}")
            finally:
                job.done += 1
                try: await release_after_task(acc)
                except Exception: pass
                # Пауза между действиями (внутри потока)
                await asyncio.sleep(random.uniform(cfg.delay_min, cfg.delay_max))

    await asyncio.gather(*(_process(a) for a in accs))

    if job.status == "running":
        job.status = "finished"
    log_fn(f"Накрутка завершена. Успешно: {job.ok}, ошибок: {job.fail}")
