import { useEffect, useRef, useState, useCallback, useMemo, type CSSProperties } from 'react';
import { GameEngine } from './game/engine';
import { GamePhase, BuffType, type GameState, type ActiveBuff, type NPCData, type AttackPayload, type EnemyRegenPayload } from './game/types';
import type { NPCEncounterEvent } from './game/types';
import { GameWebSocket } from './network/websocket';
import {
  wireWSHandler,
  sendActivateBuff,
  sendUsePotion,
  sendRevive,
  sendNPCAlmsAccept,
  sendNPCAlmsDecline,
  buildLootFromCombatEnd,
  buildMerchantLootDrop,
} from './game/ws-handler';
import {
  ApiError,
  initHero,
  ackChangelog,
  getAdventureLog,
  getTowns,
  getTownNPCs,
  getTownBuildings,
  getHeroQuests,
  getHeroEquipment,
  claimQuest,
  abandonQuest,
  getAchievements,
  getNearbyHeroes,
  buyPotion,
  healAtNPC,
  requestRevive,
  defaultNpcShopCosts,
  npcShopCostsFromInit,
} from './network/api';
import type { HeroResponse, Achievement, ChangelogPayload } from './network/api';
import type { AdventureLogEntry, Town, HeroQuest, NPC, TownData, EquipmentItem, BuildingData } from './game/types';
import type { OfflineReport as OfflineReportData } from './network/api';
import {
  BUFF_COOLDOWN_MS,
  BUFF_DURATION_MS,
  debuffDurationsFromCatalog,
  mapHeroBuffsFromServer,
  mapHeroDebuffsFromServer,
} from './network/buffMap';
import { hapticImpact, hapticNotification, onThemeChanged, getTelegramUserId } from './shared/telegram';
import { Rarity } from './game/types';
import type { HeroState, BuffChargeState } from './game/types';
import { useUiClock } from './hooks/useUiClock';
import {
  adventureEntriesFromServerLog,
  appendAdventureLogMessage,
} from './game/adventureLogMap';
import { parseAdventureLogLine } from './game/adventureLogMarkers';
import { HUD } from './ui/HUD';
import { DeathScreen } from './ui/DeathScreen';
import { CombatOverlay, type CombatOverlayEvent } from './ui/CombatOverlay';
import { CombatLogPanel } from './ui/CombatLogPanel';
import { GameToast } from './ui/GameToast';
import { OfflineReport } from './ui/OfflineReport';
import { HeroSheetModal, type HeroSheetTab } from './ui/HeroSheetModal';
import { NPCDialog } from './ui/NPCDialog';
import { NameEntryScreen } from './ui/NameEntryScreen';
import { ChangelogModal } from './ui/ChangelogModal';
import { AchievementsPanel } from './ui/AchievementsPanel';
import { Minimap } from './ui/Minimap';
import { NPCInteraction } from './ui/NPCInteraction';
import { WanderingNPCPopup } from './ui/WanderingNPCPopup';
import { I18nContext, t, detectLocale, getTranslations, type Locale } from './i18n';

const appStyle: CSSProperties = {
  width: '100%',
  height: '100%',
  position: 'relative',
  overflow: 'hidden',
};

const canvasContainerStyle: CSSProperties = {
  width: '100%',
  height: '100%',
  position: 'absolute',
  top: 0,
  left: 0,
};

const connectionBanner: CSSProperties = {
  position: 'absolute',
  top: 0,
  left: 0,
  right: 0,
  textAlign: 'center',
  padding: '4px 0',
  fontSize: 12,
  fontWeight: 600,
  color: '#fff',
  backgroundColor: 'rgba(204, 51, 51, 0.85)',
  zIndex: 200,
};

function buffCdStorageKey(heroId: number): string {
  return `autohero-buffCd-v1-${heroId}`;
}

function pruneCooldownEnds(
  raw: Partial<Record<BuffType, number>>,
  now: number,
): Partial<Record<BuffType, number>> {
  const out: Partial<Record<BuffType, number>> = {};
  for (const [k, v] of Object.entries(raw)) {
    if (typeof v === 'number' && v > now && (Object.values(BuffType) as string[]).includes(k)) {
      out[k as BuffType] = v;
    }
  }
  return out;
}

function loadCooldownsFromStorage(heroId: number): Partial<Record<BuffType, number>> {
  try {
    const raw = sessionStorage.getItem(buffCdStorageKey(heroId));
    if (!raw) return {};
    const parsed = JSON.parse(raw) as Partial<Record<BuffType, number>>;
    return pruneCooldownEnds(parsed, Date.now());
  } catch {
    return {};
  }
}

function saveCooldownsToStorage(heroId: number, ends: Partial<Record<BuffType, number>>): void {
  try {
    const pruned = pruneCooldownEnds(ends, Date.now());
    sessionStorage.setItem(buffCdStorageKey(heroId), JSON.stringify(pruned));
  } catch {
    /* ignore quota / private mode */
  }
}

function xpToNextLevel(level: number): number {
  if (level < 1) level = 1;
  if (level <= 9) return Math.round(180 * Math.pow(1.28, level - 1));
  if (level <= 29) return Math.round(1450 * Math.pow(1.15, level - 10));
  return Math.round(23000 * Math.pow(1.10, level - 30));
}

/** Map backend buffCharges (keyed by string) into typed Partial<Record<BuffType, BuffChargeState>>. */
function mapBuffCharges(
  raw: Record<string, { remaining: number; periodEnd: string | null }> | undefined,
): Partial<Record<BuffType, BuffChargeState>> {
  if (!raw) return {};
  const out: Partial<Record<BuffType, BuffChargeState>> = {};
  const validTypes = new Set(Object.values(BuffType) as string[]);
  for (const [key, val] of Object.entries(raw)) {
    if (validTypes.has(key)) {
      out[key as BuffType] = { remaining: val.remaining, periodEnd: val.periodEnd };
    }
  }
  return out;
}

/** Map backend equipment record to typed EquipmentItem map. */
function mapEquipment(
  raw: HeroResponse['equipment'],
  res: HeroResponse,
): Record<string, EquipmentItem> {
  const out: Record<string, EquipmentItem> = {};

  // REST uses `equipment`, WS hero_state from Go uses `gear`. Treat JSON null like missing.
  const merged = raw ?? res.gear;
  const rawSlots =
    merged != null && typeof merged === 'object' ? merged : undefined;

  if (rawSlots) {
    for (const [slot, item] of Object.entries(rawSlots)) {
      out[slot] = {
        id: item.id,
        slot: item.slot ?? slot,
        formId: item.formId ?? '',
        name: item.name,
        rarity: (item.rarity?.toLowerCase() ?? 'common') as EquipmentItem['rarity'],
        ilvl: item.ilvl ?? 1,
        primaryStat: item.primaryStat ?? 0,
        statType: item.statType ?? 'mixed',
      };
    }
  }

  // Fallback: populate main_hand from legacy weapon if not already set
  if (!out['main_hand'] && res.weapon) {
    out['main_hand'] = {
      id: res.weapon.id ?? 0,
      slot: 'main_hand',
      formId: '',
      name: res.weapon.name ?? 'Unknown',
      rarity: (res.weapon.rarity?.toLowerCase() ?? 'common') as EquipmentItem['rarity'],
      ilvl: res.weapon.ilvl ?? 1,
      primaryStat: res.weapon.damage ?? 0,
      statType: 'attack',
    };
  }

  if (!out['chest'] && res.armor) {
    out['chest'] = {
      id: res.armor.id ?? 0,
      slot: 'chest',
      formId: '',
      name: res.armor.name ?? 'Unknown',
      rarity: (res.armor.rarity?.toLowerCase() ?? 'common') as EquipmentItem['rarity'],
      ilvl: res.armor.ilvl ?? 1,
      primaryStat: res.armor.defense ?? 0,
      statType: 'defense',
    };
  }

  return out;
}

/** Convert Town (from /towns API) to engine-facing TownData, optionally with NPCs and buildings */
function townToTownData(town: Town, npcs?: NPC[], buildings?: BuildingData[]): TownData {
  const npcData: NPCData[] | undefined = npcs?.map((n) => ({
    id: n.id,
    name: n.name,
    type: n.type,
    worldX: town.worldX + n.offsetX,
    worldY: town.worldY + n.offsetY,
    buildingId: n.buildingId,
  }));
  return {
    id: town.id,
    name: town.name,
    centerX: town.worldX,
    centerY: town.worldY,
    radius: town.radius,
    biome: town.biome,
    levelMin: town.levelMin,
    size: town.radius > 40 ? 'XL' : town.radius > 25 ? 'M' : town.radius > 15 ? 'S' : 'XS',
    npcs: npcData,
    buildings: buildings,
  };
}

function heroResponseToState(res: HeroResponse): HeroState {
  const now = Date.now();
  return {
    id: res.id,
    hp: res.hp,
    maxHp: res.maxHp,
    position: { x: res.positionX ?? 0, y: res.positionY ?? 0 },
    serverActivityState: res.state,
    restKind: res.restKind,
    excursionPhase: res.excursionPhase,
    attackSpeed: res.attackSpeed ?? res.speed,
    damage: res.attackPower ?? res.attack,
    defense: res.defensePower ?? res.defense,
    weaponType: (res.weapon?.type ?? 'sword') as HeroState['weaponType'],
    weaponName: res.weapon?.name ?? '',
    weaponRarity: (res.weapon?.rarity ?? 'common') as Rarity,
    weaponIlvl: res.weapon?.ilvl,
    armorType: (res.armor?.type ?? 'medium') as HeroState['armorType'],
    armorName: res.armor?.name ?? '',
    armorRarity: (res.armor?.rarity ?? 'common') as Rarity,
    armorIlvl: res.armor?.ilvl,
    activeBuffs: mapHeroBuffsFromServer(res.buffs, now),
    debuffs: mapHeroDebuffsFromServer(res.debuffs, now),
    level: res.level,
    xp: res.xp,
    xpToNext: res.xpToNext ?? xpToNextLevel(res.level || 1),
    gold: res.gold,
    strength: res.strength,
    constitution: res.constitution,
    agility: res.agility,
    luck: res.luck,
    reviveCount: res.reviveCount,
    subscriptionActive: res.subscriptionActive,
    buffFreeChargesRemaining: res.buffFreeChargesRemaining,
    buffQuotaPeriodEnd: res.buffQuotaPeriodEnd,
    buffCharges: mapBuffCharges(res.buffCharges),
    potions: res.potions ?? 0,
    moveSpeed: res.moveSpeed,
    equipment: mapEquipment(res.equipment, res),
    inventory: mapInventoryFromResponse(res.inventory),
    debuffCatalogDurations: debuffDurationsFromCatalog(res.debuffCatalog),
  };
}

function mapInventoryFromResponse(raw: HeroResponse['inventory']): EquipmentItem[] | undefined {
  if (!raw?.length) return undefined;
  return raw.map((it) => ({
    id: it.id,
    slot: it.slot,
    formId: it.formId ?? '',
    name: it.name,
    rarity: it.rarity as Rarity,
    ilvl: it.ilvl ?? 1,
    primaryStat: it.primaryStat ?? 0,
    statType: it.statType ?? 'mixed',
  }));
}


export function App() {
  const [locale, setLocale] = useState<Locale>(() => detectLocale());
  const translations = useMemo(() => getTranslations(locale), [locale]);
  const handleSetLocale = useCallback((l: Locale) => {
    setLocale(l);
    try { localStorage.setItem('autohero_locale', l); } catch { /* ignore */ }
  }, []);
  const tr = translations;

  const canvasRef = useRef<HTMLDivElement>(null);
  const engineRef = useRef<GameEngine | null>(null);
  const wsRef = useRef<GameWebSocket | null>(null);

  const [gameState, setGameState] = useState<GameState>({
    phase: GamePhase.Walking,
    hero: null,
    enemy: null,
    loot: null,
    lastVictoryLoot: null,
    tick: 0,
    serverTimeMs: 0,
    routeWaypoints: null,
  });

  const [combatEvents, setCombatEvents] = useState<CombatOverlayEvent[]>([]);
  const [wsConnected, setWsConnected] = useState(false);
  const [wsEverConnected, setWsEverConnected] = useState(false);
  const [buffCooldownEndsAt, setBuffCooldownEndsAt] = useState<
    Partial<Record<BuffType, number>>
  >({});
  const [connectionError, setConnectionError] = useState<string | null>(null);
  const [toast, setToast] = useState<{ message: string; color: string } | null>(null);
  const [logEntries, setLogEntries] = useState<AdventureLogEntry[]>([]);
  /** Live combat narration (mirrors prefixed adventure log lines). */
  const [combatLogLines, setCombatLogLines] = useState<string[]>([]);
  const [offlineReport, setOfflineReport] = useState<OfflineReportData | null>(null);
  const [needsName, setNeedsName] = useState(false);
  type ChangelogOpen = { payload: ChangelogPayload; serverVersion?: string };
  const pendingChangelogRef = useRef<ChangelogOpen | null>(null);
  const [changelogOpen, setChangelogOpen] = useState<ChangelogOpen | null>(null);
  const logIdCounter = useRef(0);
  const nearbyIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  // Quest system state
  const [towns, setTowns] = useState<Town[]>([]);
  const townsRef = useRef<Town[]>([]);
  const [heroQuests, setHeroQuests] = useState<HeroQuest[]>([]);
  const [currentTown, setCurrentTown] = useState<Town | null>(null);
  const [selectedNPC, setSelectedNPC] = useState<NPC | null>(null);
  const [heroSheetOpen, setHeroSheetOpen] = useState(false);
  const [heroSheetInitialTab, setHeroSheetInitialTab] = useState<HeroSheetTab>('stats');

  // NPC interaction state (server-driven via town_enter)
  const [nearestNPC, setNearestNPC] = useState<NPCData | null>(null);
  const [npcInteractionDismissed, setNpcInteractionDismissed] = useState<number | null>(null);
  /** Server signaled a town NPC visit; UI waits until the hero display reaches the NPC. */
  const [npcVisitAwaitingProximity, setNpcVisitAwaitingProximity] = useState<NPCData | null>(null);

  // Wandering NPC encounter state
  const [wanderingNPC, setWanderingNPC] = useState<NPCEncounterEvent | null>(null);
  const [npcShopCosts, setNpcShopCosts] = useState(defaultNpcShopCosts);
  // Achievements
  const [achievements, setAchievements] = useState<Achievement[]>([]);
  const prevAchievementsRef = useRef<Achievement[]>([]);

  const sheetNowMs = useUiClock(100);

  const appendLogLine = useCallback((rawMessage: string) => {
    setLogEntries((prev) =>
      appendAdventureLogMessage(prev, rawMessage, () => {
        logIdCounter.current += 1;
        return logIdCounter.current;
      }),
    );
    const parsed = parseAdventureLogLine(rawMessage);
    if (parsed.type === 'encounter') {
      setCombatLogLines([parsed.title]);
    } else if (parsed.type === 'battle') {
      setCombatLogLines((prev) => [...prev, parsed.text].slice(-5));
    }
  }, []);

  const appendCombatEvent = useCallback((evt: CombatOverlayEvent) => {
    setCombatEvents((prev) => [...prev, evt]);
  }, []);

  const clearCombatEvents = useCallback(() => {
    setCombatEvents([]);
  }, []);

  const expireCombatEvent = useCallback((id: number) => {
    setCombatEvents((prev) => prev.filter((evt) => evt.id !== id));
  }, []);

  const handleCombatAttack = useCallback((p: AttackPayload) => {
    if (p.source === 'potion') {
      return;
    }

    const defender: CombatOverlayEvent['target'] =
      p.source === 'enemy' ? 'hero' : 'enemy';
    const isBlocked = p.outcome === 'block';
    const isEvaded = p.outcome === 'dodge';
    const isStunned = p.outcome === 'stun';
    const isCrit = Boolean(p.isCrit);

    if (isStunned) {
      appendCombatEvent({
        id: Date.now() + Math.random(),
        kind: 'stunned',
        target: 'hero',
        value: 0,
        createdAt: performance.now(),
      });
    } else if (isBlocked || isEvaded) {
      appendCombatEvent({
        id: Date.now() + Math.random(),
        kind: isBlocked ? 'blocked' : 'evaded',
        target: defender,
        value: 0,
        createdAt: performance.now(),
      });
    } else {
      appendCombatEvent({
        id: Date.now() + Math.random(),
        kind: 'damage',
        target: defender,
        value: p.damage,
        isCrit,
        createdAt: performance.now(),
      });
    }

    const engine = engineRef.current;
    if (!engine) return;

    const isDotLike = p.source === 'dot' || p.source === 'summon';
    if (isDotLike) return;

    if (isStunned) {
      hapticImpact('light');
      engine.camera.shake(3, 120);
      return;
    }

    if (isBlocked || isEvaded) {
      hapticImpact('light');
      engine.camera.shake(3, 120);
      return;
    }

    if (isCrit) {
      hapticImpact('heavy');
      engine.camera.shake(8, 250);
    } else {
      hapticImpact('light');
      engine.camera.shake(4, 150);
    }
  }, [appendCombatEvent]);

  const handleEnemyRegen = useCallback((p: EnemyRegenPayload) => {
    if (p.amount <= 0) return;
    appendCombatEvent({
      id: Date.now() + Math.random(),
      kind: 'regen',
      target: 'enemy',
      value: p.amount,
      createdAt: performance.now(),
    });
  }, [appendCombatEvent]);

  const refreshEquipment = useCallback(() => {
    const telegramId = getTelegramUserId() ?? 1;
    getHeroEquipment(telegramId)
      .then((eqMap) => {
        const merged: Record<string, EquipmentItem> = {};
        for (const [slot, item] of Object.entries(eqMap)) {
          merged[slot] = {
            id: item.id,
            slot: item.slot ?? slot,
            formId: item.formId ?? '',
            name: item.name,
            rarity: (item.rarity?.toLowerCase() ?? 'common') as EquipmentItem['rarity'],
            ilvl: item.ilvl ?? 1,
            primaryStat: item.primaryStat ?? 0,
            statType: item.statType ?? 'mixed',
          };
        }
        const engine = engineRef.current;
        if (engine?.gameState.hero) {
          const prevEquip = engine.gameState.hero.equipment ?? {};
          const combined = { ...prevEquip, ...merged };
          engine.gameState.hero.equipment = combined;
        }
        setGameState((prev) => {
          if (!prev.hero) return prev;
          const prevEquip = prev.hero.equipment ?? {};
          return { ...prev, hero: { ...prev.hero, equipment: { ...prevEquip, ...merged } } };
        });
      })
      .catch(() => console.warn('[App] Could not fetch equipment'));
  }, []);

  const refreshHeroQuests = useCallback(() => {
    const telegramId = getTelegramUserId() ?? 1;
    getHeroQuests(telegramId)
      .then((q) => setHeroQuests(q))
      .catch(() => console.warn('[App] Could not refresh hero quests'));
  }, []);

  // Initialize engine and WebSocket
  useEffect(() => {
    const container = canvasRef.current;
    if (!container) return;

    // ---- Game Engine ----
    const engine = new GameEngine();
    engineRef.current = engine;

    engine.onStateChange((state) => {
      setGameState(state);
    });

    engine.init(container).then(async () => {
      let shouldOpenWS = false;
      try {
        const telegramId = getTelegramUserId() ?? 1;
        const initRes = await initHero(telegramId);
        setNpcShopCosts(npcShopCostsFromInit(initRes));

        if (initRes.showChangelog && initRes.changelog) {
          const bundle: ChangelogOpen = {
            payload: initRes.changelog,
            serverVersion: initRes.serverVersion,
          };
          if (initRes.needsName) {
            pendingChangelogRef.current = bundle;
          } else {
            setChangelogOpen(bundle);
          }
        }

        // Gate game start behind name entry — no hero row until POST /hero/name
        if (initRes.needsName) {
          setNeedsName(true);
          if (initRes.hero) {
            const heroState = heroResponseToState(initRes.hero);
            engine.initFromServer(heroState, initRes.hero.state);
          }
          getTowns()
            .then(async (t) => {
              setTowns(t);
              townsRef.current = t;
              const townNPCMap = new Map<number, NPC[]>();
              const townBuildingMap = new Map<number, BuildingData[]>();
              try {
                const [npcResults, buildingResults] = await Promise.all([
                  Promise.allSettled(t.map((town) => getTownNPCs(town.id).then((npcs) => ({ townId: town.id, npcs })))),
                  Promise.allSettled(t.map((town) => getTownBuildings(town.id).then((b) => ({ townId: town.id, buildings: b })))),
                ]);
                for (const result of npcResults) {
                  if (result.status === 'fulfilled') townNPCMap.set(result.value.townId, result.value.npcs);
                }
                for (const result of buildingResults) {
                  if (result.status === 'fulfilled') townBuildingMap.set(result.value.townId, result.value.buildings);
                }
              } catch {
                /* ignore */
              }
              const townDataList = t.map((town) => townToTownData(town, townNPCMap.get(town.id), townBuildingMap.get(town.id)));
              engine.setTowns(townDataList);
              const allNPCs: NPCData[] = [];
              for (const td of townDataList) {
                if (td.npcs) allNPCs.push(...td.npcs);
              }
              engine.setNPCs(allNPCs);
            })
            .catch(() => console.warn('[App] Could not fetch towns (name gate)'));
          console.info('[App] Hero needs name, showing name entry screen');
          return;
        }

        if (!initRes.hero) {
          console.error('[App] init: missing hero without needsName');
          setConnectionError('Invalid server response.');
          return;
        }

        shouldOpenWS = true;
        const heroState = heroResponseToState(initRes.hero);
        engine.initFromServer(heroState, initRes.hero.state);
        engine.setHeroName(initRes.hero.name);
        console.info('[App] Loaded hero from server, id=', initRes.hero.id);

        if (initRes.offlineReport && initRes.offlineReport.monstersKilled > 0) {
          const r = initRes.offlineReport;
          console.info(`[Offline] ${r.message} Killed ${r.monstersKilled} monsters, +${r.xpGained} XP, +${r.goldGained} gold, +${r.levelsGained} levels`);
          setOfflineReport(r);
        }

        // Fetch towns, then their NPCs, and pass everything to the engine
        getTowns()
          .then(async (t) => {
            setTowns(t);
            townsRef.current = t;
            const townNPCMap = new Map<number, NPC[]>();
            const townBuildingMap = new Map<number, BuildingData[]>();
            try {
              const [npcResults, buildingResults] = await Promise.all([
                Promise.allSettled(t.map((town) => getTownNPCs(town.id).then((npcs) => ({ townId: town.id, npcs })))),
                Promise.allSettled(t.map((town) => getTownBuildings(town.id).then((b) => ({ townId: town.id, buildings: b })))),
              ]);
              for (const result of npcResults) {
                if (result.status === 'fulfilled') townNPCMap.set(result.value.townId, result.value.npcs);
              }
              for (const result of buildingResults) {
                if (result.status === 'fulfilled') townBuildingMap.set(result.value.townId, result.value.buildings);
              }
            } catch {
              console.warn('[App] Error fetching town NPCs/buildings');
            }
            const townDataList = t.map((town) => townToTownData(town, townNPCMap.get(town.id), townBuildingMap.get(town.id)));
            engine.setTowns(townDataList);
            const allNPCs: NPCData[] = [];
            for (const td of townDataList) {
              if (td.npcs) allNPCs.push(...td.npcs);
            }
            engine.setNPCs(allNPCs);
          })
          .catch(() => console.warn('[App] Could not fetch towns'));

        getHeroQuests(telegramId)
          .then((q) => setHeroQuests(q))
          .catch(() => console.warn('[App] Could not fetch hero quests'));
        refreshEquipment();

        getAchievements(telegramId)
          .then((a) => { prevAchievementsRef.current = a; setAchievements(a); })
          .catch(() => console.warn('[App] Could not fetch achievements'));

        // Poll nearby heroes every 5 seconds
        const nearbyInterval = setInterval(() => {
          getNearbyHeroes(telegramId)
            .then((heroes) => engine.setNearbyHeroes(heroes.map((h) => ({
              id: h.id,
              name: h.name,
              level: h.level,
              positionX: h.positionX,
              positionY: h.positionY,
            }))))
            .catch(() => {});
        }, 5000);
        getNearbyHeroes(telegramId)
          .then((heroes) => engine.setNearbyHeroes(heroes.map((h) => ({
            id: h.id,
            name: h.name,
            level: h.level,
            positionX: h.positionX,
            positionY: h.positionY,
          }))))
          .catch(() => {});
        nearbyIntervalRef.current = nearbyInterval;

        // Fetch adventure log (same source as server DB; response shape { log: [...] })
        try {
          const serverLog = await getAdventureLog(telegramId, 50);
          const { entries, maxId } = adventureEntriesFromServerLog(serverLog);
          logIdCounter.current = Math.max(logIdCounter.current, maxId);
          setLogEntries(entries);
        } catch {
          console.warn('[App] Could not fetch adventure log');
        }
      } catch (err) {
        console.error('[App] Backend not available. Game requires a server connection:', err);
        setConnectionError('Cannot reach game server. Please try again later.');
        return;
      }
      engine.start();
      if (shouldOpenWS) {
        ws.connect();
      }
    }).catch((err) => {
      console.error('[App] Failed to initialize game engine:', err);
    });

    // ---- WebSocket ----
    const ws = new GameWebSocket();
    wsRef.current = ws;

    // Pass telegram ID so the WS connection identifies the correct hero.
    const wsTelegramId = getTelegramUserId() ?? 1;
    ws.setTelegramId(wsTelegramId);

    ws.onConnectionState((connState) => {
      const connected = connState === 'connected';
      setWsConnected(connected);
      if (connected) setWsEverConnected(true);
    });

    // Wire WS handler -- routes server messages to engine + UI callbacks
    wireWSHandler(ws, engine, {
      onCombatStart: () => {
        setCombatLogLines([]);
        clearCombatEvents();
      },
      onCombatAttack: handleCombatAttack,
      onEnemyRegen: handleEnemyRegen,

      onHeroStateReceived: (payload) => {
        // Convert raw payload to HeroResponse shape and apply
        const res = payload as unknown as HeroResponse;
        const heroState = heroResponseToState(res);
        engine.applyHeroState(heroState);
      },

      onCombatEnd: (p) => {
        setCombatLogLines([]);
        clearCombatEvents();
        const loot = buildLootFromCombatEnd(p);
        engine.applyLoot(loot);
        hapticNotification('success');

        const parts: string[] = [];
        if (p.xpGained > 0) parts.push(`+${p.xpGained} XP`);
        if (p.goldGained > 0) parts.push(`+${p.goldGained} gold`);
        const equipDrop = (p.loot ?? []).find(
          (l) => l.itemType !== 'gold' && l.itemType !== 'potion',
        );
        if (equipDrop?.name) parts.push(`found ${equipDrop.name}`);
        // Victory line comes from server adventure log (Defeated …) + WS adventure_log_line

        if (p.leveledUp && p.newLevel) {
          setToast({ message: t(tr.levelUp, { level: p.newLevel }), color: '#ffd700' });
          hapticNotification('success');
        }

        // Refresh quests, equipment, achievements after combat
        const tid = getTelegramUserId() ?? 1;
        getHeroQuests(tid).then((q) => setHeroQuests(q)).catch(() => {});
        refreshEquipment();
        getAchievements(tid).then((a) => {
          const prevIds = new Set(prevAchievementsRef.current.filter((x) => x.unlocked).map((x) => x.id));
          const newlyUnlocked = a.filter((x) => x.unlocked && !prevIds.has(x.id));
          for (const ach of newlyUnlocked) {
            setToast({ message: `Achievement unlocked: ${ach.title}!`, color: '#ffd700' });
            hapticNotification('success');
          }
          prevAchievementsRef.current = a;
          setAchievements(a);
        }).catch(() => {});
      },

      onHeroDied: (_p) => {
        hapticNotification('error');
        // Death line comes from server log + WS
      },

      onHeroRevived: () => {
        setCombatLogLines([]);
        clearCombatEvents();
        setToast({ message: tr.heroRevived, color: '#44cc44' });
        // "Hero revived" comes from server log + WS
      },

      onBuffApplied: (_p) => {
        // Buff activation comes from server log + WS
      },

      onTownEnter: (p) => {
        const town = townsRef.current.find((t) => t.id === p.townId) ?? null;
        setCurrentTown(town);
        setToast({ message: t(tr.entering, { townName: p.townName }), color: '#daa520' });
        appendLogLine(`Entered ${p.townName}`);
        setNearestNPC(null);
        setNpcVisitAwaitingProximity(null);
        setSelectedNPC(null);
        setNpcInteractionDismissed(null);
      },

      onAdventureLogLine: (p) => {
        appendLogLine(p.message);
      },

      onTownNPCVisit: (p) => {
        setNearestNPC(null);
        setNpcInteractionDismissed(null);
        setNpcVisitAwaitingProximity({
          id: p.npcId,
          name: p.name,
          type: p.type as NPCData['type'],
          worldX: p.worldX ?? 0,
          worldY: p.worldY ?? 0,
        });
      },

      onTownExit: () => {
        setCurrentTown(null);
        setNearestNPC(null);
        setNpcVisitAwaitingProximity(null);
      },

      onNPCEncounter: (p) => {
        const npcEvent: NPCEncounterEvent = {
          type: 'npc_event',
          npcName: p.npcName,
          message: `${p.npcName} approaches!`,
          cost: p.cost,
        };
        setWanderingNPC(npcEvent);
      },

      onNPCEncounterEnd: (p) => {
        if (p.reason === 'timeout') {
          appendLogLine('Wandering merchant moved on');
        }
        setWanderingNPC(null);
      },

      onLevelUp: (p) => {
        setToast({ message: t(tr.levelUp, { level: p.newLevel }), color: '#ffd700' });
        hapticNotification('success');
        // Level-up lines come from server log + WS
      },

      onEquipmentChange: (p) => {
        setToast({ message: t(tr.newEquipment, { slot: p.slot, itemName: p.item.name }), color: '#cc88ff' });
        // Equipment line comes from server log + WS
        refreshEquipment();
      },

      onPotionCollected: (p) => {
        setToast({ message: t(tr.potionsCollected, { count: p.count }), color: '#44cc44' });
      },

      onQuestProgress: (p) => {
        setHeroQuests((prev) =>
          prev.map((hq) =>
            hq.questId === p.questId
              ? { ...hq, progress: p.current }
              : hq,
          ),
        );
        if (p.title) {
          setToast({
            message: t(tr.questProgress, { title: p.title, current: p.current, target: p.target }),
            color: '#44aaff',
          });
        }
      },

      onQuestComplete: (p) => {
        setHeroQuests((prev) =>
          prev.map((hq) =>
            hq.questId === p.questId
              ? { ...hq, status: 'completed' as const }
              : hq,
          ),
        );
        setToast({ message: t(tr.questCompleted, { title: p.title }), color: '#ffd700' });
        hapticNotification('success');
      },

      onError: (p) => {
        setToast({ message: p.message, color: '#ff4444' });
      },

      onMerchantLoot: (p) => {
        const loot = buildMerchantLootDrop(p);
        const eng = engineRef.current;
        if (loot && eng) {
          eng.applyLoot(loot);
        }
        hapticNotification('success');
        refreshEquipment();
      },
    });

    // ---- Telegram Theme Listener ----
    const unsubTheme = onThemeChanged();

    // ---- Cleanup ----
    return () => {
      engine.destroy();
      ws.disconnect();
      unsubTheme();
      if (nearbyIntervalRef.current) {
        clearInterval(nearbyIntervalRef.current);
        nearbyIntervalRef.current = null;
      }
      engineRef.current = null;
      wsRef.current = null;
    };
  }, []);

  // Open trader / quest / healer panel only after the hero sprite has reached the NPC (not on town_enter).
  useEffect(() => {
    if (!npcVisitAwaitingProximity) return;
    const pending = npcVisitAwaitingProximity;
    const proximityR = 0.55;
    const proximityR2 = proximityR * proximityR;
    const timeoutMs = 5000;
    const started = performance.now();
    let raf = 0;

    const step = () => {
      const eng = engineRef.current;
      let closeEnough = false;
      if (eng) {
        const { x, y } = eng.getHeroDisplayWorldPosition();
        const dx = x - pending.worldX;
        const dy = y - pending.worldY;
        closeEnough = dx * dx + dy * dy <= proximityR2;
      }
      if (closeEnough || performance.now() - started > timeoutMs) {
        const role =
          pending.type === 'merchant'
            ? tr.shopLabel
            : pending.type === 'healer'
              ? tr.healerLabel
              : tr.questLabel;
        setToast({ message: `${role}: ${pending.name}`, color: '#c9a227' });
        setNearestNPC(pending);
        setNpcVisitAwaitingProximity(null);
        return;
      }
      raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [npcVisitAwaitingProximity, tr]);

  // Restore per-hero buff button cooldowns
  useEffect(() => {
    const id = gameState.hero?.id;
    if (id == null) return;
    const loaded = loadCooldownsFromStorage(id);
    if (Object.keys(loaded).length === 0) return;
    setBuffCooldownEndsAt((prev) => ({ ...loaded, ...prev }));
  }, [gameState.hero?.id]);

  // ---- Handlers ----

  const handleBuffActivate = useCallback((type: BuffType) => {
    const engine = engineRef.current;
    const ws = wsRef.current;
    const hero = engine?.gameState.hero;
    const now = Date.now();

    if (engine?.gameState.phase === GamePhase.Dead) {
      return;
    }

    // Check per-buff charge quota
    if (hero) {
      const charge = hero.buffCharges?.[type];
      if (charge != null && charge.remaining <= 0) {
        const label = type.charAt(0).toUpperCase() + type.slice(1).replace('_', ' ');
        setToast({ message: t(tr.noChargesLeft, { label }), color: '#ff8844' });
        return;
      }
    }

    // Optimistic update
    if (engine && hero) {
      const durationMs = BUFF_DURATION_MS[type];
      const optimisticBuff: ActiveBuff = {
        type,
        remainingMs: durationMs,
        durationMs,
        cooldownMs: BUFF_COOLDOWN_MS[type],
        cooldownRemainingMs: 0,
        expiresAtMs: now + durationMs,
      };

      const alreadyActive = hero.activeBuffs.some(
        (b) => b.type === type && (b.expiresAtMs ?? 0) > now,
      );
      const updatedBuffs = alreadyActive
        ? hero.activeBuffs.map((b) => (b.type === type ? optimisticBuff : b))
        : [...hero.activeBuffs, optimisticBuff];

      let { damage, defense, attackSpeed, hp, maxHp } = hero;

      if (!alreadyActive) {
        switch (type) {
          case BuffType.Rage:
            damage = Math.round(damage * 2);
            break;
          case BuffType.PowerPotion:
            damage = Math.round(damage * 2.5);
            break;
          case BuffType.WarCry:
            attackSpeed = Math.round(attackSpeed * 2 * 100) / 100;
            break;
          case BuffType.Heal:
            hp = Math.min(maxHp, hp + Math.round(maxHp * 0.5));
            break;
        }
      }

      engine.patchHeroCombat({
        damage,
        defense,
        attackSpeed,
        activeBuffs: updatedBuffs,
      });

      if (type === BuffType.Heal && hp !== hero.hp) {
        engine.patchHeroHp(hp);
      }

      setBuffCooldownEndsAt((prev) => {
        const next = { ...prev, [type]: now + BUFF_COOLDOWN_MS[type] };
        if (hero.id != null) saveCooldownsToStorage(hero.id, next);
        return next;
      });

      // Optimistic decrement of per-buff charge (server always consumes via ConsumeBuffCharge)
      const currentCharge = hero.buffCharges?.[type];
      if (currentCharge != null && currentCharge.remaining > 0) {
        const updatedCharges: Partial<Record<BuffType, BuffChargeState>> = {
          ...hero.buffCharges,
          [type]: { ...currentCharge, remaining: currentCharge.remaining - 1 },
        };
        engine.patchHeroBuffQuota({
          buffFreeChargesRemaining: hero.buffFreeChargesRemaining,
          buffQuotaPeriodEnd: hero.buffQuotaPeriodEnd,
          buffCharges: updatedCharges,
        });
      }

      hapticImpact('medium');
      // Server logs buff activation + sends adventure_log_line
    }

    // Send command to server via WebSocket
    if (ws) {
      sendActivateBuff(ws, type);
    }
  }, []);

  const handleRevive = useCallback(() => {
    const ws = wsRef.current;
    if (ws && ws.getState() === 'connected') {
      sendRevive(ws);
    } else {
      // Fallback to REST if WS not connected
      const telegramId = getTelegramUserId() ?? 1;
      requestRevive(telegramId)
        .then((hero) => {
          const engine = engineRef.current;
          if (engine) {
            const state = heroResponseToState(hero);
            engine.applyHeroRevived(state.hp);
            engine.applyHeroState(state);
          }
        })
        .catch((err) => {
          console.warn('[App] Revive failed:', err);
        });
    }
  }, []);

  const handleQuestClaim = useCallback(
    (heroQuestId: number) => {
      const telegramId = getTelegramUserId() ?? 1;
      claimQuest(heroQuestId, telegramId)
        .then((hero) => {
          const merged = heroResponseToState(hero);
          const engine = engineRef.current;
          if (engine) {
            const pos = engine.gameState.hero?.position;
            if (pos) merged.position = pos;
            engine.applyHeroState(merged);
          }
          setToast({ message: tr.questRewardsClaimed, color: '#ffd700' });
          hapticNotification('success');
          refreshHeroQuests();
        })
        .catch((err) => {
          console.warn('[App] Failed to claim quest:', err);
          setToast({ message: tr.failedToClaimRewards, color: '#ff4444' });
        });
    },
    [refreshHeroQuests],
  );

  const handleQuestAbandon = useCallback(
    (heroQuestId: number) => {
      const telegramId = getTelegramUserId() ?? 1;
      abandonQuest(heroQuestId, telegramId)
        .then(() => {
          setToast({ message: tr.questAbandoned, color: '#ff8844' });
          refreshHeroQuests();
        })
        .catch((err) => {
          console.warn('[App] Failed to abandon quest:', err);
          setToast({ message: tr.failedToAbandonQuest, color: '#ff4444' });
        });
    },
    [refreshHeroQuests],
  );

  const handleNPCHeroUpdated = useCallback((hero: HeroResponse) => {
    const merged = heroResponseToState(hero);
    const engine = engineRef.current;
    if (engine) {
      const pos = engine.gameState.hero?.position;
      if (pos) merged.position = pos;
      engine.applyHeroState(merged);
    }
  }, []);

  const handleNameSet = useCallback((hero: HeroResponse) => {
    setNeedsName(false);
    const engine = engineRef.current;
    if (engine) {
      const heroState = heroResponseToState(hero);
      engine.initFromServer(heroState, hero.state);
      engine.setHeroName(hero.name);
      engine.start();
      wsRef.current?.connect();

      const telegramId = getTelegramUserId() ?? 1;
      getTowns()
        .then(async (t) => {
          setTowns(t);
          const townNPCMap = new Map<number, NPC[]>();
          const townBuildingMap = new Map<number, BuildingData[]>();
          try {
            const [npcResults, buildingResults] = await Promise.all([
              Promise.allSettled(t.map((town) => getTownNPCs(town.id).then((npcs) => ({ townId: town.id, npcs })))),
              Promise.allSettled(t.map((town) => getTownBuildings(town.id).then((b) => ({ townId: town.id, buildings: b })))),
            ]);
            for (const result of npcResults) {
              if (result.status === 'fulfilled') townNPCMap.set(result.value.townId, result.value.npcs);
            }
            for (const result of buildingResults) {
              if (result.status === 'fulfilled') townBuildingMap.set(result.value.townId, result.value.buildings);
            }
          } catch { /* ignore */ }
          const townDataList = t.map((town) => townToTownData(town, townNPCMap.get(town.id), townBuildingMap.get(town.id)));
          engine.setTowns(townDataList);
          const allNPCs: NPCData[] = [];
          for (const td of townDataList) {
            if (td.npcs) allNPCs.push(...td.npcs);
          }
          engine.setNPCs(allNPCs);
        })
        .catch(() => console.warn('[App] Could not fetch towns'));
      getHeroQuests(telegramId)
        .then((q) => setHeroQuests(q))
        .catch(() => console.warn('[App] Could not fetch hero quests'));
      getAdventureLog(telegramId, 50)
        .then((serverLog) => {
          const { entries, maxId } = adventureEntriesFromServerLog(serverLog);
          logIdCounter.current = Math.max(logIdCounter.current, maxId);
          setLogEntries(entries);
        })
        .catch(() => console.warn('[App] Could not fetch adventure log'));
      getAchievements(telegramId)
        .then((a) => { prevAchievementsRef.current = a; setAchievements(a); })
        .catch(() => {});

      if (pendingChangelogRef.current) {
        setChangelogOpen(pendingChangelogRef.current);
        pendingChangelogRef.current = null;
      }
    }
  }, []);

  const handleDismissChangelog = useCallback(() => {
    setChangelogOpen(null);
    const telegramId = getTelegramUserId() ?? 1;
    ackChangelog(telegramId).catch(() => console.warn('[App] changelog ack failed'));
  }, []);

  const handleUsePotion = useCallback(() => {
    const ws = wsRef.current;
    const hero = engineRef.current?.gameState.hero;
    if (!hero || (hero.potions ?? 0) <= 0) return;

    // Send via WS
    if (ws) {
      sendUsePotion(ws);
    }
    hero.potions--

    hapticImpact('medium');
    // Server logs potion use + sends adventure_log_line
  }, []);

  // ---- NPC Interaction Handlers ----

  const handleNPCViewQuests = useCallback((npc: NPCData) => {
    const matchedNPC: NPC = {
      id: npc.id,
      townId: 0,
      name: npc.name,
      type: npc.type,
      offsetX: 0,
      offsetY: 0,
    };
    setSelectedNPC(matchedNPC);
    setNpcInteractionDismissed(npc.id);
  }, []);

  const handleNPCBuyPotion = useCallback((_npc: NPCData) => {
    const telegramId = getTelegramUserId() ?? 1;
    buyPotion(telegramId)
      .then((hero) => {
        hapticImpact('medium');
        setToast({ message: t(tr.boughtPotion, { cost: npcShopCosts.potionCost }), color: '#88dd88' });
        handleNPCHeroUpdated(hero);
        // Server logs purchase + WS
      })
      .catch((err) => {
        console.warn('[App] Failed to buy potion:', err);
        if (err instanceof ApiError) {
          try {
            const j = JSON.parse(err.body) as { error?: string };
            setToast({ message: j.error ?? tr.failedToBuyPotion, color: '#ff4444' });
          } catch {
            setToast({ message: tr.failedToBuyPotion, color: '#ff4444' });
          }
        }
      });
  }, [handleNPCHeroUpdated, npcShopCosts.potionCost, tr]);

  const handleNPCHeal = useCallback((npc: NPCData) => {
    const telegramId = getTelegramUserId() ?? 1;
    healAtNPC(telegramId, npc.id)
      .then((hero) => {
        hapticImpact('medium');
        setToast({ message: tr.healedToFull, color: '#44cc44' });
        handleNPCHeroUpdated(hero);
        // Server logs heal + WS
      })
      .catch((err) => {
        console.warn('[App] Failed to heal:', err);
        if (err instanceof ApiError) {
          try {
            const j = JSON.parse(err.body) as { error?: string };
            setToast({ message: j.error ?? tr.failedToHeal, color: '#ff4444' });
          } catch {
            setToast({ message: tr.failedToHeal, color: '#ff4444' });
          }
        }
      });
  }, [handleNPCHeroUpdated]);

  const handleNPCInteractionDismiss = useCallback(() => {
    if (nearestNPC) {
      setNpcInteractionDismissed(nearestNPC.id);
    }
  }, [nearestNPC]);

  // ---- Wandering NPC Encounter Handlers (via WS) ----

  const handleWanderingAccept = useCallback(() => {
    const ws = wsRef.current;
    if (ws) {
      sendNPCAlmsAccept(ws);
    }
    setWanderingNPC(null);
    // Alms outcome is logged on server + WS when trade completes
  }, []);

  const handleWanderingDecline = useCallback(() => {
    const ws = wsRef.current;
    if (ws) {
      sendNPCAlmsDecline(ws);
    }
    setWanderingNPC(null);
    appendLogLine('Declined wandering merchant');
  }, [appendLogLine]);

  // Show NPC interaction when near an NPC and not dismissed
  const showNPCInteraction =
    nearestNPC !== null &&
    npcInteractionDismissed !== nearestNPC.id &&
    (gameState.phase === GamePhase.Walking || gameState.phase === GamePhase.InTown) &&
    !selectedNPC;

  const completedQuestCount = useMemo(
    () =>
      heroQuests.filter((q) => q.status === 'completed').length,
    [heroQuests],
  );

  const dismissToast = useCallback(() => setToast(null), []);

  const questClaimDisabled =
    gameState.phase === GamePhase.Dead || (gameState.hero?.hp ?? 0) <= 0;

  return (
    <I18nContext.Provider value={{ tr: translations, locale, setLocale: handleSetLocale }}>
    <div style={appStyle}>
      {/* PixiJS Canvas */}
      <div ref={canvasRef} style={canvasContainerStyle} />

      {/* React UI Overlay */}
      <HUD
        gameState={gameState}
        onBuffActivate={handleBuffActivate}
        buffCooldownEndsAt={buffCooldownEndsAt}
        onUsePotion={handleUsePotion}
        onHeroUpdated={handleNPCHeroUpdated}
        completedQuestCount={completedQuestCount}
        onOpenHeroSheet={() => {
          setHeroSheetInitialTab('stats');
          setHeroSheetOpen(true);
        }}
      />

      {gameState.hero && (
        <HeroSheetModal
          open={heroSheetOpen}
          onClose={() => setHeroSheetOpen(false)}
          initialTab={heroSheetInitialTab}
          hero={gameState.hero}
          nowMs={sheetNowMs}
          equipment={gameState.hero.equipment ?? {}}
          logEntries={logEntries}
          quests={heroQuests}
          onQuestClaim={handleQuestClaim}
          onQuestAbandon={handleQuestAbandon}
          questClaimDisabled={questClaimDisabled}
        />
      )}

      {/* Combat overlay */}
      <CombatOverlay events={combatEvents} onExpire={expireCombatEvent} />

      <CombatLogPanel
        visible={
          gameState.phase === GamePhase.Fighting || gameState.phase === GamePhase.Dead
        }
        lines={combatLogLines}
        anchor={gameState.enemyOnScreenRight !== false ? 'left' : 'right'}
      />

      {/* Name Entry Screen */}
      {needsName && <NameEntryScreen onNameSet={handleNameSet} />}

      {changelogOpen && (
        <ChangelogModal
          title={changelogOpen.payload.title}
          items={changelogOpen.payload.items}
          serverVersion={changelogOpen.serverVersion}
          onDismiss={handleDismissChangelog}
        />
      )}

      {/* Death Screen */}
      <DeathScreen
        visible={gameState.phase === GamePhase.Dead}
        onRevive={handleRevive}
        subscriptionUnlimited={!!gameState.hero?.subscriptionActive}
        revivesRemaining={Math.max(0, 2 - (gameState.hero?.reviveCount ?? 0))}
      />

      {/* Toast Notification */}
      {toast && (
        <GameToast
          message={toast.message}
          color={toast.color}
          onDone={dismissToast}
        />
      )}

      {/* Town Name Indicator */}
      {currentTown && (
        <div
          style={{
            position: 'absolute',
            top: 48,
            left: '50%',
            transform: 'translateX(-50%)',
            padding: '3px 14px',
            borderRadius: 6,
            backgroundColor: 'rgba(218, 165, 32, 0.15)',
            border: '1px solid rgba(218, 165, 32, 0.3)',
            color: '#daa520',
            fontSize: 11,
            fontWeight: 600,
            zIndex: 30,
            pointerEvents: 'none',
            textShadow: '0 0 6px rgba(218, 165, 32, 0.3)',
          }}
        >
          {currentTown.name}
        </div>
      )}

      {/* NPC Proximity Interaction */}
      {showNPCInteraction && nearestNPC && (
        <NPCInteraction
          npc={nearestNPC}
          heroGold={gameState.hero?.gold ?? 0}
          potionCost={npcShopCosts.potionCost}
          healCost={npcShopCosts.healCost}
          onViewQuests={handleNPCViewQuests}
          onBuyPotion={handleNPCBuyPotion}
          onHeal={handleNPCHeal}
          onDismiss={handleNPCInteractionDismiss}
        />
      )}

      {/* NPC Dialog */}
      {selectedNPC && (
        <NPCDialog
          npc={selectedNPC}
          heroQuests={heroQuests}
          heroGold={gameState.hero?.gold ?? 0}
          potionCost={npcShopCosts.potionCost}
          healCost={npcShopCosts.healCost}
          onClose={() => setSelectedNPC(null)}
          onQuestsChanged={refreshHeroQuests}
          onHeroUpdated={handleNPCHeroUpdated}
          onToast={(message, color) => setToast({ message, color })}
          questClaimDisabled={questClaimDisabled}
        />
      )}

      {/* Wandering NPC Encounter Popup */}
      {wanderingNPC && (
        <WanderingNPCPopup
          npcName={wanderingNPC.npcName}
          message={wanderingNPC.message}
          cost={wanderingNPC.cost}
          heroGold={gameState.hero?.gold ?? 0}
          onAccept={handleWanderingAccept}
          onDecline={handleWanderingDecline}
        />
      )}

      {/* Minimap (top-right) */}
      {gameState.hero && (
        <Minimap
          heroX={gameState.hero.position.x}
          heroY={gameState.hero.position.y}
          towns={towns}
          routeWaypoints={gameState.routeWaypoints}
        />
      )}

      {/* Achievements Panel */}
      {gameState.hero && <AchievementsPanel achievements={achievements} />}

      {/* Offline Report Overlay */}
      {offlineReport && (
        <OfflineReport
          monstersKilled={offlineReport.monstersKilled}
          xpGained={offlineReport.xpGained}
          goldGained={offlineReport.goldGained}
          levelsGained={offlineReport.levelsGained}
          onDismiss={() => setOfflineReport(null)}
        />
      )}

      {/* Connection Status Banner */}
      {wsEverConnected && !wsConnected && (
        <div style={connectionBanner}>
          Reconnecting...
        </div>
      )}

      {/* Fatal connection error */}
      {connectionError && (
        <div style={{
          position: 'absolute', inset: 0,
          display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
          backgroundColor: 'rgba(0,0,0,0.85)', color: '#fff', zIndex: 1000,
          fontFamily: 'system-ui, sans-serif', textAlign: 'center', padding: 24,
        }}>
          <div style={{ fontSize: 48, marginBottom: 16 }}>&#x26A0;</div>
          <div style={{ fontSize: 18, fontWeight: 700, marginBottom: 8 }}>Connection Failed</div>
          <div style={{ fontSize: 14, opacity: 0.8, marginBottom: 24 }}>{connectionError}</div>
          <button
            onClick={() => window.location.reload()}
            style={{
              padding: '10px 28px', fontSize: 14, fontWeight: 600,
              border: 'none', borderRadius: 8, cursor: 'pointer',
              backgroundColor: '#4a90d9', color: '#fff',
            }}
          >
            Retry
          </button>
        </div>
      )}
    </div>
    </I18nContext.Provider>
  );
}
