# -*- coding: utf-8 -*-
"""
Personal Telegram bot that obtains the owner's API ID/API Hash.

No phone, login code, API hash, cookies, or 2FA material is persisted.
The conversation state is kept in RAM and expires automatically.
"""

from __future__ import annotations

import logging
import re
import threading
import time
from typing import Dict

import requests

import config
from portal import (
    AppCreateError,
    Credentials,
    LoginError,
    PortalError,
    RateLimitError,
    TelegramPortal,
    build_proxy_map,
)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
)
log = logging.getLogger("tg-api-generator")

API = f"https://api.telegram.org/bot{config.BOT_TOKEN}"

session = requests.Session()
proxy_map = build_proxy_map()
if proxy_map:
    session.proxies.update(proxy_map)

STATE_LOCK = threading.Lock()
STATE: Dict[int, dict] = {}


def _now() -> float:
    return time.time()


def cleanup_states() -> None:
    while True:
        try:
            deadline = _now() - config.STATE_TTL
            with STATE_LOCK:
                expired = [uid for uid, item in STATE.items() if item.get("updated", 0) < deadline]
                for uid in expired:
                    # Deliberately discard all transient authentication material.
                    STATE.pop(uid, None)
        except Exception:
            log.exception("State cleanup failed")
        time.sleep(60)


def is_owner(user_id: int) -> bool:
    return int(user_id) == int(config.OWNER_ID)


def api_call(method: str, data=None, timeout=None):
    timeout = timeout or max(config.REQUEST_TIMEOUT, 60)
    response = session.post(
        f"{API}/{method}",
        json=data or {},
        timeout=timeout,
    )
    try:
        payload = response.json()
    except ValueError:
        raise RuntimeError(f"Telegram Bot API returned invalid JSON (HTTP {response.status_code})")
    if not payload.get("ok"):
        raise RuntimeError(payload.get("description", "Telegram Bot API error"))
    return payload["result"]


def send(chat_id: int, text: str, reply_markup=None) -> None:
    data = {"chat_id": chat_id, "text": text}
    if reply_markup:
        data["reply_markup"] = reply_markup
    api_call("sendMessage", data)


def answer_callback(callback_id: str) -> None:
    try:
        api_call("answerCallbackQuery", {"callback_query_id": callback_id})
    except Exception:
        pass


def keyboard_start():
    return {
        "inline_keyboard": [
            [{"text": "🔑 دریافت API ID / HASH", "callback_data": "start_generate"}],
            [{"text": "❌ لغو", "callback_data": "cancel"}],
        ]
    }


def set_state(user_id: int, **values):
    values["updated"] = _now()
    with STATE_LOCK:
        STATE[user_id] = values


def update_state(user_id: int, **values):
    with STATE_LOCK:
        current = STATE.get(user_id, {})
        current.update(values)
        current["updated"] = _now()
        STATE[user_id] = current


def get_state(user_id: int):
    with STATE_LOCK:
        item = dict(STATE.get(user_id, {}))
    return item


def clear_state(user_id: int):
    with STATE_LOCK:
        STATE.pop(user_id, None)


def normalize_phone(text: str) -> str:
    value = re.sub(r"[^0-9+]", "", (text or "").strip())
    if not value:
        return ""
    if value.startswith("00"):
        value = "+" + value[2:]
    if value.startswith("0") and len(value) > 10:
        value = "+" + value[1:]
    if value.startswith("+"):
        return value
    if value.isdigit() and len(value) >= 8:
        return "+" + value
    return value


def valid_phone(text: str) -> bool:
    return bool(normalize_phone(text))


def valid_code(text: str) -> bool:
    # Telegram/my.telegram.org may use numeric or alphanumeric
    # confirmation codes (including hyphens), so do not restrict
    # the code to digits only.
    value = text.strip()
    return bool(re.fullmatch(r"[A-Za-z0-9_-]{4,64}", value))


def start_flow(chat_id: int, user_id: int):
    # Never allow anyone except owner to trigger the credential workflow.
    if not is_owner(user_id):
        send(chat_id, "⛔ این ربات فقط برای صاحب ربات فعال است.")
        return

    clear_state(user_id)
    set_state(user_id, step="phone")
    send(
        chat_id,
        "📱 شماره Telegram را با کد کشور بفرست.\n\n"
        "مثال:\n"
        "+989123456789\n\n"
        "بعد از ارسال، فقط همان لحظه برای my.telegram.org درخواست کد می‌فرستم.",
    )


def process_text(chat_id: int, user_id: int, text: str):
    if not is_owner(user_id):
        send(chat_id, "⛔ دسترسی ندارید.")
        return

    text = text.strip()
    if text.lower() in ("/cancel", "لغو", "کنسل"):
        clear_state(user_id)
        send(chat_id, "✅ عملیات لغو شد.")
        return

    state = get_state(user_id)
    step = state.get("step")

    if step == "phone":
        normalized_phone = normalize_phone(text)
        if not valid_phone(normalized_phone):
            send(chat_id, "❌ شماره نامعتبر است.\nفرمت درست: +989123456789")
            return

        send(chat_id, "⏳ در حال درخواست کد از my.telegram.org...")
        try:
            portal = TelegramPortal(proxy_map)
            random_hash = portal.send_code(normalized_phone)
        except RateLimitError as exc:
            clear_state(user_id)
            send(chat_id, f"⚠️ {exc}")
            return
        except PortalError as exc:
            clear_state(user_id)
            send(chat_id, f"❌ {exc}")
            return
        except Exception:
            log.exception("send_code failed")
            clear_state(user_id)
            send(chat_id, "❌ خطای غیرمنتظره در ارتباط با Telegram رخ داد. جزئیات حساس ثبت نشده است.")
            return

        # Random hash is needed only for the next step, and is RAM-only.
        set_state(user_id, step="code", phone=normalized_phone, random_hash=random_hash)
        send(
            chat_id,
            "✅ کد ارسال شد.\n\n"
            "🔢 کد ورود Telegram را بفرست.\n"
            "کد را فقط همینجا وارد کن؛ من آن را ذخیره نمی‌کنم.",
        )
        return

    if step == "code":
        if not valid_code(text):
            send(chat_id, "❌ فرمت کد ورود نامعتبر است. کد را دقیقاً همان‌طور که Telegram فرستاده وارد کن.")
            return

        phone = state.get("phone")
        random_hash = state.get("random_hash")
        if not phone or not random_hash:
            clear_state(user_id)
            send(chat_id, "❌ نشست منقضی شد. دوباره از ابتدا شروع کن.")
            return

        # Use exactly one portal session for login+apps, preserving its cookies.
        send(chat_id, "⏳ کد در حال بررسی است و صفحه API development tools بررسی می‌شود...")
        portal = TelegramPortal(proxy_map)
        try:
            portal.login(phone, random_hash, text)
            # Immediately discard the one-time code and hash from the bot state.
            with STATE_LOCK:
                current = STATE.get(user_id, {})
                current.pop("random_hash", None)
                current.pop("phone", None)
                current["updated"] = _now()
                STATE[user_id] = current

            existing = portal.get_credentials()
            if existing:
                clear_state(user_id)
                send(
                    chat_id,
                    "✅ Application از قبل برای این شماره وجود داشت.\n\n"
                    f"🆔 API ID:\n`{existing.api_id}`\n\n"
                    f"🔐 API HASH:\n`{existing.api_hash}`",
                    {"inline_keyboard": [[{"text": "🔄 شروع دوباره", "callback_data": "start_generate"}]]},
                )
                return

            send(chat_id, "🛠 Application وجود ندارد؛ در حال ساخت یک Application جدید...")
            creds = portal.create_application_and_get_credentials()
            clear_state(user_id)
            if creds.existed_before:
                status = "از قبل وجود داشت"
            else:
                status = "با موفقیت ساخته شد"

            send(
                chat_id,
                f"✅ Application {status}.\n\n"
                f"🆔 API ID:\n`{creds.api_id}`\n\n"
                f"🔐 API HASH:\n`{creds.api_hash}`\n\n"
                "⚠️ این دو مقدار را عمومی نکن.",
                {"inline_keyboard": [[{"text": "🔄 شروع دوباره", "callback_data": "start_generate"}]]},
            )
            return

        except RateLimitError as exc:
            clear_state(user_id)
            send(chat_id, f"⚠️ {exc}")
        except (LoginError, AppCreateError, PortalError) as exc:
            clear_state(user_id)
            send(chat_id, f"❌ {exc}")
        except Exception:
            log.exception("generation flow failed")
            clear_state(user_id)
            send(
                chat_id,
                "❌ خطای غیرمنتظره رخ داد.\n"
                "کد ورود/شماره/API Hash در لاگ ثبت نشده است.",
            )
        return

    send(
        chat_id,
        "برای شروع روی دکمه زیر بزن.",
        keyboard_start(),
    )


def process_update(update: dict):
    if "callback_query" in update:
        cb = update["callback_query"]
        answer_callback(cb["id"])
        user = cb.get("from") or {}
        chat = cb.get("message", {}).get("chat", {})
        user_id = user.get("id")
        chat_id = chat.get("id")
        data = cb.get("data", "")
        if not user_id or not chat_id:
            return
        if data == "start_generate":
            start_flow(chat_id, user_id)
        elif data == "cancel":
            clear_state(user_id)
            send(chat_id, "✅ لغو شد.")
        return

    message = update.get("message")
    if not message:
        return
    user = message.get("from") or {}
    chat = message.get("chat") or {}
    user_id = user.get("id")
    chat_id = chat.get("id")
    text = message.get("text", "")
    if not user_id or not chat_id:
        return

    if text.startswith("/start"):
        if not is_owner(user_id):
            send(chat_id, "⛔ این ربات خصوصی است.")
            return
        clear_state(user_id)
        send(
            chat_id,
            "🤖 Telegram API Generator\n\n"
            "این نسخه فقط برای صاحب ربات فعال است.\n"
            "کدها و اطلاعات ورود در فایل ذخیره نمی‌شوند.",
            keyboard_start(),
        )
        return

    process_text(chat_id, user_id, text)


def main():
    if config.BOT_TOKEN == "PUT_YOUR_BOT_TOKEN_HERE":
        raise SystemExit("BOT_TOKEN را در config.py تنظیم کن.")
    if int(config.OWNER_ID) <= 0:
        raise SystemExit("OWNER_ID را در config.py تنظیم کن.")

    # Make sure this bot is using long polling, not an old webhook.
    try:
        api_call("deleteWebhook", {"drop_pending_updates": True})
        me = api_call("getMe", {})
        log.info("Bot connected as @%s", me.get("username", "?"))
    except Exception as exc:
        raise SystemExit(f"Bot API connection failed: {exc}")

    threading.Thread(target=cleanup_states, daemon=True).start()

    offset = None
    while True:
        try:
            data = {"timeout": config.LONG_POLL_TIMEOUT, "limit": 100}
            if offset is not None:
                data["offset"] = offset
            updates = api_call("getUpdates", data, timeout=config.LONG_POLL_TIMEOUT + 15)

            for update in updates:
                offset = int(update["update_id"]) + 1
                try:
                    process_update(update)
                except Exception:
                    # One bad update must not stop the bot.
                    log.exception("Update processing failed")
        except requests.RequestException:
            log.exception("Network error in polling; retrying")
            time.sleep(3)
        except Exception:
            log.exception("Polling failed; retrying")
            time.sleep(5)


if __name__ == "__main__":
    main()
