import asyncio
import json
import random
import re
import time
from pathlib import Path
from typing import List, Dict
from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError

import aiosocks
from opentele.api import UseCurrentSession, API
from opentele.td import TDesktop
from opentele.tl import TelegramClient as OTClient
from telethon.errors import SessionPasswordNeededError, UserAlreadyParticipantError
from telethon.network.connection.tcpabridged import ConnectionTcpAbridged
from telethon.tl.functions.channels import JoinChannelRequest
from telethon.tl.functions.messages import ImportChatInviteRequest
from telethon.tl.types import InputChannel
from aiohttp_socks import ProxyType
import logging
from logging.handlers import RotatingFileHandler
import sys

NEW_SESSION_DIR = Path("new_proxy_converted_sessions")

MILLION_CHANNEL = "https://t.me/+8AqnUALHTpZkNmJi"


# Logger setup with UTF-8 console
def setup_logger():
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)

    file_handler = RotatingFileHandler(
        'telegram_session.log',
        maxBytes=10 * 1024 * 1024,
        backupCount=3,
        encoding='utf-8'
    )
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s - %(levelname)s - %(message)s'
    ))

    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setFormatter(logging.Formatter(
        '%(levelname)s - %(message)s'
    ))

    if not logger.hasHandlers():
        logger.addHandler(file_handler)
        logger.addHandler(console_handler)

    return logger

logger = setup_logger()

DELAYS = {
    'short': (0.5, 1.5),
    'medium': (1.5, 3),
    'long': (3, 6)
}


def proxy_to_str(proxy: Dict) -> str:
    return f"{proxy['host']}:{proxy['port']}:{proxy['username']}:{proxy['password']}"


def load_used_proxies(json_path: Path) -> set:
    if not json_path.exists():
        return set()
    try:
        with open(json_path, "r", encoding="utf-8") as f:
            data = json.load(f)
    except Exception:
        return set()
    used_proxies = set()
    for account_data in data.values():
        proxy = account_data.get("proxy")
        if proxy:
            key = proxy_to_str(proxy)
            used_proxies.add(key)
    return used_proxies


def human_delay(delay_type):
    min_d, max_d = DELAYS[delay_type]
    delay = random.uniform(min_d, max_d)
    time.sleep(delay)
    return delay

async def get_phone_from_session(client):
    me = await client.get_me()
    phone = me.phone
    await client.get_dialogs(limit=3)
    await asyncio.sleep(random.uniform(0.5, 1.5))
    return phone

def extract_code_from_text(text, code_type='web'):
    if not text:
        return None

    if code_type == 'web':
        patterns = [
            r'`([A-Za-z0-9_\-]{8,12})`',
            r'(?:Вот он:|код:|code:)\s*`?([A-Za-z0-9_\-]{8,12})`?',
            r'\b([A-Za-z0-9_\-]{8,12})\b(?=.*my\.telegram\.org)'
        ]
    else:
        patterns = [
            r'\b(\d{5})\b',
            r'\b(\d{6})\b',
            r'код:\s*(\d{5,6})',
            r'code:\s*(\d{5,6})'
        ]

    for pattern in patterns:
        match = re.search(pattern, text)
        if match:
            return match.group(1)
    return None

async def get_code_from_telegram(client, phone_number, code_type='web', timeout=60):
    """
    Получает код подтверждения из Telegram через авторизованный клиент.
    """
    start_time = time.time()
    last_message_id = 0

    while time.time() - start_time < timeout:
        messages = []
        async for message in client.iter_messages(777000, limit=5):
            if message.id > last_message_id:
                messages.append(message)
                last_message_id = message.id

        for message in reversed(messages):
            if message.text:
                logger.info(f"Анализируем сообщение:\n{message.text}")
                code = extract_code_from_text(message.text, code_type)
                if code:
                    logger.info(f"Найден {code_type} код: {code}")
                    return code

        await asyncio.sleep(random.uniform(0.5, 1.5))

    logger.error("Не удалось найти код в сообщениях.")
    return None


async def create_telegram_app(client, phone_number):
    """
    Создаёт приложение на my.telegram.org с использованием Playwright (async).
    Получение кода происходит через переданный авторизованный client.
    """
    try:
        async with async_playwright() as pw:
            browser = await pw.chromium.launch(headless=False)
            context = await browser.new_context()
            page = await context.new_page()

            await page.goto("https://my.telegram.org/apps")

            # Вводим номер телефона
            await page.fill("#my_login_phone", phone_number)
            await page.click("button.btn-primary")

            await asyncio.sleep(random.uniform(3,5))  # Ждем, пока Telegram обработает запрос

            # Проверка ошибки "слишком много попыток"
            try:
                alert = await page.wait_for_selector("#my_login_alert div.alert-danger", timeout=5000)
                alert_text = await alert.text_content()
                if "too many tries" in alert_text.lower():
                    logger.error("❌ Ошибка: Слишком много попыток.")
                    await browser.close()
                    return None, None
            except PlaywrightTimeoutError:
                pass  # ошибки нет

            # Ждём поле ввода кода
            await page.wait_for_selector("#my_password", timeout=60000)

            # Получаем код через существующий авторизованный клиент
            code = await get_code_from_telegram(client, phone_number, code_type='web')
            if not code:
                raise Exception("Не удалось получить код подтверждения")

            # Вводим код и кликаем Sign In
            await page.fill("#my_password", code)
            await page.click("button:has-text('Sign In')")

            # Ждём api_id/api_hash
            try:
                api_id_el = await page.wait_for_selector("label:has-text('App api_id:') + div span", timeout=30000)
                api_hash_el = await page.wait_for_selector("label:has-text('App api_hash:') + div span", timeout=30000)

                api_id = int((await api_id_el.text_content()).strip())
                api_hash = (await api_hash_el.text_content()).strip()

                logger.info(f"Успешно получены API данные: api_id={api_id}, api_hash={api_hash}")

                await browser.close()
                return api_id, api_hash

            except PlaywrightTimeoutError:
                logger.info("Приложение ещё не создано, начинаем создание...")

            # Заполняем форму создания приложения
            await page.wait_for_selector("input[name=app_title]", timeout=30000)
            await page.fill("input[name=app_title]", "My App")
            await page.fill("input[name=app_shortname]", "myapp")
            await page.check("input[name=app_platform][value=desktop]")
            await page.fill("textarea[name=app_desc]", "My Telegram Application")

            max_attempts = 66
            for attempt in range(max_attempts):
                try:
                    # Проверяем, может api_id/api_hash уже есть и можно не нажимать кнопку
                    try:
                        api_id_el = await page.wait_for_selector("label:has-text('App api_id:') + div span",
                                                                 timeout=3000)
                        api_hash_el = await page.wait_for_selector("label:has-text('App api_hash:') + div span",
                                                                   timeout=3000)
                        api_id = int((await api_id_el.text_content()).strip())
                        api_hash = (await api_hash_el.text_content()).strip()
                        logger.info(f"API данные уже появились перед попыткой {attempt + 1}, завершаем цикл.")
                        break
                    except PlaywrightTimeoutError:
                        # Элементы не появились — идём к нажатию кнопки
                        pass

                    # Жмём кнопку сохранить/создать
                    await page.click("#app_save_btn")
                    await asyncio.sleep(random.uniform(3, 6))

                    # Проверяем, есть ли dialog с ошибкой
                    try:
                        dialog = await page.wait_for_event("dialog", timeout=3000)
                        if "error" in dialog.message.lower():
                            logger.warning(f"Ошибка при попытке {attempt + 1}/{max_attempts}: {dialog.message}")
                            await asyncio.sleep(random.uniform(1, 2))
                            await dialog.dismiss()
                            await asyncio.sleep(random.uniform(1, 2))
                            continue
                        else:
                            logger.info(f"Диалог без ошибки: {dialog.message}")
                            await dialog.dismiss()
                    except asyncio.TimeoutError:
                        pass  # Диалог не появился

                    # Теперь ещё раз проверяем, появился ли api_id/api_hash после клика
                    try:
                        api_id_el = await page.wait_for_selector("label:has-text('App api_id:') + div span",
                                                                 timeout=3000)
                        api_hash_el = await page.wait_for_selector("label:has-text('App api_hash:') + div span",
                                                                   timeout=3000)
                        api_id = int((await api_id_el.text_content()).strip())
                        api_hash = (await api_hash_el.text_content()).strip()
                        logger.info(f"API данные появились после попытки {attempt + 1}, завершаем цикл.")
                        break
                    except PlaywrightTimeoutError:
                        logger.warning(f"API данные не появились после попытки {attempt + 1}, пробуем снова.")
                        await asyncio.sleep(random.uniform(1, 2))

                except Exception as e:
                    logger.warning(f"Ошибка при создании приложения (попытка {attempt + 1}): {e}")
                    await asyncio.sleep(random.uniform(0.5, 1.5))

            # После выхода из цикла забираете api_id/api_hash (повторно, на всякий случай)
            api_id_el = await page.wait_for_selector("label:has-text('App api_id:') + div span", timeout=30000)
            api_hash_el = await page.wait_for_selector("label:has-text('App api_hash:') + div span", timeout=30000)
            api_id = int((await api_id_el.text_content()).strip())
            api_hash = (await api_hash_el.text_content()).strip()

            logger.info(f"Успешно получены API данные: api_id={api_id}, api_hash={api_hash}")

            await browser.close()
            return api_id, api_hash

    except Exception as e:
        logger.error(f"Критическая ошибка create_telegram_app: {e}", exc_info=True)
        raise


async def get_channel_entity(client, invite_link):
    try:
        if any(invite_link.startswith(prefix) for prefix in ["https://t.me/+", "http://t.me/+", "t.me/+"]):
            invite_hash = invite_link.split("+")[-1]
            try:
                result = await client(ImportChatInviteRequest(invite_hash))
                return InputChannel(result.chats[0].id, result.chats[0].access_hash)
            except UserAlreadyParticipantError:
                entity = await client.get_entity(invite_link)
                return entity
        else:
            entity = await client.get_entity(invite_link)
            return entity
    except Exception as e:
        logger.error(f"Ошибка получения entity: {e}")
        raise


async def subscribe_to_channel(client, channel_link):
    try:
        entity = await get_channel_entity(client, channel_link)
        try:
            await client(JoinChannelRequest(channel=entity))
            logger.info(f"Успешно подписались на канал: {channel_link}")
        except UserAlreadyParticipantError:
            logger.info(f"Уже подписан на канал: {channel_link}")
    except Exception as e:
        logger.error(f"Ошибка подписки: {e}")


async def create_new_session(phone_number: str, api_id: int, api_hash: str,
                             proxy_config: Dict, session_number: int, code_client: OTClient):
    global successful_sessions, failed_sessions

    max_attempts = 3
    last_error = None
    session_name = NEW_SESSION_DIR / f"new_session_{phone_number}"

    if not NEW_SESSION_DIR.exists():
        NEW_SESSION_DIR.mkdir(parents=True, exist_ok=True)

    session_file = Path(f"{session_name}.session")
    if session_file.exists():
        try:
            session_file.unlink()
        except PermissionError:
            logger.warning(f"Не удалось удалить файл сессии {session_name}.session, возможно он занят другим процессом")
            return None

    for attempt in range(max_attempts):
        client = None
        try:
            logger.info(f"[{session_number}] Попытка {attempt + 1}/{max_attempts} создания сессии для {phone_number}")

            client = OTClient(
                session=str(session_name),
                api_id=api_id,
                api_hash=api_hash,
                api=API.TelegramDesktop.Generate(),
                connection=ConnectionTcpAbridged,
                proxy=(
                    ProxyType.SOCKS5,
                    proxy_config["host"],
                    proxy_config["port"],
                    True,  # rdns
                    proxy_config["username"],
                    proxy_config["password"]
                ),
                use_ipv6=True
            )

            logger.debug(f"[{session_number}] Инициализация клиента завершена, подключаемся...")
            await client.connect()
            logger.info(f"[{session_number}] Успешное подключение к Telegram")

            logger.debug(f"[{session_number}] Отправка запроса кода...")
            sent_code = await client.send_code_request(phone_number)
            logger.info(f"[{session_number}] Код подтверждения отправлен")

            code = await get_code_from_telegram(code_client, phone_number, code_type='login')
            if not code:
                raise Exception("Не удалось получить код подтверждения")
            logger.debug(f"[{session_number}] Получен код: {code}")

            try:
                await client.sign_in(
                    phone=phone_number,
                    code=code,
                    phone_code_hash=sent_code.phone_code_hash
                )
                logger.info(f"[{session_number}] Успешная авторизация по коду")
            except SessionPasswordNeededError:
                logger.warning(f"[{session_number}] Требуется пароль двухфакторной аутентификации")
                password = input(f"Введите пароль 2FA для {phone_number}: ")
                await client.sign_in(password=password)
                logger.info(f"[{session_number}] Успешная авторизация с паролем 2FA")

            await subscribe_to_channel(client, MILLION_CHANNEL)
            logger.info(f"[{session_number}] Сессия {phone_number} успешно создана!")

            # Сохраняем данные сессии в JSON
            try:
                me = await client.get_me()
                session_data = {
                    "phone_number": phone_number,
                    "telegram_id": me.id,
                    "api_id": api_id,
                    "api_hash": api_hash,
                    "proxy": {
                        "host": proxy_config["host"],
                        "port": proxy_config["port"],
                        "username": proxy_config["username"],
                        "password": proxy_config["password"]
                    }
                }
                json_path = NEW_SESSION_DIR / "all_api_id_phones.json"

                if json_path.exists():
                    with open(json_path, "r", encoding="utf-8") as f:
                        try:
                            all_data = json.load(f)
                        except json.JSONDecodeError:
                            all_data = {}
                else:
                    all_data = {}

                composite_key = f"{phone_number}|{me.id}"
                all_data[composite_key] = session_data

                with open(json_path, "w", encoding="utf-8") as f:
                    json.dump(all_data, f, ensure_ascii=False, indent=4)

                logger.info(f"[{session_number}] Информация о сессии обновлена в {json_path}")
            except Exception as e:
                logger.warning(f"[{session_number}] Не удалось сохранить JSON сессии: {e}")

            successful_sessions += 1
            return client

        except Exception as e:
            last_error = e
            logger.error(f"[{session_number}] Ошибка при попытке {attempt + 1}: {str(e)}", exc_info=True)
            if client and client.is_connected():
                await client.disconnect()
            human_delay('long')

    logger.critical(f"[{session_number}] Не удалось создать сессию после {max_attempts} попыток")
    failed_sessions += 1
    raise Exception(f"[{session_number}] Последняя ошибка: {last_error}")


def find_all_tdata_paths(base_path="all_tdatas") -> List[str]:
    tdata_paths = []
    base_path = Path(base_path)

    if not base_path.exists():
        raise FileNotFoundError(f"Директория {base_path} не найдена")

    for entry in base_path.iterdir():
        if entry.is_dir():
            tdata_path = entry / "tdata"
            if tdata_path.exists():
                tdata_paths.append(str(tdata_path))

    if not tdata_paths:
        raise FileNotFoundError(f"Не найдено ни одной папки tdata в {base_path}")

    return tdata_paths


def load_proxies(filename="proxies.txt") -> List[Dict]:
    proxies = []
    try:
        with open(filename, 'r') as f:
            for line in f:
                line = line.strip()
                if line:
                    parts = line.split(':')
                    if len(parts) == 4:
                        proxy = {
                            "host": parts[0],
                            "port": int(parts[1]),
                            "username": parts[2],
                            "password": parts[3],
                            "check_timeout": 3,
                            "rdns": True
                        }
                        proxies.append(proxy)

        if not proxies:
            raise ValueError("Файл проксей пуст или имеет неверный формат")

        logger.info(f"Загружено {len(proxies)} проксей")
        return proxies
    except Exception as e:
        logger.error(f"Ошибка загрузки проксей: {e}")
        raise


async def limited_process_session(semaphore, tdata_path, proxy_config, session_number):
    async with semaphore:
        await process_session(tdata_path, proxy_config, session_number)


async def process_session(tdata_path: str, proxy_config: Dict, session_number: int):
    global total_sessions, failed_sessions
    total_sessions += 1

    logger.info(f"\n{'=' * 50}")
    logger.info(f"Обработка сессии #{session_number} ({tdata_path}) - старт")
    logger.info(f"{'=' * 50}\n")

    try:
        logger.info(f"[{session_number}] 1. Загружаем tdata...")
        tdata = TDesktop(tdata_path)
        if not tdata.isLoaded():
            logger.error(f"[{session_number}] ⚠️ tdata не загружено или повреждено")
            return

        session_name = f"session_{session_number}"

        converter_client = await tdata.ToTelethon(
            session=session_name,
            flag=UseCurrentSession,
            api=API.TelegramDesktop.Generate()
        )
        await converter_client.disconnect()
        logger.info(f"[{session_number}] Сессия сохранена")

        existing_client = OTClient(
            session=session_name,
            api_id=2040,
            api_hash="b18441a1ff607e10a989891a5462e627",
            api=API.TelegramDesktop.Generate(),
            connection=ConnectionTcpAbridged,
        )

        await existing_client.connect()
        me = await existing_client.get_me()
        logger.info(f"[{session_number}] User info: {me}")

        if not await existing_client.is_user_authorized():
            logger.error(f"[{session_number}] ⚠️ Сессия не авторизована, пропускаем")
            failed_sessions += 1
            return

        logger.info(f"[{session_number}] 2. Получаю номер телефона...")
        phone_number = await get_phone_from_session(existing_client)
        logger.info(f"[{session_number}] Номер: {phone_number}")

        logger.info(f"[{session_number}] 3. Создаю приложение (Playwright)...")
        api_id, api_hash = await create_telegram_app(existing_client, phone_number)
        if not api_id or not api_hash:
            logger.error(f"[{session_number}] Не удалось получить API данные, пропускаем сессию")
            return

        logger.info(f"[{session_number}] 4. Создаю новую сессию...")
        await create_new_session(str(phone_number), int(api_id), str(api_hash), proxy_config, session_number, code_client=existing_client)

        logger.info(f"[{session_number}] ✅ Обработка сессии завершена - конец")

    except Exception as e:
        logger.error(f"[{session_number}] ⚠️ Критическая ошибка: {str(e)[:100]}", exc_info=True)
        failed_sessions += 1
    finally:
        if 'existing_client' in locals() and existing_client.is_connected():
            await existing_client.disconnect()
        logger.info(f"[{session_number}] Сессия отключена")


async def main():
    global successful_sessions, failed_sessions, total_sessions

    logger.info("=== Запуск программы ===")
    start_time = time.time()

    try:
        proxies = load_proxies()
        tdata_paths = find_all_tdata_paths()
        logger.info(f"Найдено {len(tdata_paths)} сессий для обработки")

        json_path = NEW_SESSION_DIR / "all_api_id_phones.json"
        used_proxies = load_used_proxies(json_path)
        logger.info(f"Найдено {len(used_proxies)} уже используемых проксей в сессиях")

        semaphore = asyncio.Semaphore(40)  # Максимум 50 браузеров одновременно

        # Фильтруем доступные прокси — убираем уже используемые
        available_proxies = [p for p in proxies if proxy_to_str(p) not in used_proxies]

        if len(available_proxies) < len(tdata_paths):
            logger.warning(f"Доступных проксей меньше сессий! Будет повторное использование проксей.")
            exit()

        tasks = []
        for i, tdata_path in enumerate(tdata_paths, 1):
            # Для корректного распределения по доступным проксям по кругу:
            proxy = available_proxies[(i - 1) % len(available_proxies)]
            task = asyncio.create_task(limited_process_session(semaphore, tdata_path, proxy, i))
            tasks.append(task)
            await asyncio.sleep(1)

        await asyncio.gather(*tasks)

        logger.info("=== Все задачи выполнены! ===")
        logger.info(f"Обработано всего сессий: {total_sessions}")
        logger.info(f"Успешно создано сессий: {successful_sessions}")
        logger.info(f"Не удалось создать сессий: {failed_sessions}")

    except Exception as e:
        logger.error(f"Ошибка в main: {e}", exc_info=True)

    finally:
        for task in asyncio.all_tasks():
            if not task.done():
                task.cancel()

        duration = time.time() - start_time
        logger.info(f"=== Завершение программы. Продолжительность: {duration:.2f} секунд ===")

if __name__ == "__main__":
    successful_sessions = 0
    failed_sessions = 0
    total_sessions = 0

    asyncio.run(main())

Когда будет
ERROR - ❌ Ошибка: Слишком много попыток.

Или любая другая ошибка, но api_id или api_hash не была получена, тогда нужно папку из all_tdatas/(рандомные символы) - перенести в папку need_to_restart_tdata.