"""
checker.py — Чекер аккаунтов через @SpamBot.

Все папки строго внутри ACCOUNTS_DIR — никаких выходов наружу через ../ или
абсолютные пути. См. _safe_under().

Возможности:
1. Проверка статуса аккаунта (живой / временный спамблок / вечный спамблок / заморожен)
2. Авто-подача заявки на снятие временного спамблока (тексты из настроек)
3. Авто-снятие заморозки через DeepSeek-AI (генерит ответы на вопросы @SpamBot)
4. Капча через RuCaptcha (когда @SpamBot просит)
5. Сортировка аккаунтов по папкам результата (Живые / Спамблок / Замороженные)
6. Удаление диалога с @SpamBot после проверки
7. Переименование session/json если в имени неверный номер

Хранение настроек: ctx.user_settings["checker"] = {
    "rucaptcha_key": str,
    "deepseek_key": str,
    "spamblock_texts": [str, ...],
    "freeze_prompt": str,
    "check_spamblock": bool,
    "submit_spamblock_appeal": bool,
    "submit_freeze_appeal": bool,
    "use_hacked_reason": bool,
    "use_mistake_reason": bool,
    "sort_by_folders": bool,
    "rename_files": bool,
    "delete_spambot_dialog": bool,
    "folder_alive": str,
    "folder_temp_spam": str,
    "folder_eternal_spam": str,
    "folder_frozen": str,
}
"""
from __future__ import annotations

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

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


def _safe_under(user_input: str, base: Path) -> Optional[Path]:
    """Возвращает путь относительно base, гарантируя что результат ВНУТРИ base.

    Принимает:
      - "Живые"           → base/Живые
      - "Аккаунты/Живые"  → base/Живые (префикс "Аккаунты/" игнорируется)
      - "Подпапка/X/Y"    → base/Подпапка/X/Y
    Отвергает:
      - "../escape"           → None (path traversal)
      - "C:/Other/Folder"     → None (абсолютный путь вне base)
      - пустую строку         → None
    """
    if not user_input or not str(user_input).strip():
        return None
    raw = str(user_input).strip().replace("\\", "/").lstrip("/")
    # Снимаем префикс "Аккаунты/" — base УЖЕ это и есть Аккаунты
    for prefix in ("Аккаунты/", "Accounts/"):
        if raw.startswith(prefix):
            raw = raw[len(prefix):]
            break
    if not raw:
        return None
    # Резолвим и проверяем что внутри base
    try:
        candidate = (base / raw).resolve()
        base_resolved = base.resolve()
        # Python 3.9+: is_relative_to
        try:
            if not candidate.is_relative_to(base_resolved):
                return None
        except AttributeError:
            # Старые версии
            try:
                candidate.relative_to(base_resolved)
            except ValueError:
                return None
        return candidate
    except Exception:
        return None


# ── Анализ ответа @SpamBot ──────────────────────────────────────────────────

# Маркеры ответов @SpamBot (русский + английский)
_ALIVE_MARKERS = [
    "all is good", "всё в порядке", "всё хорошо", "your account is free",
    "you're free as a bird", "no limits",
]
_TEMP_SPAM_MARKERS = [
    "limit", "ограничение", "временно", "automated mechanism",
    "automatic system", "your account is now limited",
]
_ETERNAL_SPAM_MARKERS = [
    "permanent", "постоянно", "indefinitely", "indefinite",
    "permanently", "вечно", "forever",
]
_FREEZE_MARKERS = [
    "frozen", "заморожен", "deactivat", "блокирован",
    "your account has been frozen", "limited indefinitely",
]
_CAPTCHA_MARKERS = [
    "captcha", "капча", "verify you", "i'm not a bot",
    "пройди проверку",
]


def classify_spambot_response(text: str) -> str:
    """Классифицирует ответ @SpamBot.

    Returns: "alive" | "temp_spam" | "eternal_spam" | "frozen" | "captcha" | "unknown"
    """
    if not text:
        return "unknown"
    t = text.lower()
    for m in _CAPTCHA_MARKERS:
        if m in t:
            return "captcha"
    for m in _FREEZE_MARKERS:
        if m in t:
            return "frozen"
    for m in _ETERNAL_SPAM_MARKERS:
        if m in t:
            return "eternal_spam"
    for m in _TEMP_SPAM_MARKERS:
        if m in t:
            return "temp_spam"
    for m in _ALIVE_MARKERS:
        if m in t:
            return "alive"
    return "unknown"


# ── DeepSeek для генерации ответов на @SpamBot ─────────────────────────────

async def deepseek_answer(api_key: str, question: str, context: str = "") -> Optional[str]:
    """Генерирует ответ для @SpamBot через DeepSeek API.

    Пытается дать «человеческий» ответ на вопрос (e.g. «когда вы в последний
    раз использовали аккаунт?», «у вас есть друзья в Telegram?»).
    """
    if not api_key:
        return None
    try:
        import aiohttp
        prompt = (
            "Ты — обычный пользователь Telegram. Тебе пишет техподдержка @SpamBot и "
            "задаёт вопросы чтобы снять заморозку с твоего аккаунта. "
            "Отвечай коротко (1-2 предложения), естественно, по-человечески, "
            "без формальностей. " + (context + "\n\n" if context else "") +
            f"Вопрос: {question}\n\nТвой ответ:"
        )
        async with aiohttp.ClientSession() as session:
            resp = await session.post(
                "https://api.deepseek.com/v1/chat/completions",
                headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
                json={
                    "model": "deepseek-chat",
                    "messages": [{"role": "user", "content": prompt}],
                    "temperature": 0.8,
                    "max_tokens": 150,
                },
                timeout=aiohttp.ClientTimeout(total=20),
            )
            if resp.status != 200:
                logger.debug("[checker] DeepSeek %d: %s", resp.status, await resp.text())
                return None
            data = await resp.json()
            return (data.get("choices") or [{}])[0].get("message", {}).get("content", "").strip()
    except Exception as e:
        logger.debug("[checker] DeepSeek error: %s", e)
        return None


# ── RuCaptcha для решения капчи ────────────────────────────────────────────

async def rucaptcha_solve(api_key: str, image_bytes: bytes) -> Optional[str]:
    """Отправляет картинку в RuCaptcha, ждёт решения (до 60с)."""
    if not api_key or not image_bytes:
        return None
    try:
        import aiohttp
        import base64
        async with aiohttp.ClientSession() as session:
            # 1. Отправка
            data = aiohttp.FormData()
            data.add_field("key", api_key)
            data.add_field("method", "base64")
            data.add_field("body", base64.b64encode(image_bytes).decode())
            data.add_field("json", "1")
            async with session.post("https://rucaptcha.com/in.php", data=data, timeout=aiohttp.ClientTimeout(total=15)) as r:
                jr = await r.json()
                if jr.get("status") != 1:
                    return None
                cid = jr.get("request")
            # 2. Опрос результата
            for _ in range(30):
                await asyncio.sleep(3)
                async with session.get(f"https://rucaptcha.com/res.php?key={api_key}&action=get&id={cid}&json=1") as r:
                    jr = await r.json()
                    if jr.get("status") == 1:
                        return jr.get("request")
                    if jr.get("request") and "ERROR" in str(jr.get("request")):
                        return None
        return None
    except Exception as e:
        logger.debug("[checker] RuCaptcha error: %s", e)
        return None


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

@dataclass
class CheckResult:
    phone: str
    status: str = "unknown"  # alive / temp_spam / eternal_spam / frozen / captcha / unknown / error
    raw_response: str = ""
    actions: List[str] = field(default_factory=list)
    error: str = ""


async def check_one_account(client, phone: str, settings: dict) -> CheckResult:
    """Проверяет один аккаунт через @SpamBot, опционально подаёт заявки."""
    result = CheckResult(phone=phone)
    try:
        # 1. Стучимся в @SpamBot
        from telethon.tl.functions.messages import StartBotRequest
        try:
            spambot = await client.get_entity("@SpamBot")
        except Exception as e:
            result.error = f"не нашёл @SpamBot: {e}"
            return result

        # /start (если ещё нет диалога)
        await client.send_message(spambot, "/start")
        await asyncio.sleep(random.uniform(2, 4))

        # Ответ
        msgs = await client.get_messages(spambot, limit=1)
        if not msgs:
            result.error = "@SpamBot не ответил"
            return result
        response = msgs[0].text or ""
        result.raw_response = response[:500]
        result.status = classify_spambot_response(response)
        result.actions.append(f"статус: {result.status}")

        # 2. Если временный спам-блок и опция включена — подаём заявку
        if result.status == "temp_spam" and settings.get("submit_spamblock_appeal"):
            texts = settings.get("spamblock_texts") or []
            if texts:
                appeal_text = random.choice(texts)
                # @SpamBot отвечает кнопками: «Yes / No» — жмём «No»
                if msgs[0].reply_markup:
                    rows = msgs[0].reply_markup.rows or []
                    for row in rows:
                        for btn in (row.buttons or []):
                            if btn.text and ("no" in btn.text.lower() or "нет" in btn.text.lower()):
                                from telethon.tl.functions.messages import GetBotCallbackAnswerRequest
                                try:
                                    await client(GetBotCallbackAnswerRequest(peer=spambot, msg_id=msgs[0].id, data=btn.data))
                                except Exception:
                                    pass
                                await asyncio.sleep(2)
                                break
                # Отправляем текст заявки
                await client.send_message(spambot, appeal_text)
                result.actions.append(f"заявка: '{appeal_text[:40]}...'")
                await asyncio.sleep(random.uniform(3, 6))

        # 3. Если заморозка и опция включена — диалог через DeepSeek
        if result.status == "frozen" and settings.get("submit_freeze_appeal"):
            deepseek_key = settings.get("deepseek_key", "").strip()
            if deepseek_key:
                # Выбираем причину
                reasons = []
                if settings.get("use_hacked_reason"):
                    reasons.append("Аккаунт был взломан")
                if settings.get("use_mistake_reason"):
                    reasons.append("Это ошибка")
                if not reasons:
                    reasons = ["Это ошибка"]
                first_reason = random.choice(reasons)
                await client.send_message(spambot, first_reason)
                result.actions.append(f"причина: '{first_reason}'")

                # Цикл диалога — до 5 ответов
                context = settings.get("freeze_prompt") or "Я обычный пользователь, аккаунтом пользуюсь редко."
                for i in range(5):
                    await asyncio.sleep(random.uniform(4, 8))
                    try:
                        msgs = await client.get_messages(spambot, limit=1)
                        if not msgs:
                            break
                        question = msgs[0].text or ""
                        if classify_spambot_response(question) == "alive":
                            result.status = "alive"
                            result.actions.append("заморозка снята!")
                            break
                        ans = await deepseek_answer(deepseek_key, question, context)
                        if not ans:
                            break
                        await client.send_message(spambot, ans[:500])
                        result.actions.append(f"ответ #{i+1}: '{ans[:30]}...'")
                    except Exception as fe:
                        logger.debug("[checker] freeze loop %s: %s", phone, fe)
                        break

        return result
    except Exception as e:
        result.error = str(e)[:120]
        return result


async def run_checker(ctx, job, log_fn):
    """Главный воркер чекера. Проходит по аккаунтам, проверяет, сортирует."""
    settings = job.config or {}
    target_phones = list(job.target_phones or [])

    if not target_phones:
        # Берём все живые
        target_phones = [p for p, a in ctx.accounts.items() if not a.dead and not a.frozen]

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

    job.total = len(target_phones)
    job.done = 0
    job.alive_count = 0
    job.temp_spam_count = 0
    job.eternal_spam_count = 0
    job.frozen_count = 0
    job.error_count = 0

    log_fn(f"Чекер: {len(target_phones)} аккаунтов")

    from core.client_builder import ensure_connected, release_after_task
    from config import ACCOUNTS_DIR
    import shutil as _sh

    folder_map = {
        "alive": settings.get("folder_alive", "Аккаунты/Живые"),
        "temp_spam": settings.get("folder_temp_spam", "Аккаунты/Спамблок/Временный"),
        "eternal_spam": settings.get("folder_eternal_spam", "Аккаунты/Спамблок/Вечный"),
        "frozen": settings.get("folder_frozen", "Аккаунты/Замороженные"),
    }

    for phone in target_phones:
        if job.status != "running":
            break
        acc = ctx.accounts.get(phone)
        if not acc:
            job.error_count += 1
            job.done += 1
            log_fn(f"✗ {phone}: акк не найден")
            continue
        if not await ensure_connected(acc):
            job.error_count += 1
            job.done += 1
            log_fn(f"✗ {phone}: не подключился")
            continue

        try:
            res = await check_one_account(acc.client, phone, settings)

            # Лог
            status_emoji = {"alive": "✓", "temp_spam": "⏳", "eternal_spam": "✗", "frozen": "❄", "captcha": "🤖"}.get(res.status, "?")
            log_fn(f"{status_emoji} {phone}: {res.status}" + (f" ({', '.join(res.actions[:2])})" if res.actions else ""))

            # Счётчики
            if res.status == "alive":
                job.alive_count += 1
            elif res.status == "temp_spam":
                job.temp_spam_count += 1
            elif res.status == "eternal_spam":
                job.eternal_spam_count += 1
            elif res.status == "frozen":
                job.frozen_count += 1
            else:
                job.error_count += 1

            # Сортировка по папкам — только внутри ACCOUNTS_DIR
            if settings.get("sort_by_folders") and res.status in folder_map:
                target_dir = _safe_under(folder_map[res.status], ACCOUNTS_DIR)
                if not target_dir:
                    log_fn(f"  ⚠ {phone}: невалидный путь '{folder_map[res.status]}' — пропускаю перемещение")
                else:
                    target_dir.mkdir(parents=True, exist_ok=True)
                    try:
                        if acc.session_path and acc.session_path.exists():
                            _sh.move(str(acc.session_path), str(target_dir / acc.session_path.name))
                            if acc.json_path and acc.json_path.exists():
                                _sh.move(str(acc.json_path), str(target_dir / acc.json_path.name))
                            log_fn(f"  ↪ {phone} → {target_dir.name}")
                    except Exception as me:
                        log_fn(f"  ⚠ {phone}: move failed ({me})")

            # Удаление диалога с @SpamBot
            if settings.get("delete_spambot_dialog") and res.status == "alive":
                try:
                    await acc.client.delete_dialog("@SpamBot")
                except Exception:
                    pass

            # Переименование файлов session/json (если в имени неверный номер)
            if settings.get("rename_files"):
                try:
                    me = await acc.client.get_me()
                    real_phone = "+" + str(me.phone).lstrip("+")
                    if acc.session_path and acc.session_path.stem != real_phone.lstrip("+"):
                        new_name = real_phone.lstrip("+") + acc.session_path.suffix
                        new_path = acc.session_path.parent / new_name
                        if not new_path.exists():
                            _sh.move(str(acc.session_path), str(new_path))
                            log_fn(f"  ↻ {phone}: переименован в {new_name}")
                except Exception as re:
                    logger.debug("[checker] rename %s: %s", phone, re)
        except Exception as e:
            job.error_count += 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(2, 5))

    if job.status == "running":
        job.status = "finished"
    log_fn(
        f"Чекер завершён. Живых: {job.alive_count}, "
        f"Врем.спам: {job.temp_spam_count}, "
        f"Вечн.спам: {job.eternal_spam_count}, "
        f"Замороженных: {job.frozen_count}, "
        f"Ошибок: {job.error_count}"
    )
