# -*- coding: utf-8 -*-
"""
my.telegram.org client.

Fixes applied:
- Fixed form body payload for app creation (valid app_url, valid shortname length/format).
- Better parsing of existing API ID / Hash and error messages.
- Updated User-Agent and headers.
"""

from __future__ import annotations

import random
import re
import string
import time
from dataclasses import dataclass
from typing import Dict, Optional, Tuple

import requests
from bs4 import BeautifulSoup

import config


class PortalError(Exception):
    """Expected, user-facing error from my.telegram.org workflow."""


class RateLimitError(PortalError):
    pass


class LoginError(PortalError):
    pass


class AppCreateError(PortalError):
    pass


@dataclass
class Credentials:
    api_id: str
    api_hash: str
    existed_before: bool


class TelegramPortal:
    BASE = "https://my.telegram.org"
    AUTH_PAGE = BASE + "/auth?to=apps"
    SEND_CODE = BASE + "/auth/send_password"
    LOGIN = BASE + "/auth/login"
    APPS = BASE + "/apps"
    APPS_CREATE = BASE + "/apps/create"

    def __init__(self, proxy_map: Optional[Dict[str, str]] = None):
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": (
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/128.0.0.0 Safari/537.36"
            ),
            "Accept-Language": "en-US,en;q=0.9",
        })
        self.proxy_map = proxy_map or {}
        if self.proxy_map:
            self.session.proxies.update(self.proxy_map)

    def _request(self, method: str, url: str, **kwargs) -> requests.Response:
        last_exc = None
        for attempt in range(config.MAX_NETWORK_RETRIES):
            try:
                timeout = kwargs.pop("timeout", config.REQUEST_TIMEOUT)
                response = self.session.request(
                    method, url, timeout=timeout, allow_redirects=True, **kwargs
                )
                if response.status_code in (429, 502, 503, 504):
                    if attempt + 1 < config.MAX_NETWORK_RETRIES:
                        time.sleep(2 ** attempt)
                        continue
                return response
            except requests.RequestException as exc:
                last_exc = exc
                if attempt + 1 < config.MAX_NETWORK_RETRIES:
                    time.sleep(2 ** attempt)
        raise PortalError(
            "ارتباط با my.telegram.org برقرار نشد. "
            "اتصال/پروکسی هاست را بررسی کن."
        ) from last_exc

    @staticmethod
    def _looks_rate_limited(text: str) -> bool:
        t = (text or "").lower()
        markers = [
            "too many tries",
            "too many requests",
            "flood",
            "try again later",
            "429",
        ]
        return any(m in t for m in markers)

    def prepare(self) -> None:
        response = self._request("GET", self.AUTH_PAGE)
        if response.status_code != 200:
            raise PortalError(
                f"صفحه ورود Telegram باز نشد (HTTP {response.status_code})."
            )

    def send_code(self, phone: str) -> str:
        self.prepare()
        headers = {
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
            "Origin": self.BASE,
            "Referer": self.AUTH_PAGE,
            "X-Requested-With": "XMLHttpRequest",
            "Accept": "application/json, text/javascript, */*; q=0.01",
        }
        response = self._request(
            "POST",
            self.SEND_CODE,
            data={"phone": phone},
            headers=headers,
        )
        if self._looks_rate_limited(response.text):
            raise RateLimitError(
                "Telegram درخواست کد جدید را موقتاً محدود کرده است. "
                "چند ساعت صبر کن و دوباره فقط یک بار امتحان کن."
            )
        try:
            data = response.json()
        except ValueError:
            data = {}

        random_hash = data.get("random_hash")
        if not random_hash:
            msg = self._extract_server_message(response.text)
            raise LoginError(
                "Telegram نتوانست کد ورود را ارسال کند"
                + (f": {msg}" if msg else ".")
            )
        return str(random_hash)

    def login(self, phone: str, random_hash: str, code: str) -> None:
        headers = {
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
            "Origin": self.BASE,
            "Referer": self.AUTH_PAGE,
            "X-Requested-With": "XMLHttpRequest",
            "Accept": "application/json, text/javascript, */*; q=0.01",
        }
        response = self._request(
            "POST",
            self.LOGIN,
            data={
                "phone": phone,
                "random_hash": random_hash,
                "password": code,
            },
            headers=headers,
        )
        body = (response.text or "").strip()
        lower = body.lower()

        if lower in ("true", "1", '"true"'):
            return

        if self._looks_rate_limited(body):
            raise RateLimitError(
                "Telegram ورود را موقتاً محدود کرده است. بعداً دوباره امتحان کن."
            )

        if "invalid" in lower and "code" in lower:
            raise LoginError("کد ورود اشتباه است.")
        if "expired" in lower and "code" in lower:
            raise LoginError("کد ورود منقضی شده است.")
        if "password" in lower and "required" in lower:
            raise LoginError(
                "حساب برای ورود به این صفحه به مرحله رمز اضافی نیاز دارد؛ "
                "این نسخه آن مرحله را عمداً در سرور ذخیره نمی‌کند."
            )

        msg = self._extract_server_message(body)
        raise LoginError(
            "ورود به my.telegram.org ناموفق بود"
            + (f": {msg}" if msg else f" (HTTP {response.status_code}).")
        )

    @staticmethod
    def _extract_server_message(text: str) -> str:
        if not text:
            return ""
        soup = BeautifulSoup(text, "html.parser")
        for selector in [".alert", ".alert-danger", ".help-block", ".error"]:
            node = soup.select_one(selector)
            if node:
                value = node.get_text(" ", strip=True)
                if value:
                    return value[:300]
        clean = re.sub(r"\s+", " ", soup.get_text(" ", strip=True))
        return clean[:300] if clean else ""

    @staticmethod
    def _text_of(node) -> str:
        return node.get_text(" ", strip=True) if node else ""

    @classmethod
    def _extract_existing_credentials(cls, soup: BeautifulSoup) -> Optional[Tuple[str, str]]:
        # Selector 1: Check inputs or standard spans
        candidates = [
            ("#app_id", "#app_hash"),
            ("span.form-control.input-xlarge.uneditable-input:nth-of-type(1)",
             "span.form-control.input-xlarge.uneditable-input:nth-of-type(2)"),
        ]
        for id_sel, hash_sel in candidates:
            n1 = soup.select_one(id_sel)
            n2 = soup.select_one(hash_sel)
            if n1 and n2:
                a = cls._text_of(n1)
                h = cls._text_of(n2)
                if re.fullmatch(r"\d{3,20}", a or "") and re.fullmatch(r"[A-Fa-f0-9]{20,128}", h or ""):
                    return a, h

        # Selector 2: Search by label
        def by_label(label_text: str) -> str:
            label = soup.find("label", string=lambda s: isinstance(s, str) and label_text.lower() in s.lower())
            if not label:
                return ""
            sibling = label.find_next_sibling()
            if not sibling:
                return ""
            span = sibling.select_one("span")
            return cls._text_of(span or sibling)

        api_id = by_label("App api_id:")
        api_hash = by_label("App api_hash:")
        if re.fullmatch(r"\d{3,20}", api_id or "") and re.fullmatch(r"[A-Fa-f0-9]{20,128}", api_hash or ""):
            return api_id, api_hash

        # Selector 3: Search all uneditable spans
        spans = soup.select("span.form-control.input-xlarge.uneditable-input")
        texts = [cls._text_of(x) for x in spans]
        numeric = next((x for x in texts if re.fullmatch(r"\d{3,20}", x or "")), "")
        hashed = next((x for x in texts if re.fullmatch(r"[A-Fa-f0-9]{20,128}", x or "")), "")
        if numeric and hashed:
            return numeric, hashed

        # Selector 4: Regex on entire body text
        all_text = soup.get_text()
        m1 = re.search(r"App api_id:\s*(\d+)", all_text) or re.search(r"\b(\d{5,10})\b", all_text)
        m2 = re.search(r"App api_hash:\s*([a-fA-F0-9]{32})", all_text) or re.search(r"\b([a-fA-F0-9]{32})\b", all_text)
        if m1 and m2:
            return m1.group(1), m2.group(1)

        return None

    @staticmethod
    def _extract_create_hash(soup: BeautifulSoup) -> str:
        for selector in [
            "input[name='hash']",
            "input[name=\"hash\"]",
            "input[name='csrf']",
            "input[name=\"csrf\"]",
            "input[name='auth_hash']",
            "input[name=\"auth_hash\"]",
        ]:
            node = soup.select_one(selector)
            value = (node.get("value") if node else "") or ""
            if value and re.fullmatch(r"[A-Za-z0-9_-]{6,256}", str(value)):
                return str(value)

        for pattern in [
            r'name=["\']hash["\']\s+value=["\']([A-Za-z0-9_-]{6,256})["\']',
            r'"hash"\s*:\s*"([A-Za-z0-9_-]{6,256})"',
            r"'hash'\s*:\s*'([A-Za-z0-9_-]{6,256})'",
            r'window\.[A-Za-z0-9_]*hash\s*=\s*["\']([A-Za-z0-9_-]{6,256})["\']',
            r'\bhash\s*[:=]\s*["\']([A-Za-z0-9_-]{6,256})["\']',
        ]:
            match = re.search(pattern, str(soup), flags=re.IGNORECASE)
            if match:
                return match.group(1)

        return ""

    @staticmethod
    def _build_create_payload_variants(
        create_hash: str,
        app_title: str,
        shortname: str,
    ) -> list[dict]:
        """Return a small set of known-valid form bodies.

        The endpoint is not an API with a documented schema. Community clients
        consistently use ``www.telegram.org`` and platform ``other``. Sending
        hundreds of speculative bodies is counterproductive: a rejected POST
        can count towards Telegram's anti-abuse limits.
        """
        title = (app_title or "MyTelegramApp").strip()[:64] or "MyTelegramApp"
        base_short = re.sub(r"[^a-zA-Z0-9]", "", shortname or "appx").lower()[:32]
        if len(base_short) < 5:
            base_short = "appx"
        description = (
            getattr(config, "APP_DESCRIPTION", "")
            or "Personal Telegram API application"
        ).strip()[:255]

        return [
            {
                "hash": create_hash,
                "app_title": title,
                "app_shortname": base_short,
                "app_url": "www.telegram.org",
                "app_platform": "other",
                "app_desc": description,
            },
            {
                "hash": create_hash,
                "app_title": title,
                "app_shortname": f"{base_short[:27]}tg",
                "app_url": "https://telegram.org",
                "app_platform": "other",
                "app_desc": description,
            },
            {
                "hash": create_hash,
                "app_title": title,
                "app_shortname": f"{base_short[:26]}api",
                "app_url": "www.telegram.org",
                "app_platform": "desktop",
                "app_desc": description,
            },
        ]

    @staticmethod
    def _unique_shortname() -> str:
        # Must be 5 to 32 lowercase alphanumeric chars
        suffix = "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8))
        return f"app{suffix}"

    def get_credentials(self) -> Optional[Credentials]:
        response = self._request("GET", self.APPS)
        if response.status_code != 200:
            raise PortalError(
                f"صفحه API development tools باز نشد (HTTP {response.status_code})."
            )
        soup = BeautifulSoup(response.text, "html.parser")
        existing = self._extract_existing_credentials(soup)
        if existing:
            return Credentials(existing[0], existing[1], True)

        return None

    def create_application_and_get_credentials(self) -> Credentials:
        response = self._request("GET", self.APPS)
        if response.status_code != 200:
            raise AppCreateError(
                f"قبل از ساخت برنامه، /apps با HTTP {response.status_code} برگشت."
            )

        soup = BeautifulSoup(response.text, "html.parser")

        existing = self._extract_existing_credentials(soup)
        if existing:
            return Credentials(existing[0], existing[1], True)

        create_hash = self._extract_create_hash(soup)
        if not create_hash:
            raise AppCreateError(
                "فیلد امنیتی (hash) فرم ساخت برنامه پیدا نشد؛ "
                "ساختار my.telegram.org تغییر کرده است یا نشست معتبر نیست."
            )

        headers = {
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
            "Origin": self.BASE,
            "Referer": self.APPS,
            "X-Requested-With": "XMLHttpRequest",
            "Accept": "text/html, */*; q=0.01",
        }

        last_error = None
        for index, payload in enumerate(
            self._build_create_payload_variants(
                create_hash=create_hash,
                app_title=getattr(config, "APP_TITLE", "MyTelegramApp"),
                shortname=self._unique_shortname(),
            ),
            start=1,
        ):
            try:
                create_response = self.session.post(
                    self.APPS_CREATE,
                    data=payload,
                    headers=headers,
                    timeout=config.REQUEST_TIMEOUT,
                    allow_redirects=True,
                )
            except requests.RequestException as exc:
                last_error = f"variant {index}: {exc}"
                continue

            try:
                verify = self._request("GET", self.APPS)
                verify_soup = BeautifulSoup(verify.text, "html.parser")
                existing = self._extract_existing_credentials(verify_soup)
                if existing:
                    return Credentials(existing[0], existing[1], False)
            except PortalError:
                pass

            body = create_response.text or ""
            if self._looks_rate_limited(body):
                raise RateLimitError(
                    "Telegram ساخت برنامه را موقتاً محدود کرده است؛ "
                    "درخواست‌های بیشتری ارسال نمی‌کنیم. بعداً دوباره امتحان کن."
                )

            msg = self._extract_server_message(body)
            if msg:
                last_error = f"variant {index}: {msg[:180]}"
                continue
            if create_response.status_code not in (200, 201, 302, 303, 307, 308):
                last_error = f"variant {index}: HTTP {create_response.status_code}"
                continue

            last_error = f"variant {index}: پاسخ موفق بود اما Application تأیید نشد"

        if last_error:
            raise AppCreateError(
                "ساخت Application با روش‌های معتبر ناموفق بود. "
                f"آخرین خطا: {last_error}"
            )

        raise AppCreateError(
            "ساخت Application ناموفق بود؛ my.telegram.org هیچ راه‌حل سازگاری را قبول نکرد."
        )

    def run(self, phone: str, code: str) -> Credentials:
        random_hash = self.send_code(phone)
        self.login(phone, random_hash, code)
        existing = self.get_credentials()
        if existing:
            return existing
        return self.create_application_and_get_credentials()


def build_proxy_map():
    result = {}
    if getattr(config, "HTTP_PROXY", "").strip():
        result["http"] = config.HTTP_PROXY.strip()
    if getattr(config, "HTTPS_PROXY", "").strip():
        result["https"] = config.HTTPS_PROXY.strip()
    return result


def get_credentials(phone: str, code: str) -> Credentials:
    client = TelegramPortal(build_proxy_map())
    return client.run(phone, code)