"""
Точка входа. Управляет состоянием, авторизацией и переходами.
"""
import asyncio
import traceback
from pyscript import document, window
from pyodide.ffi import create_proxy

from player import Player, POINTS_PER_LEVEL
from particles import spawn_particles
from ui import inject_rocky_portrait, render_rocky, show_modal, show_screen, render_main_menu
from dialogue import DialogueRunner
from battle import Battle
from tutorial_data import TUTORIAL_DIALOGUE
import auth_client


# ============================================================
#  СОСТОЯНИЕ
# ============================================================
class App:
    def __init__(self):
        self.player = Player()
        self.dialogue = None
        self.battle = None
        self.tutorial_completed = self._load_tutorial_flag()
        self.current_user = None      # имя залогиненного юзера или None
        self.auth_mode = "login"      # "login" / "register"
        self.server_online = False

    @staticmethod
    def _load_tutorial_flag():
        try:
            return window.localStorage.getItem("brawlgame_tutorial_done") == "1"
        except Exception:
            return False

    def mark_tutorial_done(self):
        self.tutorial_completed = True
        try:
            window.localStorage.setItem("brawlgame_tutorial_done", "1")
        except Exception:
            pass


app = App()

# Глобальный множитель наград опыта (баланс: раньше левелап шёл слишком быстро —
# 13→19 за один акт). 0.3 ≈ срезано на 70%.
XP_REWARD_MULT = 0.3
TUTORIAL_XP = 150


# ============================================================
#  СИНХРОНИЗАЦИЯ С СЕРВЕРОМ
# ============================================================
def player_to_dict():
    p = app.player
    return {
        "level": p.level,
        "xp": p.xp,
        "gold": p.gold,
        "strength": p.strength,
        "defense": p.defense,
        "speed": p.speed,
        "energy": p.energy,
        "stamina": p.stamina,
        "upgrade_points": p.upgrade_points,
        "unlocked_abilities": list(p.unlocked_abilities),
        "has_hp": p.has_hp,
        "tutorial_done": app.tutorial_completed,
        "equipped": p.equipped,
        "backpack": p.backpack,
        "chosen_class": p.chosen_class,
        "chosen_abilities": list(p.chosen_abilities),
        "rocky_theme": p.rocky_theme,
        "customize_done": p.customize_done,
        "story_completed": list(p.story_completed),
        "story_unlocked": p.story_unlocked,
    }


def player_from_dict(data):
    p = app.player
    p.level    = data.get("level", 1)
    p.xp       = data.get("xp", 0)
    p.gold     = data.get("gold", 0)
    p.strength = data.get("strength", 10)
    p.defense  = data.get("defense", 10)
    p.speed    = data.get("speed", 10)
    p.energy   = data.get("energy", 10)
    p.stamina  = data.get("stamina", 10)
    p.upgrade_points = data.get("upgrade_points", 0)
    p.unlocked_abilities = set(data.get("unlocked_abilities", []))
    p.has_hp = data.get("has_hp", False)
    if data.get("tutorial_done"):
        app.mark_tutorial_done()
    # инвентарь
    from inventory import INITIAL_WEAPON, SLOTS
    eq = data.get("equipped")
    if isinstance(eq, dict):
        # дополним недостающие слоты
        for s in SLOTS:
            if s not in eq:
                eq[s] = None
        # если нет оружия — поставим базовое
        if eq.get("weapon") is None:
            eq["weapon"] = dict(INITIAL_WEAPON)
        p.equipped = eq
    bp = data.get("backpack")
    if isinstance(bp, list):
        p.backpack = bp
    # класс
    p.chosen_class = data.get("chosen_class")
    p.chosen_abilities = list(data.get("chosen_abilities", []))
    # кастомизация
    p.rocky_theme = data.get("rocky_theme", "stone")
    p.customize_done = data.get("customize_done", False)
    # сюжет
    p.story_completed = set(data.get("story_completed", []))
    p.story_unlocked = data.get("story_unlocked", "1-1")


LOCAL_SAVE_KEY = "brawlgame_progress"

def save_local():
    """Сохраняет прогресс в localStorage (работает без сервера)."""
    try:
        import json
        data = player_to_dict()
        data["tutorial_done"] = app.tutorial_completed
        window.localStorage.setItem(LOCAL_SAVE_KEY, json.dumps(data))
    except Exception as e:
        print("Local save error:", e)

def load_local():
    """Загружает прогресс из localStorage."""
    try:
        import json
        raw = window.localStorage.getItem(LOCAL_SAVE_KEY)
        if raw:
            data = json.loads(raw)
            player_from_dict(data)
            if data.get("tutorial_done"):
                app.mark_tutorial_done()
            return True
    except Exception as e:
        print("Local load error:", e)
    return False


async def sync_to_server():
    save_local()  # всегда сохраняем локально
    if app.current_user is None:
        return
    try:
        await auth_client.push_progress(player_to_dict())
    except Exception as e:
        print("Save error:", e)


def schedule_sync():
    save_local()  # локальное сохранение сразу
    if app.current_user is None:
        return
    asyncio.ensure_future(sync_to_server())


# ============================================================
#  UI: ХЕДЕР И СТАТУС СЕРВЕРА
# ============================================================
def render_user_header():
    info_el = document.getElementById("user-info")
    btn_el  = document.getElementById("signin-btn")
    if app.current_user is None:
        info_el.classList.remove("show")
        btn_el.style.display = ""
    else:
        info_el.classList.add("show")
        btn_el.style.display = "none"
        document.getElementById("hdr-username").textContent = app.current_user
        document.getElementById("hdr-level").textContent = "LVL " + str(app.player.level)


def render_server_status(online):
    el = document.getElementById("server-status")
    el.classList.remove("online", "offline")
    if online:
        el.classList.add("online")
        el.textContent = "● Server online"
    else:
        el.classList.add("offline")
        el.textContent = "○ Offline · Login disabled"


# ============================================================
#  МОДАЛКА АВТОРИЗАЦИИ
# ============================================================
def open_auth_modal():
    if not app.server_online:
        show_modal("SERVER OFFLINE",
            "The backend is not running. Start it with: "
            "python -m uvicorn server:app --port 8001")
        return
    document.getElementById("auth-overlay").classList.remove("hidden")
    set_auth_mode("login")
    set_auth_message("", "")
    document.getElementById("auth-username").value = ""
    document.getElementById("auth-password").value = ""
    document.getElementById("auth-username").focus()


def close_auth_modal():
    document.getElementById("auth-overlay").classList.add("hidden")


def set_auth_mode(mode):
    app.auth_mode = mode
    is_login = mode == "login"
    document.getElementById("auth-title").textContent = "SIGN IN" if is_login else "REGISTER"
    # сабмит — каменная плита: меняем картинку через класс режима
    submit = document.getElementById("auth-submit")
    if is_login:
        submit.classList.remove("mode-register")
    else:
        submit.classList.add("mode-register")
    tabs = document.querySelectorAll(".auth-tab")
    for i in range(tabs.length):
        t = tabs.item(i)
        action = t.getAttribute("data-action")
        if (is_login and action == "auth-tab-login") or ((not is_login) and action == "auth-tab-register"):
            t.classList.add("active")
        else:
            t.classList.remove("active")
    document.getElementById("auth-username-hint").style.display = "" if (not is_login) else "none"
    document.getElementById("auth-password-hint").style.display = "" if (not is_login) else "none"
    set_auth_message("", "")


def set_auth_message(text, kind="info"):
    el = document.getElementById("auth-message")
    el.textContent = text
    el.classList.remove("error", "success", "info")
    if text:
        el.classList.add(kind)


# ============================================================
#  АВТОРИЗАЦИЯ
# ============================================================
async def submit_auth():
    username = document.getElementById("auth-username").value.strip()
    password = document.getElementById("auth-password").value

    if len(username) < 3:
        set_auth_message("Username must be at least 3 characters", "error")
        return
    if len(password) < 6:
        set_auth_message("Password must be at least 6 characters", "error")
        return

    btn = document.getElementById("auth-submit")
    btn.disabled = True
    btn.classList.add("loading")
    set_auth_message("Connecting...", "info")

    if app.auth_mode == "login":
        ok, msg = await auth_client.login(username, password)
    else:
        ok, msg = await auth_client.register(username, password)

    btn.disabled = False
    btn.classList.remove("loading")

    if not ok:
        set_auth_message(msg, "error")
        return

    app.current_user = msg
    set_auth_message("Welcome, " + msg + "!", "success")

    progress = await auth_client.fetch_progress()
    if progress:
        player_from_dict(progress)
    else:
        await sync_to_server()

    render_user_header()
    render_rocky(app.player)
    window.setTimeout(create_proxy(close_auth_modal), 600)


def logout():
    auth_client.clear_token()
    app.current_user = None
    app.player = Player()
    render_user_header()
    render_rocky(app.player)
    show_modal("LOGGED OUT", "See you next time, warrior.")


async def try_auto_login():
    token = auth_client.get_token()
    saved = auth_client.get_saved_username()
    if not token or not saved:
        return
    progress = await auth_client.fetch_progress()
    if progress is None:
        if not auth_client.get_token():
            return
        progress = {}
    app.current_user = saved
    if progress:
        player_from_dict(progress)
    render_user_header()
    render_rocky(app.player)
    print("Auto-logged in as " + saved)


# ============================================================
#  ДЕЙСТВИЯ
# ============================================================
def on_action(action):
    p = app.player

    if action == "open-auth":
        open_auth_modal()
    elif action == "auth-close":
        close_auth_modal()
    elif action == "auth-tab-login":
        set_auth_mode("login")
    elif action == "auth-tab-register":
        set_auth_mode("register")
    elif action == "auth-submit":
        asyncio.ensure_future(submit_auth())
    elif action == "auth-guest":
        close_auth_modal()
    elif action == "logout":
        logout()

    elif action == "story":
        # сюжет доступен после туториала (гостевой режим тоже работает)
        if not app.tutorial_completed:
            show_modal("PATH UNKNOWN",
                "Complete the tutorial first \u2014 you must choose your path "
                "before walking it.")
        else:
            show_screen("screen-story")
            from ui import render_story_screen
            render_story_screen(app.player, on_chapter_pick=launch_story_chapter)
    elif action == "rocky":
        show_screen("screen-rocky")
        render_rocky(p)
        # подсказка от Лирона висит ВСЕГДА, пока игрок в меню My Rocky
        # (показываем при каждом входе на экран).
        window.setTimeout(create_proxy(lambda *_: show_lyron_tip(
            "Gather equipment, level up, and hoard gold, fighter. "
            "Soon the <b>Shop</b> and the <b>Arena</b> &mdash; player&nbsp;vs&nbsp;player "
            "with a global <b>leaderboard</b> &mdash; will open their gates."
        )), 400)
    elif action == "tutorial":
        if app.tutorial_completed:
            launch_battle(give_reward=False)
        else:
            start_tutorial_with_dialogue()
    elif action == "back-main":
        if app.battle is not None:
            app.battle.end()
            app.battle = None
        if app.dialogue is not None:
            app.dialogue = None
        _show_main()
    elif action == "equipment":
        from ui import show_equipment_modal
        show_equipment_modal(app.player,
                             on_change=lambda p: (render_rocky(p), schedule_sync()))
    elif action == "customize":
        from ui import show_customization
        show_customization(app.player, on_confirm=lambda theme: (render_rocky(app.player), schedule_sync()))
    elif action == "begin":
        if app.tutorial_completed:
            launch_battle(give_reward=False)
        else:
            start_tutorial_with_dialogue()

    elif action == "upgrade-str":
        if p.upgrade("strength"):
            render_rocky(p); render_user_header(); schedule_sync()
    elif action == "upgrade-def":
        if p.upgrade("defense"):
            render_rocky(p); render_user_header(); schedule_sync()
    elif action == "upgrade-spd":
        if p.upgrade("speed"):
            render_rocky(p); render_user_header(); schedule_sync()

    elif action == "dlg-next":
        if app.dialogue is not None:
            app.dialogue.advance()
    elif action == "dlg-skip":
        if app.dialogue is not None:
            app.dialogue.skip_all()

    elif action == "toggle-combos":
        toggle_combo_panel()


# ============================================================
#  БОЙ
# ============================================================
def start_tutorial_with_dialogue():
    show_screen("screen-dialogue")
    app.dialogue = DialogueRunner(TUTORIAL_DIALOGUE,
                                  on_finish=lambda: launch_battle(give_reward=True, is_tutorial=True))
    app.dialogue.start()


def launch_battle(give_reward=True, enemy_def=None, on_end_callback=None, is_tutorial=False):
    """
    Запускает бой.
    enemy_def — словарь {name, hp, strength, defense, svg} для не-туториальных боёв.
    on_end_callback(won) — кастомный коллбэк (например для сюжетного режима).
                          Если None — используется стандартный on_battle_end.
    """
    app.dialogue = None
    # Глушим прошлый бой целиком: иначе его отложенные таймеры продолжат
    # писать чужой спрайт в fighter-p2 и новый враг будет «мигать» старым.
    if getattr(app, "battle", None) is not None:
        try:
            app.battle._stop_all_timers()
            app.battle._unbind_keys()
        except Exception:
            pass
        app.battle = None
    # Force-clear any chapter/req modals that may still be in the DOM
    for _mid in ("req-modal-overlay", "chap-map-overlay"):
        _mel = document.getElementById(_mid)
        if _mel:
            try:
                _mel.style.display = "none"   # hide immediately even if remove races
                if _mel.parentElement:
                    _mel.parentElement.removeChild(_mel)
            except Exception:
                pass
    _mount = document.getElementById("modal-mount")
    if _mount:
        _mount.innerHTML = ""
    show_screen("screen-battle")
    cb = on_end_callback or (lambda won: on_battle_end(won, give_reward))
    app.battle = Battle(
        on_end=cb,
        give_reward=give_reward,
        player_obj=app.player,
        enemy_def=enemy_def,
        is_tutorial=is_tutorial,
    )
    app.battle.start()
    # показываем подсказки комбо на 4с, потом сворачиваем
    panel = document.getElementById("combo-hints")
    if panel is not None:
        panel.classList.remove("collapsed")
        def _auto_collapse(*_):
            p = document.getElementById("combo-hints")
            if p is not None:
                p.classList.add("collapsed")
        window.setTimeout(create_proxy(_auto_collapse), 4000)


def on_battle_end(player_won, give_reward):
    app.battle = None

    if player_won and give_reward:
        is_first_victory = (app.player.chosen_class is None)
        app.player.add_xp(TUTORIAL_XP)
        import random
        gold_reward = random.randint(50, 100)
        app.player.gold += gold_reward
        app.mark_tutorial_done()
        dropped_item = try_drop_loot()
        render_user_header()
        schedule_sync()

        from ui import show_victory_reward
        _show_main()

        def _after_reward():
            if is_first_victory:
                trigger_class_selection()
            else:
                if dropped_item:
                    slot, item = dropped_item
                    show_loot_toast(item, slot)

        window.setTimeout(
            create_proxy(lambda *_: show_victory_reward(
                xp=TUTORIAL_XP,
                gold=gold_reward,
                dropped_item=dropped_item,
                on_close=_after_reward
            )),
            400
        )
        return
    elif player_won and (not give_reward):
        show_modal("TRAINING COMPLETE",
            "Well fought. The forms are settling into your bones. "
            "(No rewards \u2014 this was practice.)")
    else:
        show_modal("DEFEAT",
            "The earth shakes, but you have fallen. "
            "Train. Grow. Return when you are ready.")
    _show_main()


# ============================================================
#  СЮЖЕТНЫЙ РЕЖИМ
# ============================================================
def launch_story_chapter(act_id, chap_id):
    """Запускает главу: диалог-до → бой → диалог-после → награды."""
    from story_data import get_chapter, ENEMIES

    act, chapter = get_chapter(act_id, chap_id)
    if not chapter:
        return

    enemy_key = chapter["enemy"]
    enemy_def = dict(ENEMIES.get(enemy_key, {}))
    enemy_def["bg"] = chapter.get("bg")

    def step_battle():
        app.dialogue = None
        launch_battle(
            give_reward=False,
            enemy_def=enemy_def,
            on_end_callback=lambda won: step_post_dialogue(won),
        )

    def step_post_dialogue(player_won):
        app.battle = None
        if player_won:
            lines = chapter.get("dialogue_after_win", [])
        else:
            lines = chapter.get("dialogue_after_loss", [])

        def after_dlg():
            app.dialogue = None
            step_resolve(player_won)

        if lines:
            show_screen("screen-dialogue")
            app.dialogue = DialogueRunner(lines, on_finish=after_dlg)
            app.dialogue.start()
        else:
            after_dlg()

    def step_resolve(player_won):
        if player_won:
            chap_key = f"{act_id}-{chap_id}"
            first_clear = chap_key not in app.player.story_completed
            app.player.story_completed.add(chap_key)
            rewards = chapter["rewards"]
            xp = int(rewards.get("xp", 0) * XP_REWARD_MULT)   # урезанный опыт
            gold = rewards.get("gold", 0)
            if not first_clear:
                xp = xp // 2
                gold = gold // 2
            app.player.add_xp(xp)
            app.player.gold += gold
            # лут падает только при первом прохождении главы;
            # переигровка даёт лишь золото и опыт (вдвое меньше)
            dropped = try_drop_loot() if first_clear else None
            from story_data import next_chapter
            nx_act, nx_chap = next_chapter(act_id, chap_id)
            if nx_act is not None:
                app.player.story_unlocked = f"{nx_act}-{nx_chap}"
            render_user_header()
            schedule_sync()

            def _go_to_map():
                show_screen("screen-story")
                from ui import render_story_screen
                render_story_screen(app.player, on_chapter_pick=launch_story_chapter)

            def _after_reward():
                # После показа наград — предлагаем выбрать способность (если first_clear)
                if first_clear and app.player.chosen_class:
                    from ui import show_ability_selection
                    from classes import CLASSES
                    cls = app.player.chosen_class
                    # Способности класса которые ещё не выбраны
                    all_abs = CLASSES.get(cls, {}).get("abilities", [])
                    unlocked = list(app.player.chosen_abilities or [])
                    available = [a for a in all_abs if a not in unlocked]
                    if available:
                        def _after_ability(ab_id):
                            if ab_id and ab_id not in app.player.chosen_abilities:
                                app.player.chosen_abilities.append(ab_id)
                                # Применить в бою (добавить в p_abilities)
                                app.player.unlocked_abilities.add(ab_id)
                            schedule_sync()
                            _go_to_map()
                        show_ability_selection(app.player, cls, _after_ability)
                        return
                _go_to_map()

            # Показываем красивую награду
            from ui import show_victory_reward
            window.setTimeout(
                create_proxy(lambda *_: show_victory_reward(
                    xp=xp,
                    gold=gold,
                    dropped_item=dropped,
                    on_close=_after_reward
                )),
                400
            )
        else:
            show_modal("DEFEAT",
                "The path doesn't end here \u2014 but you must train and return.")
            show_screen("screen-story")
            from ui import render_story_screen
            render_story_screen(app.player, on_chapter_pick=launch_story_chapter)

    lines = chapter.get("dialogue_before", [])
    if lines:
        show_screen("screen-dialogue")
        app.dialogue = DialogueRunner(lines, on_finish=step_battle)
        app.dialogue.start()
    else:
        step_battle()


def trigger_class_selection():
    """Показать модалку выбора класса, потом выбор стартовой способности."""
    from ui import show_class_selection, show_ability_selection
    def after_class(class_id):
        # уже что-то выбирал? — игнорируем
        if app.player.chosen_class:
            return
        app.player.chosen_class = class_id
        from ui import close_modal
        close_modal()
        # применить бонусы класса (один раз)
        from classes import CLASSES
        cls = CLASSES[class_id]
        for stat, val in cls["stat_bonus"].items():
            if hasattr(app.player, stat):
                cur = getattr(app.player, stat)
                setattr(app.player, stat, max(1, cur + val))
        # СРАЗУ привязываем класс к Rocky и сохраняем (локально + на сервер),
        # чтобы класс не терялся, даже если игрок выйдет на выборе способности.
        render_user_header()
        schedule_sync()
        # перейти к выбору способности
        show_ability_selection(app.player, class_id, after_ability)

    def after_ability(ability_id):
        if ability_id not in app.player.chosen_abilities:
            app.player.chosen_abilities.append(ability_id)
        # закрыть модалку и обновить UI
        from ui import close_modal
        close_modal()
        render_rocky(app.player)
        render_user_header()
        schedule_sync()
        show_modal("CHOSEN",
            "Your path is set. Train, fight, grow stronger. "
            "The world awaits.")

    show_class_selection(app.player, after_class)


def try_drop_loot():
    """Случайный шанс дропа предмета. Возвращает (slot, item_dict) или None."""
    import random
    from inventory import roll_loot_drop, DROP_CHANCE
    if random.random() > DROP_CHANCE:
        return None
    drop = roll_loot_drop(app.player.level)
    if drop is None:
        return None
    slot, item = drop
    app.player.add_inventory_item(slot, item)
    schedule_sync()
    return slot, item


def show_lyron_tip(html_text):
    """Диалоговое окошко-подсказка Лирона внизу слева (как при левел-апе)."""
    # не дублируем, если уже висит
    old = document.getElementById("lyron-tip")
    if old is not None:
        try: old.parentElement.removeChild(old)
        except Exception: pass
    tip = document.createElement("div")
    tip.id = "lyron-tip"
    tip.className = "lyron-tip"
    tip.innerHTML = (
        '<button class="lyron-tip-close" id="lyron-tip-close">✕</button>'
        '<div class="lyron-tip-portrait"></div>'
        '<div class="lyron-tip-body">'
        '<div class="lyron-tip-name">LYRON <span>· The Drifter</span></div>'
        f'<div class="lyron-tip-text">{html_text}</div>'
        '</div>'
    )
    document.body.appendChild(tip)
    void = tip.offsetWidth  # noqa  (reflow для анимации)
    tip.classList.add("show")

    def _close(*_):
        t = document.getElementById("lyron-tip")
        if t is not None:
            t.classList.remove("show")
            def _rm(*_):
                try: t.parentElement.removeChild(t)
                except Exception: pass
            window.setTimeout(create_proxy(_rm), 400)

    btn = document.getElementById("lyron-tip-close")
    if btn is not None:
        btn.addEventListener("click", create_proxy(_close))
    # без авто-скрытия: подсказка висит, пока игрок в меню My Rocky
    # (убирается при уходе с экрана в show_screen или по крестику)


def show_loot_toast(item, slot):
    """Показать всплывающее уведомление о дропе."""
    container = document.body
    toast = document.createElement("div")
    toast.className = "loot-toast"
    rarity = item.get("rarity", "common").upper()
    icon = item.get("icon", "▣")
    toast.innerHTML = (
        f'<div class="loot-title">★ ITEM DROPPED ★</div>'
        f'<div class="loot-icon">{icon}</div>'
        f'<div class="loot-name">{item["name"]}</div>'
        f'<div class="loot-rarity">{rarity} · {slot.upper()}</div>'
    )
    container.appendChild(toast)
    # запускаем анимацию
    void = toast.offsetWidth  # noqa
    toast.classList.add("show")
    # убираем через 4.5с (анимация 4с + запас)
    def _remove(*_):
        try:
            container.removeChild(toast)
        except Exception:
            pass
    window.setTimeout(create_proxy(_remove), 4500)


# ============================================================
#  ВВОД
# ============================================================
def handle_click(event):
    target = event.target
    while target and target != document.body:
        if hasattr(target, "getAttribute"):
            try:
                action = target.getAttribute("data-action")
            except Exception:
                action = None
            if action:
                on_action(action)
                return
        target = target.parentElement


def handle_global_key(event):
    overlay = document.getElementById("auth-overlay")
    if overlay is not None and (not overlay.classList.contains("hidden")):
        if event.key == "Enter":
            event.preventDefault()
            asyncio.ensure_future(submit_auth())
        elif event.key == "Escape":
            close_auth_modal()
        return

    if app.dialogue is not None:
        if event.key in (" ", "Enter"):
            event.preventDefault()
            app.dialogue.advance()
        return

    # TAB во время боя — свернуть/развернуть панель комбо
    if app.battle is not None and event.key == "Tab":
        event.preventDefault()
        toggle_combo_panel()


def toggle_combo_panel():
    """Переключить свёрнутость панели комбо."""
    panel = document.getElementById("combo-hints")
    if panel is None:
        return
    panel.classList.toggle("collapsed")


# ============================================================
#  ИНИЦИАЛИЗАЦИЯ — С ЗАЩИТОЙ ОТ ОШИБОК
# ============================================================
def _show_main():
    """Показать главное меню и обновить состояние кнопок."""
    show_screen("screen-main")
    render_main_menu(app.tutorial_completed, app.player.chosen_class)


# ============================================================
#  ИНИЦИАЛИЗАЦИЯ — С ЗАЩИТОЙ ОТ ОШИБОК
# ============================================================
def install_handlers_now():
    """Сначала вешаем обработчики кликов — это самое главное.
    Делается синхронно, не зависит от async."""
    document.body.addEventListener("click", create_proxy(handle_click))
    document.addEventListener("keydown", create_proxy(handle_global_key))
    print("[boot] click/key handlers installed")


def init_static_ui():
    """Статичная инициализация UI — не делает сетевых запросов."""
    spawn_particles()
    inject_rocky_portrait()
    render_rocky(app.player)
    render_user_header()
    render_main_menu(app.tutorial_completed, app.player.chosen_class)
    print("[boot] static UI ready")
    # Показать кастомизацию при первом запуске
    if not app.player.customize_done:
        def _after_customize(theme):
            render_rocky(app.player)
            schedule_sync()
        from ui import show_customization
        window.setTimeout(
            create_proxy(lambda *_: show_customization(app.player, _after_customize)),
            800
        )


async def init_network():
    """Сетевая часть. Может упасть — главное чтобы UI уже работал."""
    try:
        app.server_online = await auth_client.check_server()
        render_server_status(app.server_online)
        print("[boot] server check =", app.server_online)
        if app.server_online:
            await try_auto_login()
            print("[boot] auto-login finished")
    except Exception as e:
        print("[boot] network init error:", e)
        print(traceback.format_exc())
        render_server_status(False)


# ВАЖНО: обработчики ставим СИНХРОННО, до любых async операций.
# Так даже если сеть отвалится — кнопки на странице всё равно будут работать.
try:
    install_handlers_now()
    load_local()  # загружаем локальный прогресс до рендера UI
    init_static_ui()
    print("Brawlgame initialized · Python + WASM")
except Exception as e:
    print("[boot] STATIC INIT ERROR:", e)
    print(traceback.format_exc())

# Сетевая инициализация запускается отдельно
asyncio.ensure_future(init_network())