"""
anti_detect.py — общие хелперы для имитации человеческой активности.

Используется во всех активных модулях (рассылка, ЛС, инвайт, прогрев, и т.д.):
- keep_online_loop  — держит акк в онлайне через UpdateStatusRequest
- mark_chat_read    — отмечает все непрочитанные в чате прочитанными
- view_profile      — смотрит профиль (GetFullUserRequest)
- view_profile_photo— скачивает фото профиля (имитирует тап на аватарку)
- solve_captcha     — пытается пройти капчу-бот в чате (Shieldy / MissRose / Combot)
- pre_spam_check    — батч-проверка списка аккаунтов на @SpamBot перед стартом

Все действия пишут в логгер `telepush.<module>` (виден в UI-консоли).
"""
from __future__ import annotations

import asyncio
import logging
import random
import re
import time
from typing import List, Optional, Tuple

logger = logging.getLogger("telepush")

# Глобальный счётчик anti-detect-событий (для UI-аналитики).
# Не персистится между перезапусками — это live-стата текущей сессии.
AD_COUNTERS = {
    "pre_check_runs": 0,        # сколько раз был запущен pre-spam-check
    "pre_check_blocked": 0,     # сколько акков отсеяно как заблокированные
    "captcha_solved": 0,        # сколько капч успешно решено
    "captcha_seen": 0,          # сколько капча-ботов обнаружено
    "profiles_viewed": 0,       # сколько просмотров профиля
    "photos_viewed": 0,         # сколько просмотров фото
    "messages_read": 0,         # сколько сообщений отмечено прочитанными
    "online_minutes": 0,        # суммарное время онлайн-режима (мин)
}


def _ad_counter_inc(key: str, by: int = 1):
    try:
        if key in AD_COUNTERS:
            AD_COUNTERS[key] += by
    except Exception:
        pass


# ── Онлайн-статус ────────────────────────────────────────────────────────────

async def keep_online_loop(client, stop_event: asyncio.Event, interval: int = 75,
                           jitter: int = 15, module: str = "system") -> None:
    """Фоновая корутина: каждые N сек ± jitter шлёт UpdateStatus(offline=False).
    Реальный Telegram-клиент обновляет статус раз в 60-120 сек — поэтому 75±15.
    Раньше было 25 сек (как cron) — слишком часто, выглядит как бот.
    Останавливается когда stop_event.set()."""
    from telethon.tl.functions.account import UpdateStatusRequest
    log = logging.getLogger(f"telepush.{module}")
    while not stop_event.is_set():
        try:
            await client(UpdateStatusRequest(offline=False))
        except Exception as e:
            log.debug("[online] %s", e)
        delay = interval + random.randint(-jitter, jitter)
        try:
            await asyncio.wait_for(stop_event.wait(), timeout=delay)
            return
        except asyncio.TimeoutError:
            continue


# ── Лёгкая alive-проверка ────────────────────────────────────────────────────

async def light_alive_check(acc, module: str = "system") -> tuple:
    """Лёгкая проверка живости через help.getConfig — 1 запрос, безопасно.
    Это служебный вызов Telegram, клиенты дёргают его при старте сессии.
    Возвращает (alive: bool, reason: str). При frozen/dead обновляет acc.

    Используется вместо тяжёлого deep_health_check_account на старте задачи —
    деёп-чек делает 6 запросов, лайт-чек 1.
    """
    log = logging.getLogger(f"telepush.{module}")
    try:
        from telethon.tl.functions.help import GetConfigRequest
        from telethon import errors as _te
    except Exception:
        return True, ""  # Telethon недоступен — пусть проверит реальная задача
    if not getattr(acc, 'client', None):
        return False, "no_client"
    try:
        await asyncio.wait_for(acc.client(GetConfigRequest()), timeout=10.0)
        return True, ""
    except _te.AuthKeyUnregisteredError:
        acc.dead = True
        acc.dead_reason = "AuthKeyUnregistered"
        log.warning("[alive] %s — сессия отозвана", acc.phone)
        return False, "session_revoked"
    except _te.SessionRevokedError:
        acc.dead = True
        acc.dead_reason = "SessionRevoked"
        log.warning("[alive] %s — сессия отозвана", acc.phone)
        return False, "session_revoked"
    except _te.UserDeactivatedError:
        acc.dead = True
        acc.dead_reason = "UserDeactivated"
        return False, "deactivated"
    except _te.UserDeactivatedBanError:
        acc.dead = True
        acc.dead_reason = "UserDeactivatedBan"
        return False, "banned"
    except Exception as e:
        ename = type(e).__name__
        emsg = str(e).upper()
        if "Frozen" in ename or "FROZEN" in emsg:
            acc.frozen = True
            acc.frozen_reason = ename
            log.warning("[alive] %s — заморожен (%s)", acc.phone, ename)
            return False, "frozen"
        # Прочие ошибки — не делаем выводов, пусть задача сама ловит при работе
        log.debug("[alive] %s — %s: %s", acc.phone, ename, str(e)[:80])
        return True, ""


# ── Чтение сообщений ─────────────────────────────────────────────────────────

async def mark_chat_read(client, peer, module: str = "system") -> int:
    """Отмечает все непрочитанные в чате как прочитанные. Возвращает кол-во."""
    log = logging.getLogger(f"telepush.{module}")
    try:
        msgs = []
        async for m in client.iter_messages(peer, limit=10):
            if m.id and not (getattr(m, 'out', False)):
                msgs.append(m)
        if not msgs:
            return 0
        await client.send_read_acknowledge(peer, msgs[0])
        title = getattr(peer, 'title', '') or getattr(peer, 'username', '') or str(peer)
        log.info("[чтение] прочитано %d сообщений в %s", len(msgs), title[:40])
        _ad_counter_inc("messages_read", len(msgs))
        return len(msgs)
    except Exception as e:
        log.debug("[чтение] %s", e)
        return 0


# ── Просмотр профиля ─────────────────────────────────────────────────────────

async def human_activity_loop(acc, ad: dict, stop_event: asyncio.Event,
                               interval_min: int = 300, interval_max: int = 900,
                               module: str = "system") -> None:
    """Фоновая корутина: пока работает основной job, аккаунт периодически
    делает «человеческие» действия — читает сообщения / смотрит профили /
    фото. Не блокирует runner, добавляет естественный паттерн.

    Использует флаги ad: read_messages, mark_read, view_profiles, view_photos.
    Интервал между раундами 5-15 мин — выглядит как реальная активность.
    """
    if not ad or not isinstance(ad, dict):
        return
    if not (ad.get('read_messages') or ad.get('mark_read') or
            ad.get('view_profiles') or ad.get('view_photos')):
        return

    log = logging.getLogger(f"telepush.{module}")
    while not stop_event.is_set():
        # Случайный wait перед раундом
        wait_sec = random.randint(interval_min, interval_max)
        try:
            await asyncio.wait_for(stop_event.wait(), timeout=wait_sec)
            return
        except asyncio.TimeoutError:
            pass

        if not getattr(acc, 'client', None):
            continue

        # 1-3 случайных действия за раунд
        try:
            dialogs = []
            async for d in acc.client.iter_dialogs(limit=20):
                dialogs.append(d)
            if not dialogs:
                continue
            picks = random.sample(dialogs, min(random.randint(1, 3), len(dialogs)))

            for d in picks:
                try:
                    # Чтение
                    if (ad.get('read_messages') or ad.get('mark_read')) and d.unread_count and d.unread_count > 0:
                        await mark_chat_read(acc.client, d.entity, module=module)

                    # Просмотр профиля (только для user-диалогов)
                    if ad.get('view_profiles') and d.is_user:
                        await view_profile(acc.client, d.entity, module=module, sample_rate=1.0)

                    # Фото (реже)
                    if ad.get('view_photos') and d.is_user:
                        await view_profile_photo(acc.client, d.entity, module=module, sample_rate=0.4)
                except Exception as e:
                    log.debug("[human] %s — action: %s", acc.phone, e)
                # Между действиями небольшая пауза
                await asyncio.sleep(random.uniform(2, 8))
        except Exception as e:
            log.debug("[human] %s — loop: %s", acc.phone, e)


def should_sample(rate: float = 0.20) -> bool:
    """Случайно решает делать ли «человеческое» действие. По умолчанию 20%.
    Реальный человек НЕ открывает каждый профиль / каждое фото —
    100% покрытие выдаёт бота сразу."""
    return random.random() < max(0.0, min(1.0, rate))


async def view_profile(client, user, module: str = "system",
                       sample_rate: float = 0.20) -> bool:
    """Имитация просмотра профиля. По умолчанию sample 20% — раз в 5 целей.
    GetFullUserRequest = 1 запрос. Логирует только когда реально дёрнули."""
    if not should_sample(sample_rate):
        return False
    from telethon.tl.functions.users import GetFullUserRequest
    log = logging.getLogger(f"telepush.{module}")
    try:
        await client(GetFullUserRequest(user))
        nm = getattr(user, 'username', None) or getattr(user, 'first_name', '') or '?'
        log.info("[профиль] открыл @%s", nm[:30])
        _ad_counter_inc("profiles_viewed")
        return True
    except Exception as e:
        log.debug("[профиль] %s", e)
        return False


async def view_profile_photo(client, user, module: str = "system",
                             sample_rate: float = 0.10) -> bool:
    """Имитация тапа по аватарке. Sample 10% — реже чем view_profile,
    клик по фото более редкий. GetUserPhotosRequest = 1 запрос."""
    if not should_sample(sample_rate):
        return False
    from telethon.tl.functions.photos import GetUserPhotosRequest
    log = logging.getLogger(f"telepush.{module}")
    try:
        photos = await client(GetUserPhotosRequest(
            user_id=user, offset=0, max_id=0, limit=1
        ))
        if not photos.photos:
            return False
        nm = getattr(user, 'username', None) or getattr(user, 'first_name', '') or '?'
        log.info("[фото] посмотрел аватар @%s", nm[:30])
        _ad_counter_inc("photos_viewed")
        return True
    except Exception as e:
        log.debug("[фото] %s", e)
        return False


# ── Капча в чатах ────────────────────────────────────────────────────────────

# Известные капча-боты в Telegram-чатах. При старте задачи софт
# проверяет последние 5 сообщений в чате — если автор бота из этого
# списка и в сообщении есть кнопки → пробуем нажать кнопку.
KNOWN_CAPTCHA_BOTS = [
    # Большие популярные боты
    "ShieldyBot", "MissRose_bot", "CombotOfficial", "rose", "shieldy",
    "GroupHelpBot", "grouphelp_bot", "SafeGuardRobot", "SafeGuardBot",
    # Прочие
    "ksevbot", "bansheebot", "captchabot", "nephilimcaptcha",
    "voicy_bot", "veryvirtualbot", "VirtualBotyBot",
    "PoliceTGBot", "BansheeBot", "DaisyXbot", "DaisyXBot",
    "telegabot", "evilfreedom_bot", "antispambot",
]

# Тексты на кнопках которые означают «я не бот» / «продолжить»
CAPTCHA_OK_BUTTONS = [
    r"я.*не.*бот", r"я.*человек", r"i.*am.*not.*bot", r"continue", r"verify",
    r"продолж", r"подтвер", r"начать", r"пройти", r"я.*не.*робот",
]


async def solve_captcha(client, chat, module: str = "system") -> bool:
    """Сканирует последние сообщения в чате на капчу-бот.
    Если нашёл — пытается нажать кнопку «я не бот» / решить.
    Возвращает True если что-то сделал."""
    log = logging.getLogger(f"telepush.{module}")
    try:
        async for msg in client.iter_messages(chat, limit=5):
            sender = await msg.get_sender()
            if not sender or not getattr(sender, 'bot', False):
                continue
            sender_name = (getattr(sender, 'username', '') or '').lower()
            if not any(kw.lower() in sender_name for kw in KNOWN_CAPTCHA_BOTS):
                continue
            # Капча-бот найден — пробуем нажать кнопку
            if not msg.buttons:
                continue
            for row in msg.buttons:
                for btn in row:
                    btn_text = (getattr(btn, 'text', '') or '').lower()
                    if any(re.search(p, btn_text, re.IGNORECASE) for p in CAPTCHA_OK_BUTTONS):
                        try:
                            await btn.click()
                            log.info("[капча] нажал «%s» от @%s", btn_text[:40], sender_name)
                            _ad_counter_inc("captcha_solved")
                            _ad_counter_inc("captcha_seen")
                            return True
                        except Exception as e:
                            log.debug("[капча] click failed: %s", e)
            # Кнопок «я не бот» не нашли — возможно числовая капча или иная
            log.debug("[капча] @%s в чате — есть кнопки но «я не бот» не найдена", sender_name)
            return False
    except Exception as e:
        log.debug("[капча] scan failed: %s", e)
    return False


# ── Pre-start spam-block check ──────────────────────────────────────────────

async def pre_spam_check_batch(accounts: list, log_module: str = "system",
                               on_progress=None,
                               cache_ttl_sec: int = 3 * 3600) -> Tuple[list, list]:
    """Проверяет список акков на спам-блок через @SpamBot перед стартом задачи.

    КЕШ: если `acc.spam_checked_at` < cache_ttl_sec назад — берём результат
    из памяти, не дёргаем @SpamBot заново. По умолчанию 3 часа — Telegram
    редко меняет статус чаще, экономим запросы.

    Возвращает (ok_accounts, blocked_accounts). Заблокированные исключаются.
    on_progress(idx, total, phone, status) — опциональный коллбэк для UI.
    """
    log = logging.getLogger(f"telepush.{log_module}")
    from core.spam import probe_spamblock as check_spam_block

    ok_list = []
    blocked_list = []
    total = len(accounts)
    now = time.time()
    cached_n = 0

    for idx, acc in enumerate(accounts, 1):
        try:
            # Используем кеш если он свежий
            checked_at = getattr(acc, 'spam_checked_at', None) or 0
            if checked_at and (now - checked_at) < cache_ttl_sec:
                if getattr(acc, 'spam_blocked', False):
                    blocked_list.append((acc, getattr(acc, 'spam_block_note', '?')))
                else:
                    ok_list.append(acc)
                cached_n += 1
                if on_progress:
                    on_progress(idx, total, acc.phone, "из кеша")
                continue

            if not getattr(acc, 'client', None):
                from core.account_helpers import ensure_connected
                ok = await ensure_connected(acc, log_fn=lambda m: log.debug(m))
                if not ok:
                    blocked_list.append((acc, "не подключился"))
                    if on_progress:
                        on_progress(idx, total, acc.phone, "не подключился")
                    continue
            # force=False даст probe_spamblock использовать его внутренний 1ч-кеш.
            # Здесь форсируем только если наш 3ч-кеш протух.
            blocked, note = await check_spam_block(acc, force=False)
            if blocked:
                blocked_list.append((acc, note or "спам-блок"))
                log.warning("[pre-check] %s — спам-блок: %s", acc.phone, note or "?")
                if on_progress:
                    on_progress(idx, total, acc.phone, f"спам-блок ({note})")
            else:
                ok_list.append(acc)
                log.info("[pre-check] %s — чисто", acc.phone)
                if on_progress:
                    on_progress(idx, total, acc.phone, "чисто")
            # Пауза между @SpamBot запросами чтобы не зафлудить
            await asyncio.sleep(random.uniform(3, 7))
        except Exception as e:
            log.warning("[pre-check] %s — ошибка проверки: %s", acc.phone, e)
            ok_list.append(acc)
            if on_progress:
                on_progress(idx, total, acc.phone, f"ошибка: {e}")

    log.info("[pre-check] %d живых / %d в спам-блоке (всего %d, из кеша %d)",
             len(ok_list), len(blocked_list), total, cached_n)
    _ad_counter_inc("pre_check_runs")
    _ad_counter_inc("pre_check_blocked", len(blocked_list))
    return ok_list, blocked_list


# ── Универсальная конфигурация для всех модулей ──────────────────────────────

class AntiDetectConfig:
    """Конфиг анти-детект-фич, передаётся в job dataclass.

    Каждый модуль читает только те поля что ему релевантны:
    - online: всегда поднимать акк в онлайн пока работает
    - pre_spam_check: проверить акки на спам-блок перед стартом
    - solve_captcha: решать капчу в чатах (для модулей где акк в чатах)
    - read_messages: отмечать сообщения прочитанными после операции
    - view_profiles: имитировать просмотр профиля цели перед действием
    - view_photos:   имитировать просмотр фото профиля цели
    """
    __slots__ = ("online", "pre_spam_check", "solve_captcha",
                 "read_messages", "mark_read", "view_profiles", "view_photos")

    def __init__(self, online: bool = False, pre_spam_check: bool = False,
                 solve_captcha: bool = False, read_messages: bool = False,
                 mark_read: bool = False, view_profiles: bool = False,
                 view_photos: bool = False):
        self.online = online
        self.pre_spam_check = pre_spam_check
        self.solve_captcha = solve_captcha
        self.read_messages = read_messages
        self.mark_read = mark_read
        self.view_profiles = view_profiles
        self.view_photos = view_photos

    @classmethod
    def from_dict(cls, d: dict | None) -> "AntiDetectConfig":
        d = d or {}
        return cls(
            online=bool(d.get("online", False)),
            pre_spam_check=bool(d.get("pre_spam_check", False)),
            solve_captcha=bool(d.get("solve_captcha", False)),
            read_messages=bool(d.get("read_messages", False)),
            mark_read=bool(d.get("mark_read", False)),
            view_profiles=bool(d.get("view_profiles", False)),
            view_photos=bool(d.get("view_photos", False)),
        )

    def to_dict(self) -> dict:
        return {k: getattr(self, k) for k in self.__slots__}

    def is_active(self) -> bool:
        return any(getattr(self, k) for k in self.__slots__)
