# -*- coding: utf-8 -*-
# TG bot: generates personalized Steam landing pages from a profile link.
# /start -> "Сгенерировать" button -> asks for profile URL -> scrapes nick+avatar
# -> copies the template, replaces Ryan Cooper persona + avatar everywhere
# -> serves it at /p/<template-slug>/<random-id>/ and sends the link.
import asyncio, os, re, shutil, secrets, string, sys, urllib.request, json, time

from aiogram import Bot, Dispatcher, F
from aiogram.filters import CommandStart
from aiogram.types import (
    Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, FSInputFile
)
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage

# ---------------- config ----------------
BOT_TOKEN = os.environ.get("BOT_TOKEN", "PUT_YOUR_TOKEN_HERE")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TEMPLATE_DIR = os.path.join(BASE_DIR, "template")          # template/  (index.html + assets/)
PAGES_DIR = os.path.join(BASE_DIR, "p")                    # p/<slug>/<id>/
PUBLIC_BASE = os.environ.get("PUBLIC_BASE", "http://127.0.0.1:8733")
SLUG = "kma-mhc"                                           # /p/kma-mhc/<id>/
HTTP_PORT = int(os.environ.get("PORT", "8733"))

TEMPLATE_PERSONA = "ᵛᵃˡᵛᵉ Ryan Cooper"
TEMPLATE_TITLE = "Steam Community :: ᵛᵃˡᵛᵉ Ryan Cooper"
TEMPLATE_ACCOUNT = "rapid153"
TEMPLATE_STEAMID = "76561197997579211"

UA = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0"}

# ---------------- profile scraping ----------------
def steam_id_from_url(url: str) -> str | None:
    m = re.search(r"steamcommunity\.com/(?:profiles|id)/([^/?#]+)/?", url)
    return m.group(1) if m else None

UA_POOL = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15",
    "Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
]

def _get(url: str, tries: int = 4) -> bytes:
    """GET with UA rotation + exponential backoff for 429/5xx."""
    last = None
    for i in range(tries):
        headers = {"User-Agent": UA_POOL[i % len(UA_POOL)],
                   "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                   "Accept-Language": "en-US,en;q=0.9"}
        req = urllib.request.Request(url, headers=headers)
        try:
            with urllib.request.urlopen(req, timeout=25) as r:
                return r.read()
        except urllib.error.HTTPError as e:
            last = e
            if e.code in (429, 500, 502, 503):
                time.sleep(4 * (i + 1) + secrets.randbelow(3))
                continue
            raise
        except urllib.error.URLError:
            time.sleep(3)
            continue
    raise last if last else RuntimeError("network error")

def fetch_profile(url: str) -> tuple[str, str] | None:
    """returns (persona_name, avatar_url); XML fallback when HTML is rate-limited"""
    # normalize: /id/<vanity>/ or /profiles/<steamid> both work directly,
    # but the bulletproof path is the XML endpoint (different rate bucket)
    sid = steam_id_from_url(url)
    xml_url = None
    if re.fullmatch(r"\d{17}", sid or ""):
        xml_url = f"https://steamcommunity.com/profiles/{sid}/?xml=1"
    else:
        xml_url = f"https://steamcommunity.com/id/{sid}/?xml=1"

    persona, avatar_url = None, None
    # 1) try XML first (rarely rate-limited)
    try:
        data = _get(xml_url).decode("utf-8", errors="replace")
        m = re.search(r"<steamID><!\[CDATA\[(.*?)\]\]></steamID>", data, re.S)
        av = re.search(r"<avatarFull><!\[CDATA\[(.*?)\]\]></avatarFull>", data, re.S)
        if m:
            persona = m.group(1).strip()
            avatar_url = av.group(1).strip() if av else None
    except Exception:
        pass
    # 2) fallback to HTML if XML gave nothing
    if not persona:
        try:
            html = _get(url).decode("utf-8", errors="replace")
            m = re.search(r'<span class="actual_persona_name">\s*(.*?)\s*</span>', html) \
                or re.search(r"<title>Steam Community :: (.*?)</title>", html)
            if m:
                persona = m.group(1).strip()
            av = re.search(r'playerAvatarAutoSizeInner.*?<img[^>]+srcset="([^"]+)"', html, re.S)
            if av:
                avatar_url = av.group(1).split(" ")[0]
            elif not avatar_url:
                av = re.search(r'playerAvatarAutoSizeInner.*?<img[^>]+src="([^"]+)"', html, re.S)
                if av:
                    avatar_url = av.group(1)
        except Exception:
            pass
    if not persona:
        return None
    return persona, avatar_url

def download(url: str, dest: str):
    data = _get(url, tries=5)
    with open(dest, "wb") as f:
        f.write(data)

def rand_id(n=8):
    return "".join(secrets.choice(string.ascii_lowercase) for _ in range(n))

def html_escape(s: str) -> str:
    return (s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
             .replace('"', "&quot;"))

def build_page(persona: str, avatar_bytes: bytes, avatar_ext: str, profile_url: str) -> str:
    """Copy template -> page dir, replace persona + avatar everywhere. Returns page id."""
    pid = rand_id()
    page_dir = os.path.join(PAGES_DIR, SLUG, pid)
    shutil.copytree(TEMPLATE_DIR, page_dir)

    # figure avatar filename used by template header
    idx_path = os.path.join(page_dir, "index.html")
    html = open(idx_path, encoding="utf-8").read()

    # the template's header avatar file (ref_avatar.jpg in header + profile avatar file).
    # Strategy: find every local img referenced from assets that belongs to the Ryan
    # Cooper persona and overwrite those FILES with the new avatar bytes instead of
    # rewriting markup — keeps html structure 1:1.
    avatar_targets = set()
    # ONLY the small header avatar next to the nickname (user_avatar block).
    # The big profile avatar stays as-is (template's own).
    for m in re.finditer(r'user_avatar[^"]*"[^>]*>\s*<img src="\./assets/([^"]+)"', html, re.S):
        avatar_targets.add(m.group(1))

    wrote = []
    for rel in avatar_targets:
        p = os.path.join(page_dir, "assets", rel)
        if os.path.exists(p):
            with open(p, "wb") as f:
                f.write(avatar_bytes)
            wrote.append(rel)

    # persona replacement everywhere (visible text, title, meta, og:)
    esc = html_escape(persona)
    html = html.replace(TEMPLATE_TITLE, "Steam Community :: " + esc)
    html = html.replace(TEMPLATE_PERSONA, esc)           # spans, alt, aria
    html = html.replace(html_escape(TEMPLATE_PERSONA), esc)
    # account dropdown name (Account details: rapid153) -> keep steamid-ish neutral
    html = html.replace('>' + TEMPLATE_ACCOUNT + '<', '>' + esc + '<')

    # redirect: header avatar link -> user's real profile URL
    safe_url = profile_url.replace("'", "\\'")
    html = html.replace(
        '<a href="#" class="user_avatar',
        '<a href="' + safe_url + '" class="user_avatar', 1)

    # redirect: nickname button (account_pulldown) -> user's real profile URL
    html = html.replace(
        'id="account_pulldown">',
        'id="account_pulldown" onclick="window.location.href=\'' + safe_url + '\'">', 1)

    # redirect: username menu item in top nav (supernav username) -> user's real profile URL
    html = re.sub(
        r'(<a class="menuitem supernav supernav_active username") href="#"([^>]*)>',
        r'\1 href="' + safe_url + '"\2>', html, count=1)

    open(idx_path, "w", encoding="utf-8").write(html)
    return pid

# ---------------- FSM ----------------
class Gen(StatesGroup):
    waiting_link = State()

# ---------------- http server (threaded) ----------------
import threading
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler

def run_server():
    class H(SimpleHTTPRequestHandler):
        def __init__(self, *a, **kw):
            super().__init__(*a, directory=BASE_DIR, **kw)
        def log_message(self, *a):
            pass
    srv = ThreadingHTTPServer(("127.0.0.1", HTTP_PORT), H)
    srv.serve_forever()

# ---------------- bot ----------------
async def main():
    if BOT_TOKEN == "PUT_YOUR_TOKEN_HERE":
        print("set BOT_TOKEN env var"); sys.exit(1)
    bot = Bot(BOT_TOKEN)
    dp = Dispatcher(storage=MemoryStorage())

    @dp.message(CommandStart())
    async def start(m: Message, state: FSMContext):
        await state.clear()
        kb = InlineKeyboardMarkup(inline_keyboard=[
            [InlineKeyboardButton(text="Сгенерировать", callback_data="gen")]
        ])
        await m.answer("Жми кнопку, чтобы собрать страницу.", reply_markup=kb)

    @dp.callback_query(F.data == "gen")
    async def gen(c: CallbackQuery, state: FSMContext):
        await state.set_state(Gen.waiting_link)
        await c.message.answer("Пришли ссылку на Steam-профиль:\n"
                               "https://steamcommunity.com/profiles/7656119...\n"
                               "https://steamcommunity.com/id/username/")
        await c.answer()

    @dp.message(Gen.waiting_link)
    async def link(m: Message, state: FSMContext):
        url = (m.text or "").strip()
        if not steam_id_from_url(url):
            await m.answer("Это не ссылка на Steam-профиль. Пришли вида "
                           "https://steamcommunity.com/profiles/... или /id/...")
            return
        wait = await m.answer("Генерирую…")
        try:
            prof = fetch_profile(url)
            if not prof:
                await wait.edit_text("Не смог прочитать профиль (приватный или не отвечает).")
                await state.clear(); return
            persona, avatar_url = prof
            avatar_bytes, ext = b"", ".jpg"
            if avatar_url:
                tmp = os.path.join(BASE_DIR, "_av" + os.path.splitext(avatar_url)[1][:5])
                try:
                    download(avatar_url, tmp)
                    avatar_bytes = open(tmp, "rb").read()
                finally:
                    if os.path.exists(tmp): os.remove(tmp)
            if not avatar_bytes:
                await wait.edit_text("Не смог скачать аватарку.")
                await state.clear(); return

            pid = build_page(persona, avatar_bytes, ext, url)
            link_url = f"{PUBLIC_BASE}/p/{SLUG}/{pid}/"
            await wait.edit_text(
                f"Готово.\nНик: {persona}\nСсылка: {link_url}",
                disable_web_page_preview=False,
            )
        except Exception as e:
            await wait.edit_text(f"Ошибка: {e}")
        finally:
            await state.clear()

    threading.Thread(target=run_server, daemon=True).start()
    await dp.start_polling(bot)

if __name__ == "__main__":
    asyncio.run(main())
