"""
account_helpers.py — хелперы для аккаунтов: проверка членства, safe_join.
Редактируй логику вступления в каналы здесь, не трогая остальной код.
"""
import asyncio
import random
from typing import Optional, Tuple

from telethon import errors
from telethon.tl.functions.channels import JoinChannelRequest
from telethon.tl.functions.messages import ImportChatInviteRequest, CheckChatInviteRequest
from telethon.tl import types as tltypes

try:
    from telethon.errors.rpcerrorlist import AuthKeyDuplicatedError
except ImportError:
    class AuthKeyDuplicatedError(Exception): pass  # type: ignore
try:
    from telethon.errors.rpcerrorlist import FrozenMethodInvalidError
except ImportError:
    class FrozenMethodInvalidError(Exception): pass  # type: ignore
try:
    from telethon.errors.rpcerrorlist import FrozenParticipantMissingError
except ImportError:
    class FrozenParticipantMissingError(Exception): pass  # type: ignore

from config import (
    JOIN_MIN_INTERVAL_SEC, JOIN_JITTER_SEC, FLOODWAIT_PENALTY_FACTOR,
    JOIN_INVITE_PENALTY_MIN, JOIN_INVITE_PENALTY_MAX,
    JOIN_MAX_PER_SESSION, JOIN_SESSION_BREAK_MIN, JOIN_SESSION_BREAK_MAX,
)
from bot_ui.logger_setup import logger
from models import Account
from core.database import store, is_allowed_by_quota
from core.client_builder import safe_disconnect
from core.captcha import watch_and_solve_captcha
import state

# _FrozenParticipantMissingError — alias для единообразия с patches
_FrozenParticipantMissingError = FrozenParticipantMissingError

# Счётчик сессий подписок для длинных пауз
_join_session_count: dict = {}  # phone -> int


# TTL-кэш для refresh_known_ids (phone -> timestamp)
import time as _time_mod
_known_ids_refresh_ts: dict = {}
KNOWN_IDS_TTL = 90  # обновлять не чаще 1 раза в 90 сек

async def refresh_known_ids(acc: Account, force: bool = False):
    """Обновляет список известных dialog/entity id у аккаунта.
    
    Кэширует результат на KNOWN_IDS_TTL секунд чтобы не делать iter_dialogs
    при каждом вступлении. force=True игнорирует кэш.
    """
    phone = getattr(acc, "phone", "?")
    now = _time_mod.time()
    
    # Пропускаем если данные свежие и не форсировано
    if not force and acc.known_ids and (_known_ids_refresh_ts.get(phone, 0) + KNOWN_IDS_TTL > now):
        return
    
    acc.known_ids.clear()
    if acc.client is None:
        return
    try:
        # Ограничиваем 600 диалогами — хватает для 99% случаев, намного быстрее
        async for d in acc.client.iter_dialogs(limit=600):
            ent = d.entity
            if hasattr(ent, "id"):
                acc.known_ids.add(int(ent.id))
        _known_ids_refresh_ts[phone] = _time_mod.time()
    except (errors.AuthKeyUnregisteredError, errors.SessionRevokedError, AuthKeyDuplicatedError) as e:
        try:
            acc.dead = True
            acc.dead_reason = type(e).__name__
            await safe_disconnect(acc.client)
        except Exception:
            pass
    except Exception as e:
        try:
            logger.warning("refresh_known_ids failed for %s: %s", phone, e)
        except Exception:
            pass

async def is_already_member(acc: Account, target: dict) -> bool:
    """Проверяет членство по кэшу known_ids без лишних API-вызовов."""
    if target["kind"] == "invite":
        return False
    if acc.client is None:
        return False
    try:
        uname = target["username"]
        if not uname.startswith("@"):
            uname = "@" + uname
        # Если known_ids пусто — обновляем (с TTL-кэшем, не бьёт API лишний раз)
        if not acc.known_ids:
            await refresh_known_ids(acc)
        # Пробуем найти entity в кэше Telethon (без API-запроса)
        try:
            ent = await acc.client.get_input_entity(uname)
        except (ValueError, KeyError):
            return False
        ent_id = getattr(ent, "channel_id", None) or getattr(ent, "chat_id", None) or getattr(ent, "user_id", None)
        if ent_id and int(ent_id) in acc.known_ids:
            return True
        return False
    except Exception:
        return False


def _extract_chat_id_from_updates(upd) -> Optional[int]:
    """Best-effort: fetch chat/channel id from a result of join/import requests."""
    try:
        # 1) direct chats list
        for ch in getattr(upd, "chats", []) or []:
            if isinstance(ch, (tltypes.Chat, tltypes.ChatForbidden, tltypes.Channel, tltypes.ChannelForbidden)):
                return int(getattr(ch, "id", 0) or 0) or None
        # 2) dig inside updates -> message.to_id
        for u in getattr(upd, "updates", []) or []:
            msg = getattr(u, "message", None)
            to_id = getattr(msg, "to_id", None) if msg else None
            if isinstance(to_id, tltypes.PeerChannel):
                return int(getattr(to_id, "channel_id", 0) or 0) or None
            if isinstance(to_id, tltypes.PeerChat):
                return int(getattr(to_id, "chat_id", 0) or 0) or None
        # 3) sometimes there is a pts->users; nothing to return
        return None
    except Exception as e:
        try:
            logger.debug("extract_chat_id_from_updates failed: %s", e)
        except Exception:
            pass
        return None

async def safe_join(acc: Account, owner_id: int, target: dict) -> Tuple[str, Optional[str]]:
    if acc.client is None:
        return "failed:client_not_connected", None
    ok, reason = is_allowed_by_quota(owner_id, acc.phone, "join")
    if not ok:
        return f"failed:quota ({reason})", None
    network_retries = 1
    while True:
        try:
            if await is_already_member(acc, target):
                return "already_member", None

            if target["kind"] == "invite":
                # Проверяем инвайт до вступления
                try:
                    invite_info = await acc.client(CheckChatInviteRequest(target["hash"]))
                    # ChatInviteAlready → уже участник
                    if isinstance(invite_info, tltypes.ChatInviteAlready):
                        return "already_member", None
                    # Проверяем тип чата: broadcast-канал бесполезно вступать
                    # При подписке broadcast-каналы допустимы —
                    # проверку "можно ли писать" здесь не делаем (это для рассылки, не join)
                    if isinstance(invite_info, tltypes.ChatInvite):
                        pass  # всегда пробуем вступить
                except errors.InviteHashExpiredError:
                    return "failed:invite_expired", None
                except errors.InviteHashInvalidError:
                    return "failed:invite_invalid", None
                except errors.FloodWaitError as e:
                    return f"floodwait:{e.seconds}", None
                except Exception:
                    pass  # не смогли проверить — пробуем вступить

                upd = await acc.client(ImportChatInviteRequest(target["hash"]))
                chat_id = _extract_chat_id_from_updates(upd)
                if chat_id:
                    acc.known_ids.add(int(chat_id))
                else:
                    await refresh_known_ids(acc)
                note = None
                if chat_id:
                    res = await watch_and_solve_captcha(acc, chat_id)
                    note = f"captcha:{res}"
                store.push_quota_event(owner_id, acc.phone, "join")
                return "joined:invite", note

            if target["kind"] == "public":
                # Resolve and ensure it's a chat/channel (not a user/bot)
                # ВАЖНО: всегда используем @ префикс, иначе Telethon может не распознать
                # username в некоторых версиях (особенно если не в кэше диалогов)
                uname = target["username"]
                if not uname.startswith("@"):
                    uname = "@" + uname

                # ── Retry логика: до 2 повторов при временных сетевых ошибках ──────
                # Telegram иногда временно не резолвит username из-за нагрузки на DC.
                # В таких случаях get_entity бросает OSError/ConnectionError, а не
                # UsernameNotOccupied — поэтому нужен retry перед пометкой как "не найден".
                _get_entity_retries = 2
                ent = None
                while True:
                    try:
                        ent = await acc.client.get_entity(uname)
                        break  # успех
                    except errors.UsernameNotOccupiedError:
                        return "ignored:username_not_found", None
                    except errors.UsernameInvalidError:
                        return "ignored:username_invalid", None
                    except errors.FloodWaitError as _fw:
                        return f"floodwait:{_fw.seconds}", None
                    except ValueError as _ve:
                        # Telethon оборачивает все ошибки разрешения username в ValueError.
                        # Сообщения могут быть разными в зависимости от версии Telethon:
                        #   "Cannot find any entity corresponding to @username"
                        #   "Could not find the input entity for @username"
                        #   "No user has 'username' as username"
                        # Любой из этих случаев означает: юзернейм не существует → пропускаем.
                        _ve_str = str(_ve).lower()
                        _not_found_kws = ("not occupied", "cannot find", "not found", "could not find", "no user", "no entity", "input entity")
                        if any(k in _ve_str for k in _not_found_kws):
                            return "ignored:username_not_found", None
                        # Прочие ValueError от get_entity тоже почти всегда означают "не найден"
                        logger.debug("[safe_join] get_entity неожиданный ValueError для %s: %s", uname, _ve)
                        return "ignored:username_not_found", None
                    except (OSError, ConnectionError, asyncio.TimeoutError) as _net_err:
                        # Временная сетевая ошибка — пробуем переподключиться и повторить
                        if _get_entity_retries > 0:
                            _get_entity_retries -= 1
                            logger.warning(
                                "[safe_join] Сетевая ошибка при get_entity %s: %s — retry (осталось: %d)",
                                uname, _net_err, _get_entity_retries
                            )
                            try:
                                cli = acc.client
                                if cli and not cli.is_connected():
                                    await asyncio.wait_for(cli.connect(), timeout=15.0)
                            except Exception:
                                pass
                            await asyncio.sleep(5.0)
                            continue
                        logger.debug("[safe_join] get_entity сетевая ошибка (retries исчерпаны) для %s: %s", uname, _net_err)
                        return "ignored:cannot_resolve", None
                    except Exception as _ge_err:
                        # Любая другая ошибка: проверяем можно ли retry
                        _err_name = type(_ge_err).__name__
                        _err_str = str(_ge_err).lower()
                        # Временные ошибки Telegram (DC overload и т.п.) — retry
                        _transient = ("timed out", "connection", "network", "reset", "broken pipe", "server")
                        if _get_entity_retries > 0 and any(k in _err_str for k in _transient):
                            _get_entity_retries -= 1
                            logger.warning(
                                "[safe_join] Временная ошибка get_entity %s: %s — retry (осталось: %d)",
                                uname, _ge_err, _get_entity_retries
                            )
                            try:
                                cli = acc.client
                                if cli and not cli.is_connected():
                                    await asyncio.wait_for(cli.connect(), timeout=15.0)
                            except Exception:
                                pass
                            await asyncio.sleep(5.0)
                            continue
                        logger.debug("[safe_join] get_entity error для %s: %s", uname, _ge_err)
                        return f"failed:{_err_name}", None
                # ──────────────────────────────────────────────────────────────────

                if not isinstance(ent, (tltypes.Chat, tltypes.ChatForbidden, tltypes.Channel)):
                    return "ignored:not_a_chat", None

                # Намеренно НЕ проверяем can_write_to_chat здесь.
                # can_write_to_chat() — фильтр для РАССЫЛКИ (spam), не для ПОДПИСОК.
                # Broadcast-каналы (новости, объявления) — валидная цель для подписки.
                upd = await acc.client(JoinChannelRequest(ent))
                chat_id = _extract_chat_id_from_updates(upd)
                if not chat_id and hasattr(ent, "id"):
                    chat_id = int(getattr(ent, "id"))

                if chat_id:
                    acc.known_ids.add(int(chat_id))
                else:
                    await refresh_known_ids(acc)
                note = None
                if chat_id:
                    res = await watch_and_solve_captcha(acc, chat_id)
                    note = f"captcha:{res}"
                store.push_quota_event(owner_id, acc.phone, "join")
                return "joined:public", note

            return "ignored:unknown", None

        except (ConnectionError, OSError) as e:
            if network_retries > 0:
                network_retries -= 1
                try:
                    await asyncio.sleep(1.5)
                    if acc.client:
                        await acc.client.connect()
                except Exception:
                    pass
                continue
            return "failed:network", None
        except errors.UserAlreadyParticipantError:
            return "already_member", None
        except (errors.AuthKeyUnregisteredError, errors.SessionRevokedError) as e:
            try:
                acc.dead = True
                acc.dead_reason = type(e).__name__
                await safe_disconnect(acc.client)
            except Exception:
                pass
            return f"failed:{type(e).__name__}", None
        except FrozenMethodInvalidError as e:
            try:
                acc.frozen = True
                acc.frozen_reason = str(e)
                acc.dead = True
                acc.dead_reason = "FrozenMethodInvalidError"
                logger.warning("[join:%s] Аккаунт заморожен Telegram: %s", acc.phone, e)
            except Exception:
                pass
            return "failed:FrozenMethodInvalidError", None
        except (FrozenParticipantMissingError, _FrozenParticipantMissingError) as e:
            # Новая ошибка Telethon 1.41+ — аккаунт заморожен
            try:
                acc.frozen = True
                acc.frozen_reason = str(e)
                acc.dead = True
                acc.dead_reason = "FrozenParticipantMissingError"
                logger.warning("[join:%s] ❄️ FrozenParticipantMissingError: %s", acc.phone, e)
            except Exception:
                pass
            return "failed:FrozenMethodInvalidError", None
        except errors.InviteRequestSentError:
            return "request_sent", None
        except errors.FloodWaitError as e:
            return f"floodwait:{e.seconds}", None
        except errors.UserBannedInChannelError:
            return "failed:banned_in_channel", None
        except errors.InviteHashExpiredError:
            return "failed:invite_expired", None
        except errors.ChannelPrivateError:
            return "failed:invite_private", None
        except errors.RPCError as ex:
            # Дополнительно ловим FrozenMethodInvalidError по имени (для старых
            # версий telethon) ИЛИ по тексту ошибки. Раньше Telegram возвращал
            # FROZEN_METHOD_INVALID с кодом 420, Telethon не классифицировал
            # как FrozenMethodInvalidError (status 420 — Flood family), и
            # проверка по type.__name__ не срабатывала. Клиент видел в логах
            # «RPCError 420: FROZEN_METHOD_INVALID» 35 раз подряд и акк
            # продолжал считаться живым.
            _ex_name = type(ex).__name__
            _ex_str = str(ex).upper()
            if (
                "FrozenMethod" in _ex_name or "FrozenParticipant" in _ex_name
                or "FROZEN_METHOD_INVALID" in _ex_str
                or "FROZEN_PARTICIPANT_MISSING" in _ex_str
            ):
                try:
                    acc.frozen = True
                    acc.frozen_reason = str(ex)
                    acc.dead = True
                    acc.dead_reason = "FrozenMethodInvalidError"
                    logger.warning("[join:%s] Аккаунт заморожен Telegram (RPCError): %s", acc.phone, ex)
                except Exception:
                    pass
                return "failed:FrozenMethodInvalidError", None
            if getattr(ex, 'code', None) in (500, -500) or 'No workers running' in str(ex):
                logger.warning("Telegram internal error: %s", ex)
                return "other:server_internal", None
            logger.warning("Join failed: %s", ex)
            return f"failed:{type(ex).__name__}", None
        except Exception as ex:
            logger.warning("Join failed: %s", ex)
            return f"failed:{type(ex).__name__}", None

