"""Персональные юзербот-сессии: /к с ЛК владельца и мониторинг как у catcher."""

from __future__ import annotations

import asyncio
import re
import sqlite3
from pathlib import Path
from typing import Optional

from telethon import TelegramClient
from telethon.errors import SessionPasswordNeededError

from api.db_api import (
    delete_userbot_session_meta,
    find_owner_by_userbot_id,
    list_userbot_session_owners,
    save_userbot_session_meta,
    userbot_session_path,
)
from server.userbot import API_HASH, API_ID, register_owner_handlers
from server.userbot_media import collect_dialogs, fetch_dialog_photo
from server.userbot_service import _friendly_error, _me_payload, normalize_tg_phone


class OwnerRuntime:
    def __init__(self, owner_tg_id: int, api_id: int, api_hash: str) -> None:
        self.owner_tg_id = int(owner_tg_id)
        self.api_id = api_id
        self.api_hash = api_hash
        self.client: Optional[TelegramClient] = None
        self.status = "offline"
        self.phone: Optional[str] = None
        self.phone_code_hash: Optional[str] = None
        self.me: Optional[dict] = None
        self.error: Optional[str] = None
        self._lock = asyncio.Lock()
        self._watch_seen: set[tuple[int, int]] = set()
        self._run_task: Optional[asyncio.Task] = None

    def snapshot(self) -> dict:
        return {
            "status": self.status,
            "phone": self.phone,
            "error": self.error,
            "me": self.me,
        }

    def _persist(self) -> None:
        save_userbot_session_meta(
            self.owner_tg_id,
            phone=self.phone or (self.me or {}).get("phone"),
            me=self.me,
            status=self.status,
        )

    async def _new_client(self) -> TelegramClient:
        if not self.api_id or not self.api_hash:
            raise RuntimeError("Не заданы TG_API_ID / TG_API_HASH")
        client = TelegramClient(userbot_session_path(self.owner_tg_id), self.api_id, self.api_hash)
        register_owner_handlers(client, self.owner_tg_id, self._watch_seen)
        return client

    async def ensure_client(self) -> TelegramClient:
        if self.client is None:
            self.client = await self._new_client()
        if not self.client.is_connected():
            await self.client.connect()
        register_owner_handlers(self.client, self.owner_tg_id, self._watch_seen)
        return self.client

    async def _drop_client(self) -> None:
        task = self._run_task
        self._run_task = None
        if self.client is not None:
            try:
                if self.client.is_connected() and await self.client.is_user_authorized():
                    await self.client.log_out()
                elif self.client.is_connected():
                    await self.client.disconnect()
            except Exception:
                try:
                    await self.client.disconnect()
                except Exception:
                    pass
        self.client = None
        if task:
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                pass

    async def _ensure_unique_account(self) -> None:
        if not self.me or not self.me.get("id"):
            return
        other = find_owner_by_userbot_id(int(self.me["id"]))
        if other is not None and other != self.owner_tg_id:
            await self._drop_client()
            self.me = None
            self.phone = None
            self.phone_code_hash = None
            self.status = "logged_out"
            raise RuntimeError("Этот Telegram-аккаунт уже подключён у другого пользователя")

    async def _keep_alive(self) -> None:
        client = self.client
        if client is None:
            return
        try:
            await client.run_until_disconnected()
        except Exception as exc:
            print(f"userbot [{self.owner_tg_id}] disconnected: {exc}")
        if self.status == "online":
            self.status = "offline"
            self._persist()

    def _ensure_run_task(self) -> None:
        if self._run_task is None or self._run_task.done():
            self._run_task = asyncio.create_task(self._keep_alive())

    async def _go_online(self, client: TelegramClient) -> None:
        register_owner_handlers(client, self.owner_tg_id, self._watch_seen)
        self.me = _me_payload(await client.get_me())
        await self._ensure_unique_account()
        self.status = "online"
        self.error = None
        self.phone = None
        self.phone_code_hash = None
        try:
            await client.catch_up()
        except Exception:
            pass
        self._ensure_run_task()

    async def boot(self) -> dict:
        async with self._lock:
            try:
                client = await self.ensure_client()
                if await client.is_user_authorized():
                    await self._go_online(client)
                else:
                    self.status = "logged_out"
                    self.me = None
                    self.error = None
            except Exception as exc:
                self.status = "error"
                self.error = _friendly_error(exc)
                self.me = None
            self._persist()
            return self.snapshot()

    async def shutdown(self) -> None:
        async with self._lock:
            task = self._run_task
            self._run_task = None
            if self.client is not None:
                try:
                    await self.client.disconnect()
                except Exception:
                    pass
            self.client = None
            if task:
                task.cancel()
                try:
                    await task
                except asyncio.CancelledError:
                    pass
            if self.status == "online":
                self.status = "offline"
                self._persist()

    async def request_code(self, phone_raw: str) -> dict:
        phone = normalize_tg_phone(phone_raw)
        async with self._lock:
            try:
                client = await self.ensure_client()
                if await client.is_user_authorized():
                    await self._go_online(client)
                    self._persist()
                    return self.snapshot()
                sent = await client.send_code_request(phone)
                self.phone = phone
                self.phone_code_hash = sent.phone_code_hash
                self.status = "awaiting_code"
                self.error = None
                self.me = None
            except Exception as exc:
                self.status = "logged_out"
                self.error = _friendly_error(exc)
                self._persist()
                raise RuntimeError(self.error) from exc
            self._persist()
            return self.snapshot()

    async def submit_code(self, code_raw: str) -> dict:
        code = re.sub(r"\D", "", str(code_raw or ""))
        if len(code) < 5:
            raise ValueError("Введите код из Telegram")
        async with self._lock:
            if self.status not in ("awaiting_code", "awaiting_password"):
                raise RuntimeError("Сначала запросите код")
            if not self.phone:
                raise RuntimeError("Номер не задан")
            try:
                client = await self.ensure_client()
                await client.sign_in(
                    phone=self.phone,
                    code=code,
                    phone_code_hash=self.phone_code_hash,
                )
                await self._go_online(client)
            except SessionPasswordNeededError:
                self.status = "awaiting_password"
                self.error = None
            except Exception as exc:
                self.error = _friendly_error(exc)
                self._persist()
                raise RuntimeError(self.error) from exc
            self._persist()
            return self.snapshot()

    async def submit_password(self, password: str) -> dict:
        password = str(password or "")
        if not password:
            raise ValueError("Введите пароль 2FA")
        async with self._lock:
            if self.status != "awaiting_password":
                raise RuntimeError("Сейчас пароль не нужен")
            try:
                client = await self.ensure_client()
                await client.sign_in(password=password)
                await self._go_online(client)
            except Exception as exc:
                self.error = _friendly_error(exc)
                self._persist()
                raise RuntimeError(self.error) from exc
            self._persist()
            return self.snapshot()

    async def cancel_login(self) -> dict:
        async with self._lock:
            self.phone = None
            self.phone_code_hash = None
            self.error = None
            try:
                client = await self.ensure_client()
                if await client.is_user_authorized():
                    await self._go_online(client)
                else:
                    self.me = None
                    self.status = "logged_out"
            except Exception:
                self.me = None
                self.status = "logged_out"
            self._persist()
            return self.snapshot()

    async def logout(self) -> dict:
        async with self._lock:
            if self.client is not None:
                try:
                    if self.client.is_connected() and await self.client.is_user_authorized():
                        await self.client.log_out()
                    elif self.client.is_connected():
                        await self.client.disconnect()
                except sqlite3.OperationalError as exc:
                    self.error = _friendly_error(exc)
                    raise RuntimeError(self.error) from exc
                except Exception:
                    try:
                        await self.client.disconnect()
                    except Exception:
                        pass
            task = self._run_task
            self._run_task = None
            self.client = None
            self.me = None
            self.phone = None
            self.phone_code_hash = None
            self.error = None
            self.status = "logged_out"
            self._watch_seen.clear()
            delete_userbot_session_meta(self.owner_tg_id)
            path = Path(userbot_session_path(self.owner_tg_id) + ".session")
            journal = Path(userbot_session_path(self.owner_tg_id) + ".session-journal")
            for item in (path, journal):
                try:
                    item.unlink(missing_ok=True)
                except TypeError:
                    if item.exists():
                        item.unlink()
                except Exception:
                    pass
            if task:
                task.cancel()
                try:
                    await task
                except asyncio.CancelledError:
                    pass
            try:
                await self.ensure_client()
            except Exception as exc:
                self.status = "error"
                self.error = _friendly_error(exc)
            self._persist()
            return self.snapshot()

    async def list_dialogs(self, limit: int = 200) -> list[dict]:
        client = self.client
        if client is None or not client.is_connected() or self.status != "online":
            raise RuntimeError("Юзербот не в сети")
        return await collect_dialogs(client, limit)

    async def dialog_photo(self, chat_id: int) -> Optional[bytes]:
        client = self.client
        if client is None or not client.is_connected() or self.status != "online":
            raise RuntimeError("Юзербот не в сети")
        return await fetch_dialog_photo(client, str(self.owner_tg_id), int(chat_id))

    async def self_photo(self) -> Optional[bytes]:
        if not self.me or not self.me.get("id"):
            raise RuntimeError("Юзербот не в сети")
        return await self.dialog_photo(int(self.me["id"]))


class UserbotPool:
    def __init__(self, api_id: int, api_hash: str) -> None:
        self.api_id = api_id
        self.api_hash = api_hash
        self._sessions: dict[int, OwnerRuntime] = {}
        self._lock = asyncio.Lock()

    async def get(self, owner_tg_id: int) -> OwnerRuntime:
        owner_tg_id = int(owner_tg_id)
        async with self._lock:
            runtime = self._sessions.get(owner_tg_id)
            if runtime is None:
                runtime = OwnerRuntime(owner_tg_id, self.api_id, self.api_hash)
                self._sessions[owner_tg_id] = runtime
            return runtime

    async def boot_all(self) -> None:
        owners = list_userbot_session_owners()
        print(f"userbot pool: поднимаю {len(owners)} сессий")
        for owner_id in owners:
            runtime = await self.get(owner_id)
            try:
                await runtime.boot()
                print(f"userbot pool: owner {owner_id} -> {runtime.status}")
            except Exception as exc:
                print(f"userbot pool: owner {owner_id} boot failed: {exc}")
            await asyncio.sleep(0.15)

    async def shutdown_all(self) -> None:
        async with self._lock:
            items = list(self._sessions.values())
        await asyncio.gather(*[item.shutdown() for item in items], return_exceptions=True)


pool = UserbotPool(int(API_ID or 0), API_HASH or "")
