"""
core/api_pool.py — пул API_ID/HASH пар для разнообразия TLS-fingerprint.

Раньше ВСЕ аккаунты в TelePush использовали один OFFICIAL_TDESKTOP API_ID/HASH
из config.py. Telegram при handshake видел одинаковый платформенный fingerprint
на всех 15+ акках одного клиента — это паттерн бот-сети.

Теперь:
- При ПЕРВОМ подключении нового аккаунта (если его телефон ещё не в маппинге)
  выбираем стабильную пару из пула по hash(phone). Запоминаем выбор в
  data/api_pool_map.json.
- ВАЖНО: для уже существующих сессий (acc.session_file есть на диске) мы НЕ
  меняем api_id — иначе Telegram revoke'нет сессию. Telethon читает api_id
  из самой сессии при подключении, и при mismatch начинает новый login flow.
- Юзер может расширить пул собственными парами через UI («Настройки → API пул»).

Безопасность пар: все пары в DEFAULT_POOL — публично известные из открытых
источников (OpenTele, MTProto клиенты, leaked tdesktop credentials). Они
«spent» в том смысле что Telegram про них знает, но не блокирует — это
норма для tdata-софта на рынке.
"""
from __future__ import annotations

import hashlib
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Tuple

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


# ── Дефолтный пул публично известных пар ────────────────────────────────
# Формат: (api_id, api_hash, platform_label). platform_label — для UI,
# логически — какую платформу мы имитируем при handshake.
DEFAULT_POOL: List[Tuple[int, str, str]] = [
    # tdesktop — классика, используется в большинстве tdata-tools
    (17349, "344583e45741c457fe1862106095a5eb", "tdesktop"),
    # tdesktop — альтернатива из открытых клиентов
    (2040, "b18441a1ff607e10a989891a5462e627", "tdesktop"),
    # Android — utility-клиенты типа OpenTele
    (21724, "3e0cb5efcd52300aec5994fdfc5bdc16", "android"),
    # Android — старая официальная (legacy, известно публично)
    (6, "eb06d4abfb49dc3eeb1aeb98ae0f581e", "android"),
    # iOS — старая официальная
    (8, "7245de8e747a0d6fbe11f7cc14fcc0bb", "ios"),
    # Telegram macOS
    (2834, "68875f756c9b437a8b916ca3de215815", "macos"),
]


# ── Файлы хранения ──────────────────────────────────────────────────────

def _pool_map_path() -> Path:
    """Маппинг phone → api пара. Однажды выбрано — больше не меняется."""
    from config import DATA_DIR
    return DATA_DIR / "api_pool_map.json"


def _custom_pool_path() -> Path:
    """Дополнительный пул юзерских пар (если юзер добавил свои через UI)."""
    from config import DATA_DIR
    return DATA_DIR / "api_pool_custom.json"


def _load_map() -> Dict[str, Dict]:
    try:
        p = _pool_map_path()
        if not p.exists():
            return {}
        return json.loads(p.read_text(encoding="utf-8")) or {}
    except Exception as e:
        logger.debug("[api_pool] load map: %s", e)
        return {}


def _save_map(m: Dict[str, Dict]) -> None:
    try:
        p = _pool_map_path()
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(json.dumps(m, ensure_ascii=False, indent=2), encoding="utf-8")
    except Exception as e:
        logger.warning("[api_pool] save map: %s", e)


def _load_custom_pool() -> List[Tuple[int, str, str]]:
    try:
        p = _custom_pool_path()
        if not p.exists():
            return []
        raw = json.loads(p.read_text(encoding="utf-8")) or []
        out = []
        for it in raw:
            try:
                out.append((int(it["id"]), str(it["hash"]), str(it.get("platform", "custom"))))
            except Exception:
                continue
        return out
    except Exception as e:
        logger.debug("[api_pool] load custom: %s", e)
        return []


def _save_custom_pool(pool: List[Tuple[int, str, str]]) -> None:
    try:
        p = _custom_pool_path()
        p.parent.mkdir(parents=True, exist_ok=True)
        items = [{"id": aid, "hash": ahash, "platform": plat} for aid, ahash, plat in pool]
        p.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
    except Exception as e:
        logger.warning("[api_pool] save custom: %s", e)


def _effective_pool() -> List[Tuple[int, str, str]]:
    """Дефолтный пул + пользовательские пары."""
    return DEFAULT_POOL + _load_custom_pool()


# ── Публичный API ──────────────────────────────────────────────────────

def get_api_for_phone(phone: str) -> Optional[Tuple[int, str, str]]:
    """Возвращает (api_id, api_hash, platform) для аккаунта.

    Логика:
    1. Если phone уже в маппинге → возвращаем сохранённую пару (НЕ меняем!).
    2. Если phone нового аккаунта — выбираем стабильную пару из effective_pool
       по sha256(phone) % len(pool), запоминаем и возвращаем.
    3. Если пул пустой по какой-то причине — None (вызывающий код использует
       OFFICIAL_TDESKTOP fallback).

    КРИТИЧНО: для существующего session-файла НЕЛЬЗЯ менять api_id между
    запусками — Telegram может revoke сессию. Поэтому маппинг персистится.
    """
    if not phone:
        return None
    phone = str(phone).strip().lstrip("+")
    if not phone:
        return None

    m = _load_map()
    if phone in m:
        entry = m[phone]
        try:
            return (int(entry["id"]), str(entry["hash"]), str(entry.get("platform", "")))
        except Exception:
            pass  # fallthrough to re-pick

    pool = _effective_pool()
    if not pool:
        return None

    # Стабильный выбор: sha256 первые 4 байта как unsigned int
    h = hashlib.sha256(phone.encode("utf-8")).digest()
    idx = int.from_bytes(h[:4], "big") % len(pool)
    aid, ahash, platform = pool[idx]
    m[phone] = {"id": aid, "hash": ahash, "platform": platform}
    _save_map(m)
    return (aid, ahash, platform)


def get_mapping_for_existing(phone: str) -> Optional[Tuple[int, str, str]]:
    """Возвращает уже сохранённый маппинг для phone (если был). НЕ создаёт новый.
    Используется для существующих session-файлов: если маппинг есть — применяем,
    если нет — НЕ трогаем (старая сессия с OFFICIAL_TDESKTOP)."""
    if not phone:
        return None
    phone = str(phone).strip().lstrip("+")
    m = _load_map()
    entry = m.get(phone)
    if not entry:
        return None
    try:
        return (int(entry["id"]), str(entry["hash"]), str(entry.get("platform", "")))
    except Exception:
        return None


def list_mappings() -> Dict[str, Dict]:
    """Все сохранённые phone → пара (для UI)."""
    return _load_map()


def add_custom_pair(api_id: int, api_hash: str, platform: str = "custom") -> bool:
    """Добавляет пользовательскую пару. Возвращает True если добавлено."""
    try:
        api_id = int(api_id)
        api_hash = str(api_hash).strip()
        if not api_hash or len(api_hash) < 16:
            return False
        custom = _load_custom_pool()
        # Дедуп
        for aid, ahash, _ in custom:
            if aid == api_id and ahash == api_hash:
                return False
        custom.append((api_id, api_hash, platform or "custom"))
        _save_custom_pool(custom)
        return True
    except Exception as e:
        logger.warning("[api_pool] add_custom: %s", e)
        return False


def list_custom_pairs() -> List[Dict]:
    """Для UI: список юзерских пар."""
    return [{"id": aid, "hash": ahash, "platform": plat}
            for aid, ahash, plat in _load_custom_pool()]


def remove_custom_pair(api_id: int) -> bool:
    custom = _load_custom_pool()
    new = [p for p in custom if p[0] != api_id]
    if len(new) == len(custom):
        return False
    _save_custom_pool(new)
    return True


def pool_stats() -> Dict:
    """Сводка для UI."""
    default = len(DEFAULT_POOL)
    custom = len(_load_custom_pool())
    mappings = _load_map()
    platforms: Dict[str, int] = {}
    for entry in mappings.values():
        platforms[entry.get("platform", "unknown")] = platforms.get(entry.get("platform", "unknown"), 0) + 1
    return {
        "default_pool_size": default,
        "custom_pool_size": custom,
        "total_pool": default + custom,
        "mapped_phones": len(mappings),
        "platforms_distribution": platforms,
    }
