"""
core/profile.py — заполнение профилей + автоответчик.
"""
import asyncio
import time
import logging
import random
from typing import Optional, List, Dict
from pathlib import Path

from telethon import functions, types, events, errors
import logging
# Свой логгер «telepush.autoreply» — UI-консоль роутит на вкладку «Авто-ответ»,
# а не «Системные». Раньше шли через общий `telepush`, клиент не понимал
# где смотреть автоответчик.
logger = logging.getLogger("telepush.autoreply")
from bot_ui.utils import spintax_expand
from utils.names import generate_full


# ══════════════════════════════════════════════════════════════════
#  PROFILE FILLER — имя, био, аватарка
# ══════════════════════════════════════════════════════════════════

async def update_profile(acc, first_name: str = None, last_name: str = None,
                         about: str = None, username: str = None) -> dict:
    """Обновить имя/фамилию/био/username аккаунта."""
    if not acc.client:
        return {"ok": False, "error": "не подключён"}

    result = {"ok": True, "changes": []}

    try:
        kwargs = {}
        if first_name is not None:
            kwargs["first_name"] = first_name
        if last_name is not None:
            kwargs["last_name"] = last_name
        if about is not None:
            kwargs["about"] = about

        if kwargs:
            await acc.client(functions.account.UpdateProfileRequest(**kwargs))
            if first_name is not None:
                acc.first_name = first_name
                result["changes"].append(f"Имя: {first_name}")
            if last_name is not None:
                acc.last_name = last_name
                result["changes"].append(f"Фамилия: {last_name}")
            if about is not None:
                result["changes"].append(f"Био: {about[:30]}...")

        # Username — отдельный RPC UpdateUsernameRequest. Может падать с UsernameOccupiedError.
        if username:
            u = username.lstrip("@").strip()
            try:
                await acc.client(functions.account.UpdateUsernameRequest(username=u))
                acc.username = u
                result["changes"].append(f"Username: @{u}")
            except Exception as _ue:
                # Не роняем весь update из-за занятого username — просто добавляем warning
                result["username_error"] = f"username '@{u}' занят или невалиден: {type(_ue).__name__}"

        logger.info("[profile] %s: обновлён (%s)", acc.phone, ", ".join(result["changes"]))
    except Exception as e:
        result["ok"] = False
        result["error"] = str(e)
        logger.warning("[profile] %s: ошибка %s", acc.phone, e)

    return result


async def update_username(acc, username: str) -> dict:
    """Обновить @username аккаунта."""
    if not acc.client:
        return {"ok": False, "error": "не подключён"}

    try:
        username = username.lstrip("@").strip()
        await acc.client(functions.account.UpdateUsernameRequest(username=username))
        acc.username = username
        logger.info("[profile] %s: username -> @%s", acc.phone, username)
        return {"ok": True, "username": username}
    except Exception as e:
        return {"ok": False, "error": str(e)}


async def update_avatar(acc, photo_bytes: bytes) -> dict:
    """Загрузить новую аватарку."""
    if not acc.client:
        return {"ok": False, "error": "не подключён"}

    try:
        file = await acc.client.upload_file(photo_bytes, file_name="avatar.jpg")
        await acc.client(functions.photos.UploadProfilePhotoRequest(file=file))
        acc.avatar_cached = None  # Сбросить кэш
        logger.info("[profile] %s: аватарка обновлена", acc.phone)
        return {"ok": True}
    except Exception as e:
        return {"ok": False, "error": str(e)}


async def delete_avatar(acc) -> dict:
    """Удалить аватарку."""
    if not acc.client:
        return {"ok": False, "error": "не подключён"}

    try:
        photos = await acc.client(functions.photos.GetUserPhotosRequest(
            user_id="me", offset=0, max_id=0, limit=1
        ))
        if photos.photos:
            await acc.client(functions.photos.DeletePhotosRequest(
                id=[types.InputPhoto(
                    id=photos.photos[0].id,
                    access_hash=photos.photos[0].access_hash,
                    file_reference=photos.photos[0].file_reference,
                )]
            ))
        logger.info("[profile] %s: аватарка удалена", acc.phone)
        return {"ok": True}
    except Exception as e:
        return {"ok": False, "error": str(e)}


async def fill_profiles_bulk(accounts: list, first_name: str = None,
                             last_name: str = None, about: str = None,
                             photo_bytes: bytes = None,
                             gen_name: bool = False, gen_surname: bool = False,
                             progress_cb=None) -> dict:
    """Массовое заполнение профилей для списка аккаунтов.
    gen_name/gen_surname — генерировать случайные имена/фамилии для каждого акка.
    Спинтакс в first_name/last_name/about раскрывается заново для каждого аккаунта.
    """
    from core.client_builder import ensure_connected

    results = {"total": len(accounts), "ok": 0, "failed": 0, "details": {}}

    for i, acc in enumerate(accounts):
        phone = acc.phone
        try:
            if not await ensure_connected(acc):
                results["details"][phone] = "offline"
                results["failed"] += 1
                continue

            # Генерация / спинтакс для каждого акка
            gender = random.choice(["male", "female"])
            _fn = None
            _ln = None
            _ab = None

            if gen_name:
                _gn, _gs = generate_full(gender)
                _fn = _gn
                if gen_surname:
                    _ln = _gs
            elif first_name:
                _fn = spintax_expand(first_name)

            if gen_surname and not gen_name:
                _, _ln = generate_full(gender)
            elif last_name and not gen_surname:
                _ln = spintax_expand(last_name)

            if about:
                _ab = spintax_expand(about)

            # Имя/био
            if _fn or _ln or _ab:
                r = await update_profile(acc, _fn, _ln, _ab)
                if not r["ok"]:
                    results["details"][phone] = r.get("error", "fail")
                    results["failed"] += 1
                    continue

            # Аватарка
            if photo_bytes:
                r = await update_avatar(acc, photo_bytes)
                if not r["ok"]:
                    results["details"][phone] = "avatar: " + r.get("error", "fail")
                    results["failed"] += 1
                    continue

            results["details"][phone] = "ok"
            results["ok"] += 1
        except Exception as e:
            results["details"][phone] = str(e)
            results["failed"] += 1

        if progress_cb:
            await progress_cb(i + 1, len(accounts))
        await asyncio.sleep(random.uniform(1, 3))

    return results


# ══════════════════════════════════════════════════════════════════
#  AUTO-RESPONDER — автоответчик на входящие ЛС
# ══════════════════════════════════════════════════════════════════

# Global state per account
_autoresponders: Dict[str, dict] = {}  # phone -> {handler, config, replied_to}


async def start_autoresponder(acc, text: str, delay_min: float = 3,
                              delay_max: float = 8, once_per_user: bool = True,
                              backfill_hours: float = 0,
                              stop_words: list = None,
                              simulate_typing: bool = False,
                              mark_as_read: bool = False) -> dict:
    """Запустить автоответчик для аккаунта.

    backfill_hours > 0: перед регистрацией handler-а сканируем входящие ЛС
    за последние N часов и отвечаем на те, на которые мы ещё не отвечали.

    stop_words: если входящее сообщение содержит любое из этих слов
    (case-insensitive) — не отвечать. Полезно отсекать «стоп», «отстань», etc.

    simulate_typing: перед отправкой показать индикатор «печатает...» — выглядит
    как реальный человек который набирает текст.

    mark_as_read: отметить входящее прочитанным сразу после получения. Без этого
    у юзера в чате висит непрочитанный счётчик — выдаёт автоответ.
    """
    if not acc.client:
        return {"ok": False, "error": "не подключён"}
    stop_words = [w.strip().lower() for w in (stop_words or []) if w and w.strip()]

    phone = acc.phone

    # Остановить если уже работает
    if phone in _autoresponders:
        await stop_autoresponder(acc)

    replied_to = set()
    backfill_count = 0

    # Backfill (сканирование старых диалогов на пропущенные ЛС) — самая тяжёлая
    # операция автоответчика. Раньше делалась СИНХРОННО до регистрации handler
    # и до возврата управления — на 10 акках одновременно блокировала Telethon
    # на минуту+, в это время параллельная DM-рассылка получала пустые ответы
    # на ResolveUsernameRequest («No user has X») для абсолютно валидных юзеров.
    # Решение: handler регистрируем СРАЗУ (incoming сообщения сразу обрабатываются),
    # backfill выносим в фон через create_task с 30-сек задержкой, чтобы дать
    # рассылке стартовать без конкуренции за MTProto-сессию.
    async def _do_backfill():
        nonlocal backfill_count
        if backfill_hours <= 0:
            return
        # 30 сек форы, чтобы текущая рассылка успела стартовать.
        await asyncio.sleep(30)
        cutoff_ts = time.time() - (backfill_hours * 3600)
        try:
            async for dialog in acc.client.iter_dialogs(limit=200):
                try:
                    if not dialog.is_user:
                        continue
                    entity = dialog.entity
                    if getattr(entity, "bot", False) or getattr(entity, "is_self", False):
                        continue
                    last_msg = dialog.message
                    if not last_msg or last_msg.out:
                        continue
                    msg_ts = last_msg.date.timestamp() if last_msg.date else 0
                    if msg_ts < cutoff_ts:
                        continue
                    sender_id = getattr(entity, "id", None)
                    if not sender_id or sender_id in replied_to:
                        continue
                    await asyncio.sleep(random.uniform(delay_min, delay_max))
                    msg = spintax_expand(text)
                    try:
                        await acc.client.send_message(entity, msg, reply_to=last_msg.id)
                        replied_to.add(sender_id)
                        backfill_count += 1
                        logger.info("[autoresponder] %s: backfill ответ %s (%s)",
                                    phone, sender_id, last_msg.date)
                    except errors.FloodWaitError as fw:
                        logger.warning("[autoresponder] %s: backfill FloodWait %ds — стоп",
                                       phone, fw.seconds)
                        break
                    except Exception as _se:
                        logger.debug("[autoresponder] %s: backfill send error %s",
                                     phone, _se)
                except Exception:
                    continue
        except Exception as e:
            logger.debug("[autoresponder] %s: backfill scan error %s", phone, e)

    async def _handler(event):
        # Диагностика: что прилетело в handler. Видно в консоли «Авто-ответ»
        # → клиент сразу понимает, доходят ли вообще события и почему
        # фильтруются.
        try:
            _is_priv = bool(getattr(event, "is_private", False))
            _is_out = bool(getattr(event, "out", False))
            _sid = getattr(event, "sender_id", None) or getattr(getattr(event, "message", None), "sender_id", None)
            logger.info("[autoresponder] %s: event sender=%s private=%s out=%s",
                        phone, _sid, _is_priv, _is_out)
        except Exception:
            pass

        try:
            # Только личные сообщения
            if not event.is_private:
                logger.info("[autoresponder] %s: пропуск — не личка (group/channel)", phone)
                return
            # Не отвечаем на свои (event.out=True значит наша же отправка)
            if event.out:
                logger.info("[autoresponder] %s: пропуск — наше же исходящее", phone)
                return
            sender_id = getattr(event, "sender_id", None) or getattr(event.message, "sender_id", None)
            if not sender_id:
                logger.info("[autoresponder] %s: пропуск — не определился sender_id", phone)
                return

            if once_per_user and sender_id in replied_to:
                logger.info("[autoresponder] %s: пропуск %s — уже отвечали", phone, sender_id)
                return

            # Стоп-слова: если входящее содержит любое — не отвечаем
            try:
                _msg_text = (getattr(event, 'raw_text', None) or '').lower()
                if stop_words and any(sw in _msg_text for sw in stop_words):
                    logger.info("[autoresponder] %s: пропуск %s — стоп-слово в сообщении", phone, sender_id)
                    replied_to.add(sender_id)  # запомним чтобы не дёргался по повторным
                    return
            except Exception:
                pass

            # Резервируем сразу, чтобы при шторме входящих не отправить дважды
            replied_to.add(sender_id)

            # Mark as read — отметим входящее прочитанным сразу
            if mark_as_read:
                try:
                    await acc.client.send_read_acknowledge(event.chat_id, event.message)
                except Exception as _re:
                    logger.debug("[autoresponder] %s: mark_read err %s", phone, _re)

            delay = random.uniform(delay_min, delay_max)
            logger.info("[autoresponder] %s: отвечаю %s через %.1fs", phone, sender_id, delay)

            # Имитация набора: typing indicator на 70% времени задержки
            if simulate_typing:
                from telethon.tl.functions.messages import SetTypingRequest
                from telethon.tl.types import SendMessageTypingAction
                typing_time = max(0.5, delay * 0.7)
                # Ждём небольшую паузу перед стартом «набора»
                await asyncio.sleep(max(0.0, delay - typing_time))
                try:
                    await acc.client(SetTypingRequest(peer=event.chat_id,
                                                      action=SendMessageTypingAction()))
                except Exception:
                    pass
                await asyncio.sleep(typing_time)
            else:
                await asyncio.sleep(delay)

            msg = spintax_expand(text)
            await event.reply(msg)

            logger.info("[autoresponder] %s: ответ отправлен %s (всего %d)",
                        phone, sender_id, len(replied_to))
        except Exception as e:
            logger.warning("[autoresponder] %s: ❌ ошибка в handler: %s", phone, e, exc_info=True)
            # Снимаем claim, чтобы при следующем сообщении от этого юзера
            # дать ещё один шанс ответить.
            try:
                if sender_id and sender_id in replied_to:
                    replied_to.discard(sender_id)
            except Exception:
                pass

    # 1. Сначала регистрируем handler — incoming сообщения сразу обрабатываются.
    acc.client.add_event_handler(_handler, events.NewMessage(incoming=True))

    # 2. Backfill в фоне (если включён). Не блокирует возврат управления и
    #    не конкурирует с одновременно стартующей рассылкой за MTProto-сессию.
    _backfill_task = None
    if backfill_hours > 0:
        _backfill_task = asyncio.create_task(_do_backfill())
        # Без done-callback Python ругается только при GC «Task exception was
        # never retrieved», и баг прячется. Логируем явно.
        def _bf_done(t):
            if t.cancelled(): return
            exc = t.exception()
            if exc:
                logger.error("[profile] backfill task crashed for %s: %s", phone, exc, exc_info=exc)
        _backfill_task.add_done_callback(_bf_done)

    _autoresponders[phone] = {
        "handler": _handler,
        "text": text,
        "delay_min": delay_min,
        "delay_max": delay_max,
        "once_per_user": once_per_user,
        "replied_to": replied_to,
        "started_at": time.time(),
        "backfill_hours": backfill_hours,
        "backfill_count": backfill_count,
        "backfill_task": _backfill_task,
    }

    logger.info("[autoresponder] %s: запущен (текст: %s..., backfill=%dч в фоне через 30с)",
                phone, text[:30], backfill_hours)
    return {"ok": True, "backfill_scheduled": backfill_hours > 0}


async def stop_autoresponder(acc) -> dict:
    """Остановить автоответчик."""
    phone = acc.phone
    info = _autoresponders.pop(phone, None)
    if info and acc.client:
        try:
            acc.client.remove_event_handler(info["handler"])
        except Exception:
            pass
        # Прерываем фоновый backfill если ещё крутится
        _bt = info.get("backfill_task")
        if _bt and not _bt.done():
            _bt.cancel()
    logger.info("[autoresponder] %s: остановлен", phone)
    return {"ok": True, "replied": len(info["replied_to"]) if info else 0}


def get_autoresponder_status(phone: str) -> dict:
    """Статус автоответчика."""
    info = _autoresponders.get(phone)
    if not info:
        return {"active": False}
    return {
        "active": True,
        "text": info["text"],
        "replied": len(info["replied_to"]),
        "uptime": int(time.time() - info["started_at"]),
    }


def get_all_autoresponders() -> dict:
    """Все активные автоответчики."""
    return {
        phone: {
            "text": info["text"][:50],
            "replied": len(info["replied_to"]),
            "uptime": int(time.time() - info["started_at"]),
        }
        for phone, info in _autoresponders.items()
    }
