import React, { useEffect, useMemo, useRef, useState, type FormEvent, type ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import type {
  BglandsLauncherApi,
  CommunityPlayerRanking,
  LauncherModuleId,
  LauncherPhase,
  LauncherSettings,
  LauncherSnapshot,
} from "../shared/contracts.ts";

type PageId = "home" | "play" | "news" | "servers" | "clans" | "rankings" | "shop" | "modules" | "rules" | "settings";
type IconName = "home" | "play" | "news" | "server" | "clan" | "rank" | "shop" | "modules" | "rules" | "settings" | "user" | "copy" | "shield" | "close" | "check" | "download" | "repair" | "log" | "arrow" | "wallet" | "clock" | "swords" | "map" | "memory" | "ping";

const NAVIGATION: Array<{ id: PageId; label: string; icon: IconName }> = [
  { id: "home", label: "Accueil", icon: "home" },
  { id: "play", label: "Jouer", icon: "play" },
  { id: "news", label: "Actualités", icon: "news" },
  { id: "servers", label: "Serveurs", icon: "server" },
  { id: "clans", label: "Clans", icon: "clan" },
  { id: "rankings", label: "Classements", icon: "rank" },
  { id: "shop", label: "Boutique", icon: "shop" },
  { id: "modules", label: "Modules", icon: "modules" },
  { id: "rules", label: "Règlement", icon: "rules" },
  { id: "settings", label: "Paramètres", icon: "settings" },
];

const ICONS: Record<IconName, ReactNode> = {
  home: <><path d="m3 11 9-8 9 8"/><path d="M5 10v10h14V10M9 20v-6h6v6"/></>,
  play: <path d="m8 5 11 7-11 7V5Z"/>,
  news: <><path d="M5 4h14v16H5z"/><path d="M8 8h8M8 12h8M8 16h5"/></>,
  server: <><rect x="4" y="4" width="16" height="6" rx="2"/><rect x="4" y="14" width="16" height="6" rx="2"/><path d="M8 7h.01M8 17h.01M12 7h5M12 17h5"/></>,
  clan: <><path d="M12 3 4 7v5c0 5 3.4 8 8 10 4.6-2 8-5 8-10V7l-8-4Z"/><path d="m8 13 2.5 2.5L16 10"/></>,
  rank: <><path d="M8 21h8M12 17v4M7 4h10v4a5 5 0 0 1-10 0V4Z"/><path d="M7 6H4v2a4 4 0 0 0 4 4M17 6h3v2a4 4 0 0 1-4 4"/></>,
  shop: <><path d="M4 9h16l-1 11H5L4 9Z"/><path d="M8 9V7a4 4 0 0 1 8 0v2"/></>,
  modules: <><rect x="3" y="3" width="7" height="7" rx="2"/><rect x="14" y="3" width="7" height="7" rx="2"/><rect x="3" y="14" width="7" height="7" rx="2"/><path d="M17.5 14v7M14 17.5h7"/></>,
  rules: <><path d="M6 3h12v18H6z"/><path d="M9 7h6M9 11h6M9 15h4"/></>,
  settings: <><circle cx="12" cy="12" r="3"/><path d="M20 13.5v-3l-2-.7-.7-1.7.9-1.9-2.1-2.1-1.9.9-1.7-.7-.7-2h-3l-.7 2-1.7.7-1.9-.9-2.1 2.1.9 1.9-.7 1.7-2 .7v3l2 .7.7 1.7-.9 1.9 2.1 2.1 1.9-.9 1.7.7.7 2h3l.7-2 1.7-.7 1.9.9 2.1-2.1-.9-1.9.7-1.7 2-.7Z"/></>,
  user: <><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></>,
  copy: <><rect x="8" y="8" width="11" height="11" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></>,
  shield: <><path d="M12 3 4 6v6c0 4.8 3 8 8 10 5-2 8-5.2 8-10V6l-8-3Z"/><path d="m8 12 2.5 2.5L16 9"/></>,
  close: <path d="m6 6 12 12M18 6 6 18"/>, check: <path d="m5 12 4 4L19 6"/>,
  download: <><path d="M12 3v12M7 10l5 5 5-5"/><path d="M4 20h16"/></>,
  repair: <><path d="M20 7v5h-5M4 17v-5h5"/><path d="M7 7a7 7 0 0 1 12 3l1 2M4 12l1 2a7 7 0 0 0 12 3"/></>,
  log: <><path d="M6 3h12v18H6z"/><path d="M9 8h6M9 12h6M9 16h4"/></>, arrow: <path d="m9 18 6-6-6-6"/>,
  wallet: <><path d="M4 6h14a2 2 0 0 1 2 2v10H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h12"/><path d="M15 11h5v4h-5a2 2 0 0 1 0-4Z"/></>,
  clock: <><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>,
  swords: <><path d="m4 20 5-5M3 4l7 7-3 3-7-7 3-3ZM20 4l-7 7 3 3 7-7-3-3ZM20 20l-5-5"/></>,
  map: <><path d="m3 6 6-3 6 3 6-3v15l-6 3-6-3-6 3V6Z"/><path d="M9 3v15M15 6v15"/></>,
  memory: <><rect x="5" y="5" width="14" height="14" rx="2"/><path d="M9 9h6v6H9zM9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M19 9h3M2 15h3M19 15h3"/></>,
  ping: <><path d="M5 15a10 10 0 0 1 14 0M8 18a6 6 0 0 1 8 0"/><circle cx="12" cy="21" r="1"/></>,
};

function Icon({ name, size = 20 }: { name: IconName; size?: number }) {
  return <svg className="icon" width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">{ICONS[name]}</svg>;
}

const MODULES: Array<{ id: LauncherModuleId; title: string; description: string; icon: IconName; tag: string }> = [
  { id: "keystrokes", title: "Keystrokes", description: "Affiche ZQSD, saut et l’état instantané des touches.", icon: "modules", tag: "HUD" },
  { id: "cps", title: "Compteur CPS", description: "Mesure séparément les clics gauche et droit sur une seconde.", icon: "swords", tag: "PVP" },
  { id: "ping", title: "Latence", description: "Affiche le ping réel vers le réseau BG LANDS.", icon: "ping", tag: "RÉSEAU" },
  { id: "serverAddress", title: "Adresse serveur", description: "Garde le serveur actuel visible dans le HUD.", icon: "server", tag: "RÉSEAU" },
  { id: "resources", title: "Performances", description: "Surveille les FPS et la mémoire utilisée par le client.", icon: "memory", tag: "CLIENT" },
  { id: "coordinates", title: "Coordonnées", description: "Affiche la position précise sans ouvrir l’écran de debug.", icon: "map", tag: "MONDE" },
  { id: "minimap", title: "Mini-carte tactique", description: "Radar directionnel compact avec grille et orientation.", icon: "map", tag: "BÊTA" },
];

const RULES = [
  { id: "essential", number: "01", title: "Principe essentiel", lead: "Une zone claim est protégée ; une zone non claim expose volontairement ses ressources au PvP et au pillage.", allowed: ["Le PvP est autorisé en zone non claim.", "Le vol dans les coffres et conteneurs accessibles est autorisé.", "Le stuff tombé lors d’un combat loyal peut être récupéré.", "Protéger une base et ses coffres relève de la responsabilité du joueur ou du clan : utilisez les claims."], forbidden: ["Le grief gratuit ou massif et la destruction des infrastructures publiques.", "Le TP-kill, le spawn-kill répété, les pièges sur un spawn public et le combat log.", "Toute triche, duplication, exploitation de bug ou technique explicitement interdite."] },
  { id: "claims", number: "02", title: "Zones claim — protection totale", lead: "Dans une zone claim, la protection du territoire prime sur le combat et le pillage.", forbidden: ["Attaquer, tuer ou provoquer un joueur : tout PvP est interdit.", "Prendre, déplacer ou conserver le stuff d’un autre joueur.", "Ouvrir, vider ou voler un coffre, une shulker, un tonneau ou tout autre conteneur.", "Casser, brûler, noyer, piéger ou modifier une construction sans autorisation.", "Contourner les protections par téléportation, piston, explosion, bug, entité ou membre complice."], note: "Ajouter un joueur à son claim ou à son clan lui accorde un accès réel. Choisissez vos membres avec prudence." },
  { id: "pvp", number: "03", title: "PvP — techniques et limites", lead: "Le combat loyal est autorisé uniquement dans les zones prévues pour le PvP.", allowed: ["Le combat loyal en zone non claim et dans les modes prévus.", "Les embuscades, poursuites et stratégies normales, sans harcèlement ciblé.", "Le Rod PvP avec les mécaniques normales du jeu."], forbidden: ["Le Crystal PvP est interdit.", "Les techniques explosives visant un kill instantané ou le contournement d’une protection.", "Macros, auto-clic, scripts, clients modifiés, désynchronisation volontaire ou lag artificiel."], note: "Une technique autorisée ne doit pas être détournée pour bloquer, harceler ou exploiter une faille." },
  { id: "cheat", number: "04", title: "Triche, duplication et bugs", lead: "Tout avantage obtenu par un outil tiers, une automatisation ou une faille doit être stoppé et signalé.", forbidden: ["Killaura, reach, aim assist, fly, speed, no-clip, anti-knockback, auto-totem, scaffold, ESP, radar, X-Ray, freecam et équivalents.", "Auto-clic, macros, scripts, bots, maintien mécanique d’une touche et farm automatique en étant absent.", "Toute duplication d’item, bloc, monnaie, coffre, shulker, entité ou récompense.", "Utiliser, cacher, vendre, donner ou blanchir un bien dupliqué.", "Provoquer un crash ou une surcharge volontaire."], note: "Une duplication accidentelle ou un bug doit être arrêté immédiatement, gardé confidentiel et signalé par ticket." },
  { id: "fairplay", number: "05", title: "Jeu loyal, signalements et sanctions", lead: "Le staff protège la qualité de jeu, les performances et l’intégrité de la communauté.", forbidden: ["Machines à lag, horloges redstone incontrôlées et amas excessifs d’entités.", "Pseudos, skins, constructions ou noms de clans haineux, sexuels, insultants ou usurpant le staff.", "Se faire justice soi-même après un vol, un cheat ou un exploit."], note: "Selon la gravité : confiscation, rollback, remboursement, exclusion, bannissement temporaire ou définitif. Les décisions sont contestables uniquement par ticket." },
];

const EMPTY_SNAPSHOT: LauncherSnapshot = {
  phase: "booting", launcherVersion: "2.0.0", gameVersion: "—", statusMessage: "Initialisation du launcher…",
  account: null, deviceCode: null, progress: null, error: null,
  server: { host: "game.bglands.fr", port: 25565, online: null, playersOnline: null, playersMaximum: null },
  client: { state: "missing", version: null, installedAt: null, components: 0 },
  microsoft: { configured: false, clientId: "", source: "missing" },
  community: { state: "loading", updatedAt: null, playersOnline: null, servers: [], fortunes: [], playtime: [], clans: [], pvpAvailable: false },
  settings: { minimumMemoryMb: 2048, maximumMemoryMb: 6144, width: 1280, height: 720, autoConnect: true, closeLauncherOnGameStart: false, modules: { keystrokes: true, cps: true, ping: true, serverAddress: true, resources: true, coordinates: true, minimap: false } },
};

const BUSY = new Set<LauncherPhase>(["booting", "verifying", "downloading", "installing", "launching"]);
function currentPage(): PageId { const value = window.location.hash.replace(/^#/, "") as PageId; return NAVIGATION.some((item) => item.id === value) ? value : "home"; }
function formatNumber(value: number) { return new Intl.NumberFormat("fr-FR").format(value); }
function formatPlaytime(seconds: number) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); return `${hours} h ${minutes.toString().padStart(2, "0")}`; }
function initials(name: string) { return name.replace(/[^a-z0-9]/giu, "").slice(0, 2).toUpperCase() || "BG"; }

function StatusPill({ snapshot }: { snapshot: LauncherSnapshot }) { const online = snapshot.server.online; const players = snapshot.community.playersOnline ?? snapshot.server.playersOnline; return <span className={`status-pill ${online === true ? "is-online" : online === false ? "is-offline" : ""}`} role="status" aria-atomic="true"><i />{online === true ? `${players ?? 0} en ligne` : online === false ? "Hors ligne" : "Connexion…"}</span>; }
function PageHeader({ eyebrow, title, description, aside }: { eyebrow: string; title: string; description: string; aside?: ReactNode }) { return <header className="page-header"><div><span className="eyebrow">{eyebrow}</span><h1 tabIndex={-1}>{title}</h1><p>{description}</p></div>{aside}</header>; }
const Button = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement> & { variant?: "primary" | "secondary" | "ghost" | "danger"; icon?: IconName }>(function Button({ children, variant = "primary", icon, ...props }, ref) { return <button ref={ref} className={`button button--${variant}`} type="button" {...props}>{icon && <Icon name={icon} size={18}/>}<span>{children}</span></button>; });
function EmptyState({ icon, title, children }: { icon: IconName; title: string; children: ReactNode }) { return <div className="empty-state"><span className="empty-state__icon"><Icon name={icon} size={24}/></span><h3>{title}</h3><p>{children}</p></div>; }
function NewsCard({ tag, title, text }: { tag: string; title: string; text: string }) { return <article className="news-card"><div className="news-card__visual"><span>{tag}</span></div><div><h3>{title}</h3><p>{text}</p></div></article>; }

function HomePage({ snapshot, navigate, primaryAction }: { snapshot: LauncherSnapshot; navigate: (page: PageId) => void; primaryAction: () => void }) {
  const ready = snapshot.phase === "ready";
  const actionLabel = snapshot.phase === "unconfigured" ? "Configurer Microsoft" : snapshot.phase === "signed-out" ? "Se connecter" : ready ? "Jouer maintenant" : snapshot.phase === "error" ? "Réparer le client" : "Voir l’installation";
  const featuredClan = snapshot.community.clans[0];
  return <div className="page page--home"><section className="hero-card"><div className="hero-card__shade"/><div className="hero-card__content"><span className="eyebrow eyebrow--gold">Survie · Clans · Conquête</span><h1>Écris ta légende.<br/><em>Défends tes terres.</em></h1><p>Un réseau cross-play français où chaque claim, chaque alliance et chaque émeraude construit l’histoire de BG LANDS.</p><div className="hero-card__actions"><Button icon="play" onClick={primaryAction}>{actionLabel}</Button><Button variant="secondary" onClick={() => navigate("servers")}>Explorer le réseau</Button></div><div className="hero-card__meta"><span><Icon name="shield" size={16}/> Distribution signée</span><span><Icon name="modules" size={16}/> {snapshot.client.components || 2} composants contrôlés</span><span>Minecraft {snapshot.gameVersion}</span></div></div><aside className="hero-status"><StatusPill snapshot={snapshot}/><div><small>Adresse Java</small><strong>{snapshot.server.host}</strong></div><div><small>Client</small><strong>{snapshot.client.state === "verified" ? "À jour" : snapshot.client.state === "installed" ? "Installé" : "À installer"}</strong></div><button type="button" onClick={() => void navigator.clipboard.writeText(snapshot.server.host)}><Icon name="copy" size={16}/> Copier l’adresse</button></aside></section><section className="dashboard-grid"><article className="feature-panel feature-panel--wide"><div className="panel-title"><div><span className="eyebrow">À la une</span><h2>La conquête commence ici</h2></div><span className="panel-index">01</span></div><p>Crée ou rejoins un clan, sécurise tes constructions avec les claims, développe ton économie et participe aux assauts encadrés.</p><button className="text-link" type="button" onClick={() => navigate("clans")}>Découvrir les clans <Icon name="arrow" size={15}/></button></article><article className="feature-panel"><div className="metric-icon"><Icon name="clan"/></div><span className="eyebrow">Clan en tête</span><strong className="big-metric">{featuredClan?.name ?? "Données live"}</strong><p>{featuredClan ? `${featuredClan.members} membres · ${featuredClan.claims} claims · puissance ${featuredClan.power}` : "Connexion au classement communautaire…"}</p></article><article className="feature-panel"><div className="metric-icon"><Icon name="wallet"/></div><span className="eyebrow">Économie</span><strong className="big-metric">{snapshot.community.fortunes[0] ? `${formatNumber(snapshot.community.fortunes[0].value)} Ém.` : "Live"}</strong><p>{snapshot.community.fortunes[0] ? `${snapshot.community.fortunes[0].name} domine actuellement le classement.` : "Les fortunes sont synchronisées avec la survie."}</p></article></section><section className="section-row"><div><span className="eyebrow">Dernières informations</span><h2>Actualités du réseau</h2></div><button className="text-link" type="button" onClick={() => navigate("news")}>Tout afficher <Icon name="arrow" size={15}/></button></section><div className="news-strip"><NewsCard tag="LAUNCHER 2.0" title="Nouveau cockpit BG LANDS" text="Connexion, modules HUD, données live et installation réunis dans une seule application."/><NewsCard tag="SURVIE" title="Un réseau pensé pour les clans" text="Économie commune, territoires, carte locale, assauts et progression collective."/><NewsCard tag="CROSS-PLAY" title="Java et Bedrock, même aventure" text="Velocity, Paper, Geyser et Floodgate relient les joueurs du réseau."/></div></div>;
}

function PlayPage({ snapshot, onConnect, onDisconnect, onPlay, onRepair, onLogs, navigate }: { snapshot: LauncherSnapshot; onConnect: () => void; onDisconnect: () => void; onPlay: () => void; onRepair: () => void; onLogs: () => void; navigate: (page: PageId) => void }) {
  const signed = snapshot.account !== null; const locked = BUSY.has(snapshot.phase) || snapshot.phase === "running";
  return <div className="page"><PageHeader eyebrow="Centre de lancement" title="Prêt pour l’aventure ?" description="Le launcher vérifie ton compte, la distribution signée et tous les composants avant chaque lancement." aside={<StatusPill snapshot={snapshot}/>}/><div className="play-layout"><section className="launch-stage"><div className="launch-stage__art"><span className="eyebrow eyebrow--gold">Serveur recommandé</span><h2>Survie publique</h2><p>Claims, économie, métiers, quêtes, clans et marché. Le profil Fabric officiel est géré automatiquement.</p><ul className="chip-list"><li>Minecraft {snapshot.gameVersion}</li><li>Fabric</li><li>Mods signés</li><li>Cross-play</li></ul></div><div className="launch-bar"><div><small>État du client</small><strong>{snapshot.statusMessage}</strong></div>{signed ? <Button icon="play" disabled={locked || snapshot.server.online === false} onClick={onPlay}>{snapshot.phase === "running" ? "En jeu" : "Lancer BG LANDS"}</Button> : <Button icon="user" disabled={locked} onClick={onConnect}>Connexion Microsoft</Button>}</div>{snapshot.progress && <div className="inline-progress" aria-label={`${snapshot.progress.percent}%`}><div><span>{snapshot.progress.label}</span><strong>{snapshot.progress.percent}%</strong></div><i><b style={{ transform: `scaleX(${snapshot.progress.percent / 100})` }}/></i><small>{snapshot.progress.detail}</small></div>}</section><aside className="play-sidebar"><section className="account-card"><span className="avatar avatar--large">{snapshot.account ? initials(snapshot.account.displayName) : <Icon name="user"/>}</span><div><small>Compte Microsoft</small><h3>{snapshot.account?.displayName ?? "Non connecté"}</h3><p>{signed ? "Minecraft Java vérifié au lancement." : "Aucun mot de passe n’est stocké."}</p></div>{signed && <Button variant="ghost" onClick={onDisconnect}>Déconnecter</Button>}</section><section className="quick-tools"><button type="button" onClick={onRepair}><Icon name="repair"/><span><strong>Réparer</strong><small>Contrôler tous les fichiers</small></span></button><button type="button" onClick={onLogs}><Icon name="log"/><span><strong>Logs</strong><small>Ouvrir le diagnostic</small></span></button><button type="button" onClick={() => navigate("modules")}><Icon name="modules"/><span><strong>Modules</strong><small>Configurer le HUD</small></span></button></section></aside></div></div>;
}

function NewsPage() { return <div className="page"><PageHeader eyebrow="Journal officiel" title="Actualités" description="Les informations utiles du réseau et du client Java, sans annonces artificielles."/><div className="editorial-grid"><article className="editorial editorial--lead"><span>30 AOÛT 2026 · LAUNCHER</span><h2>BG LANDS Launcher 2.0</h2><p>Une interface intégralement reconstruite en React, un parcours d’installation plus lisible, les modules HUD intégrés et les données communautaires live.</p></article><article className="editorial"><span>RÉSEAU</span><h2>Clans et économie partagés</h2><p>Royaume de France, les Conquérants et tous les autres clans apparaissent depuis la base réelle de la survie.</p></article><article className="editorial"><span>SÉCURITÉ</span><h2>Distribution contrôlée</h2><p>Chaque artefact du client est vérifié par empreinte avant installation. L’instance reste isolée de tes autres profils Minecraft.</p></article><article className="editorial"><span>COMMUNAUTÉ</span><h2>Une seule identité réseau</h2><p>Les joueurs Java et Bedrock partagent l’économie, les clans, les quêtes et les systèmes de progression.</p></article></div></div>; }

function ServersPage({ snapshot }: { snapshot: LauncherSnapshot }) {
  const servers = snapshot.community.servers.length ? snapshot.community.servers : [{ id: "lobby-1", label: "Accueil", players: 0 }, { id: "survival-1", label: "Survie publique", players: snapshot.server.playersOnline ?? 0 }];
  return <div className="page"><PageHeader eyebrow="Infrastructure cross-play" title="Serveurs" description="Un point d’entrée public, des services isolés et une progression partagée." aside={<StatusPill snapshot={snapshot}/>}/><div className="server-grid">{servers.map((server, index) => <article className={`server-card ${index === 1 ? "server-card--featured" : ""}`} key={server.id}><div className="server-card__top"><span className="server-number">0{index + 1}</span><span className="live-dot"><i/> {snapshot.community.state === "live" ? "LIVE" : "STATUT"}</span></div><div><span className="eyebrow">{server.id}</span><h2>{server.label}</h2><p>{server.id.includes("survival") ? "Monde principal : claims, économie, métiers, quêtes et clans." : "Point d’accueil, informations et orientation vers le réseau."}</p></div><footer><strong>{server.players}</strong><span>joueur{server.players > 1 ? "s" : ""}</span></footer></article>)}</div><div className="edition-grid"><article><span className="edition-letter">J</span><div><span className="eyebrow">Windows · macOS · Linux</span><h3>Java Edition</h3><p>Utilise le launcher pour installer Fabric et les composants BG LANDS.</p><code>game.bglands.fr</code></div></article><article><span className="edition-letter edition-letter--green">B</span><div><span className="eyebrow">Mobile · Console · PC</span><h3>Bedrock Edition</h3><p>Accès direct via Geyser/Floodgate, sans launcher Java.</p><code>game.bglands.fr : 19134</code></div></article></div></div>;
}

function ClansPage({ snapshot }: { snapshot: LauncherSnapshot }) {
  return <div className="page"><PageHeader eyebrow="Territoires et alliances" title="Clans" description="Le classement est calculé à partir du niveau, des membres et des claims enregistrés sur la survie." aside={<span className={`data-badge ${snapshot.community.state === "live" ? "is-live" : ""}`}><i/>{snapshot.community.state === "live" ? "Données live" : "Synchronisation"}</span>}/>{snapshot.community.clans.length ? <div className="clan-grid">{snapshot.community.clans.map((clan, index) => <article className={`clan-card ${index < 2 ? "clan-card--hero" : ""}`} key={clan.tag}><header><span className="clan-emblem">{clan.tag.slice(0, 2)}</span><div><small>#{index + 1} · [{clan.tag}]</small><h2>{clan.name}</h2></div><strong>{formatNumber(clan.power)}<small>puissance</small></strong></header><div className="clan-stats"><span><strong>{clan.level}</strong><small>Niveau</small></span><span><strong>{clan.members}</strong><small>Membres</small></span><span><strong>{clan.claims}</strong><small>Claims</small></span><span><strong>{formatNumber(clan.bankBalance)}</strong><small>Banque</small></span></div></article>)}</div> : <EmptyState icon="clan" title="Classement en cours de synchronisation">Le launcher conserve l’accès au jeu même si le flux communautaire est momentanément indisponible.</EmptyState>}</div>;
}

function RankingRows({ rows, kind }: { rows: CommunityPlayerRanking[]; kind: "fortune" | "playtime" }) { return <div className="ranking-list">{rows.map((entry, index) => <div className={`ranking-row ${index < 3 ? `ranking-row--${index + 1}` : ""}`} key={`${entry.platform}-${entry.name}`}><span className="ranking-position">{String(index + 1).padStart(2, "0")}</span><span className="avatar">{initials(entry.name)}</span><div><strong>{entry.name}</strong><small>{entry.platform === "BEDROCK" ? "Bedrock" : "Java"}</small></div><strong className="ranking-value">{kind === "fortune" ? `${formatNumber(entry.value)} Ém.` : formatPlaytime(entry.value)}</strong></div>)}</div>; }

function RankingsPage({ snapshot }: { snapshot: LauncherSnapshot }) {
  const [tab, setTab] = useState<"fortune" | "playtime" | "pvp">("fortune"); const rows = tab === "fortune" ? snapshot.community.fortunes : snapshot.community.playtime;
  return <div className="page"><PageHeader eyebrow="Compétition communautaire" title="Classements" description="Fortunes et temps de jeu proviennent directement des données publiques de la survie." aside={<span className={`data-badge ${snapshot.community.state === "live" ? "is-live" : ""}`}><i/>{snapshot.community.state === "live" ? "Actualisé" : "Indisponible"}</span>}/><div className="tabs" role="tablist" aria-label="Type de classement"><button role="tab" aria-selected={tab === "fortune"} onClick={() => setTab("fortune")}><Icon name="wallet"/>Fortunes</button><button role="tab" aria-selected={tab === "playtime"} onClick={() => setTab("playtime")}><Icon name="clock"/>Temps de jeu</button><button role="tab" aria-selected={tab === "pvp"} onClick={() => setTab("pvp")}><Icon name="swords"/>Top PvP</button></div>{tab === "pvp" ? <EmptyState icon="swords" title="Statistiques PvP pas encore enregistrées">La base actuelle ne stocke pas encore les éliminations et morts. Aucun faux score n’est affiché ; ce classement s’activera quand le suivi serveur sera disponible.</EmptyState> : rows.length ? <RankingRows rows={rows} kind={tab}/> : <EmptyState icon="rank" title="Classement temporairement indisponible">La connexion au flux public n’a pas abouti. Réessaie dans quelques secondes.</EmptyState>}</div>;
}

const COSMETICS = [["Cape Frontière", "Cape", "Motif topographique or et mousse"], ["Ailes du Royaume", "Dos", "Effet visuel royal sans avantage"], ["Trace Émeraude", "Traînée", "Particules vertes lors des déplacements"], ["Halo des Conquérants", "Aura", "Cercle lumineux discret autour du joueur"], ["Compagnon Golem", "Familier", "Mini-golem cosmétique en lobby"], ["Pack Emotes", "Animation", "Six emotes communautaires"]];
function ShopPage() { return <div className="page"><PageHeader eyebrow="Catalogue cosmétique" title="Boutique" description="Deux grades et des objets purement visuels. Le paiement sera ouvert uniquement après validation de la boutique officielle." aside={<span className="preview-badge">APERÇU · AUCUN PAIEMENT</span>}/><div className="rank-plans"><article><span className="plan-mark">I</span><span className="eyebrow">Grade communautaire</span><h2>Aventurier</h2><p>Pour soutenir le serveur et personnaliser ton profil sans avantage de gameplay.</p><ul><li><Icon name="check"/>Préfixe et couleur de chat</li><li><Icon name="check"/>2 emotes exclusives</li><li><Icon name="check"/>1 cape au choix</li></ul><Button disabled variant="secondary">Bientôt disponible</Button></article><article className="rank-plan--premium"><span className="plan-mark">II</span><span className="eyebrow eyebrow--gold">Grade signature</span><h2>Souverain</h2><p>L’expérience cosmétique complète pour les joueurs qui portent les couleurs de BG LANDS.</p><ul><li><Icon name="check"/>Tous les avantages Aventurier</li><li><Icon name="check"/>Aura et traînée exclusives</li><li><Icon name="check"/>Cosmétiques saisonniers</li></ul><Button disabled>Bientôt disponible</Button></article></div><div className="section-row"><div><span className="eyebrow">Collection</span><h2>Cosmétiques conçus pour BG LANDS</h2></div></div><div className="cosmetic-grid">{COSMETICS.map(([name, type, description], index) => <article key={name}><div className={`cosmetic-visual cosmetic-visual--${(index % 3) + 1}`}><Icon name={index % 2 ? "shield" : "clan"} size={30}/></div><span>{type}</span><h3>{name}</h3><p>{description}</p><button type="button" disabled>À venir</button></article>)}</div></div>; }

function ModulesPage({ settings, onSave, busy }: { settings: LauncherSettings; onSave: (settings: LauncherSettings) => void; busy: boolean }) {
  const [modules, setModules] = useState(settings.modules); useEffect(() => setModules(settings.modules), [settings]); const enabledCount = Object.values(modules).filter(Boolean).length;
  return <div className="page"><PageHeader eyebrow="Client Fabric officiel" title="Modules HUD" description="Ces réglages sont transmis au mod BG LANDS au prochain lancement de Minecraft." aside={<span className="module-count"><strong>{enabledCount}</strong> / {MODULES.length} actifs</span>}/><div className="module-layout"><aside className="module-preview"><span className="eyebrow">Aperçu en jeu</span><div className="hud-preview"><div className="hud-keys"><span>W</span><span>A</span><span>S</span><span>D</span><b>{modules.cps ? "7 CPS" : "—"}</b></div><div className="hud-map">{modules.minimap ? <><small>N</small><strong>◆</strong><span>NORD</span></> : <em>Mini-carte inactive</em>}</div><div className="hud-metrics">{modules.ping && <span>PING <b>32 ms</b></span>}{modules.resources && <span>CLIENT <b>144 FPS</b></span>}{modules.coordinates && <span>POSITION <b>248 · 72 · −91</b></span>}</div></div><p>Le HUD n’apporte aucun avantage interdit. Le radar affiche seulement une grille directionnelle, jamais les joueurs ni les minerais.</p></aside><section className="module-list">{MODULES.map((module) => <label className="module-row" key={module.id}><span className="module-row__icon"><Icon name={module.icon}/></span><span><span className="module-tag">{module.tag}</span><strong>{module.title}</strong><small>{module.description}</small></span><input type="checkbox" checked={modules[module.id]} onChange={(event) => setModules((current) => ({ ...current, [module.id]: event.target.checked }))}/><i aria-hidden="true"/></label>)}<Button disabled={busy} onClick={() => onSave({ ...settings, modules })}>Appliquer au prochain lancement</Button></section></div></div>;
}

function RulesPage({ onSupport }: { onSupport: () => void }) {
  const [query, setQuery] = useState(""); const visible = RULES.filter((rule) => `${rule.title} ${rule.lead} ${(rule.allowed ?? []).join(" ")} ${rule.forbidden.join(" ")} ${rule.note ?? ""}`.toLocaleLowerCase("fr").includes(query.toLocaleLowerCase("fr")));
  return <div className="page"><PageHeader eyebrow="Version en vigueur · v6.0" title="Règlement en jeu" description="Lis ces règles avant ta première connexion. En cas de doute, demande au staff avant d’utiliser une technique." aside={<Button variant="secondary" onClick={onSupport}>Ouvrir un ticket</Button>}/><div className="rules-toolbar"><label htmlFor="rules-search">Rechercher dans le règlement</label><input id="rules-search" type="search" value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Ex. claim, PvP, duplication…"/></div><div className="rules-layout"><nav aria-label="Sommaire du règlement">{RULES.map((rule) => <a key={rule.id} href={`#rule-${rule.id}`} onClick={(event) => { event.preventDefault(); document.getElementById(`rule-${rule.id}`)?.scrollIntoView({ behavior: "smooth" }); }}><span>{rule.number}</span>{rule.title}</a>)}</nav><article className="rules-document">{visible.map((rule) => <section id={`rule-${rule.id}`} key={rule.id}><header><span>{rule.number} / 05</span><h2>{rule.title}</h2></header><p className="rule-lead">{rule.lead}</p><div className="rule-columns">{rule.allowed && <div className="rule-box rule-box--allowed"><h3>Autorisé</h3><ul>{rule.allowed.map((item) => <li key={item}>{item}</li>)}</ul></div>}<div className="rule-box rule-box--forbidden"><h3>Interdit</h3><ul>{rule.forbidden.map((item) => <li key={item}>{item}</li>)}</ul></div></div>{rule.note && <div className="rule-note"><Icon name="shield"/><p>{rule.note}</p></div>}</section>)}{visible.length === 0 && <EmptyState icon="rules" title="Aucun résultat">Essaie un mot plus court ou consulte le sommaire complet.</EmptyState>}<footer><strong>BG LANDS · Règlement en jeu v6.0</strong><span>Version en vigueur</span></footer></article></div></div>;
}

function Toggle({ title, description, checked, onChange }: { title: string; description: string; checked: boolean; onChange: (checked: boolean) => void }) { return <label className="toggle"><span><strong>{title}</strong><small>{description}</small></span><input type="checkbox" checked={checked} onChange={(event) => onChange(event.target.checked)}/><i aria-hidden="true"/></label>; }

function SettingsPage({ snapshot, onSave, onConfigureMicrosoft, onMicrosoftSetup, onRepair, onLogs, busy }: { snapshot: LauncherSnapshot; onSave: (settings: LauncherSettings) => void; onConfigureMicrosoft: (id: string) => void; onMicrosoftSetup: () => void; onRepair: () => void; onLogs: () => void; busy: boolean }) {
  const [settings, setSettings] = useState(snapshot.settings); const [clientId, setClientId] = useState(snapshot.microsoft.clientId); const [error, setError] = useState(""); useEffect(() => setSettings(snapshot.settings), [snapshot.settings]); useEffect(() => setClientId(snapshot.microsoft.clientId), [snapshot.microsoft.clientId]);
  const submitMicrosoft = (event: FormEvent) => { event.preventDefault(); const clean = clientId.trim(); if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(clean)) { setError("L’AppID doit être un UUID Microsoft Entra valide au format 8-4-4-4-12."); return; } setError(""); onConfigureMicrosoft(clean); };
  return <div className="page"><PageHeader eyebrow="Launcher et instance locale" title="Paramètres" description="Configure Microsoft, les ressources du jeu et le comportement du launcher."/><section className="settings-card microsoft-card"><div><span className="eyebrow">Configuration Microsoft</span><h2>Application BG LANDS</h2><p>L’Application (client) ID est un identifiant public. Aucun secret ni mot de passe ne doit être collé ici.</p><span className={`config-status ${snapshot.microsoft.configured ? "is-configured" : ""}`}><i/>{snapshot.microsoft.configured ? `Configuré · ${snapshot.microsoft.source === "local" ? "local" : "intégré"}` : "Action requise"}</span></div><form onSubmit={submitMicrosoft}><label htmlFor="microsoft-client-id">Application (client) ID</label><input id="microsoft-client-id" autoComplete="off" spellCheck={false} value={clientId} onChange={(event) => setClientId(event.target.value)} aria-invalid={Boolean(error)} aria-describedby={error ? "appid-error" : undefined} placeholder="12345678-1234-4234-9234-123456789abc"/><div>{error && <p id="appid-error" className="field-error" role="alert">{error}</p>}<button className="text-link" type="button" onClick={onMicrosoftSetup}>Ouvrir Microsoft Entra <Icon name="arrow"/></button></div><Button type="submit" disabled={busy}>Enregistrer l’AppID</Button></form></section><div className="settings-grid"><section className="settings-card"><span className="eyebrow">Minecraft Java</span><h2>Ressources du jeu</h2><div className="range-field"><label htmlFor="memory">Mémoire maximale <output>{(settings.maximumMemoryMb / 1024).toFixed(1)} Go</output></label><input id="memory" type="range" min="2048" max="16384" step="512" value={settings.maximumMemoryMb} onChange={(event) => setSettings({ ...settings, maximumMemoryMb: Number(event.target.value) })}/><div><span>2 Go</span><span>16 Go</span></div></div><div className="field-grid"><label>Mémoire minimale<input type="number" min="512" max="16384" step="512" value={settings.minimumMemoryMb} onChange={(event) => setSettings({ ...settings, minimumMemoryMb: Number(event.target.value) })}/></label><label>Résolution<select value={`${settings.width}x${settings.height}`} onChange={(event) => { const [width, height] = event.target.value.split("x").map(Number); setSettings({ ...settings, width: width!, height: height! }); }}><option value="1280x720">1280 × 720</option><option value="1600x900">1600 × 900</option><option value="1920x1080">1920 × 1080</option><option value="2560x1440">2560 × 1440</option></select></label></div><Toggle title="Connexion automatique" description="Rejoint le réseau dès l’ouverture de Minecraft." checked={settings.autoConnect} onChange={(checked) => setSettings({ ...settings, autoConnect: checked })}/><Toggle title="Fermer après lancement" description="Libère les ressources quand le jeu démarre." checked={settings.closeLauncherOnGameStart} onChange={(checked) => setSettings({ ...settings, closeLauncherOnGameStart: checked })}/><Button disabled={busy || settings.minimumMemoryMb > settings.maximumMemoryMb} onClick={() => onSave(settings)}>Enregistrer les paramètres</Button></section><aside className="settings-aside"><section className="settings-card"><span className="eyebrow">Installation</span><h2>Client officiel</h2><dl><div><dt>État</dt><dd>{snapshot.client.state === "verified" ? "Vérifié" : snapshot.client.state === "installed" ? "Installé" : "Absent"}</dd></div><div><dt>Version</dt><dd>{snapshot.client.version ?? "—"}</dd></div><div><dt>Composants</dt><dd>{snapshot.client.components}</dd></div><div><dt>Minecraft</dt><dd>{snapshot.gameVersion}</dd></div></dl></section><div className="tool-buttons"><button type="button" onClick={onRepair}><Icon name="repair"/><span><strong>Réparer le client</strong><small>Revérifier les composants</small></span></button><button type="button" onClick={onLogs}><Icon name="log"/><span><strong>Ouvrir les logs</strong><small>Diagnostic local</small></span></button></div></aside></div></div>;
}

function DeviceCodeDialog({ snapshot, onOpen, onCancel, onCopy }: { snapshot: LauncherSnapshot; onOpen: () => void; onCancel: () => void; onCopy: () => void }) {
  const ref = useRef<HTMLButtonElement>(null); useEffect(() => { ref.current?.focus(); const close = (event: KeyboardEvent) => { if (event.key === "Escape") onCancel(); }; window.addEventListener("keydown", close); return () => window.removeEventListener("keydown", close); }, [onCancel]); if (!snapshot.deviceCode) return null;
  return <motion.div className="modal-backdrop" role="presentation" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}><section className="modal" role="dialog" aria-modal="true" aria-labelledby="device-title"><button className="icon-button modal-close" aria-label="Annuler la connexion Microsoft" type="button" onClick={onCancel}><Icon name="close"/></button><span className="modal-mark"><Icon name="shield" size={28}/></span><span className="eyebrow">Connexion officielle</span><h2 id="device-title">Finalise sur Microsoft</h2><p>Ouvre la page sécurisée puis saisis ce code. Le launcher détectera automatiquement la validation.</p><button className="device-code" type="button" onClick={onCopy}><strong>{snapshot.deviceCode.userCode}</strong><span><Icon name="copy"/> Copier le code</span></button><Button ref={ref} icon="arrow" onClick={onOpen}>Ouvrir la page Microsoft</Button><button className="text-button" type="button" onClick={onCancel}>Annuler la connexion</button></section></motion.div>;
}

function InstallOverlay({ snapshot }: { snapshot: LauncherSnapshot }) { if (!BUSY.has(snapshot.phase) || snapshot.phase === "booting") return null; const percent = snapshot.progress?.percent ?? (snapshot.phase === "launching" ? 100 : 0); return <div className="install-overlay" role="status" aria-live="polite"><div className="install-card"><img src="../resources/assets/logo.png" alt="BG LANDS"/><span className="install-orbit" aria-hidden="true"><i/><b/></span><span className="eyebrow">Distribution officielle</span><h2>{snapshot.phase === "launching" ? "Ouverture de Minecraft" : snapshot.progress?.label ?? "Préparation du client"}</h2><p>{snapshot.statusMessage}</p><div className="install-progress"><i><b style={{ transform: `scaleX(${percent / 100})` }}/></i><span>{percent > 0 ? `${percent}%` : "Vérification…"}</span></div><small>Ne ferme pas le launcher pendant cette opération.</small></div></div>; }

function App() {
  const api = window.bglands as BglandsLauncherApi | undefined; const reducedMotion = useReducedMotion(); const [snapshot, setSnapshot] = useState(EMPTY_SNAPSHOT); const [page, setPage] = useState<PageId>(currentPage()); const [busy, setBusy] = useState(false); const [toast, setToast] = useState<{ text: string; error: boolean } | null>(null);
  useEffect(() => { if (!api) { setSnapshot({ ...EMPTY_SNAPSHOT, phase: "error", statusMessage: "Le pont sécurisé du launcher est indisponible.", error: { title: "Launcher indisponible", message: "Réinstalle l’application officielle.", recoverable: false } }); return; } const unsubscribe = api.onSnapshot(setSnapshot); void api.getSnapshot().then(setSnapshot).catch(() => setToast({ text: "Impossible de charger l’état du launcher.", error: true })); return unsubscribe; }, [api]);
  useEffect(() => { const hash = () => setPage(currentPage()); window.addEventListener("hashchange", hash); return () => window.removeEventListener("hashchange", hash); }, []);
  useEffect(() => { if (!toast) return; const timer = window.setTimeout(() => setToast(null), 3600); return () => window.clearTimeout(timer); }, [toast]);
  const navigate = (next: PageId) => { window.location.hash = next; setPage(next); window.requestAnimationFrame(() => document.querySelector<HTMLElement>(".page h1")?.focus()); };
  const action = async (operation: () => Promise<void>, success?: string) => { if (!api || busy) return; setBusy(true); try { await operation(); setSnapshot(await api.getSnapshot()); if (success) setToast({ text: success, error: false }); } catch { setToast({ text: "L’opération n’a pas pu aboutir. Consulte les logs si nécessaire.", error: true }); try { setSnapshot(await api.getSnapshot()); } catch { /* pushed state remains */ } } finally { setBusy(false); } };
  const connect = () => { if (!api || busy) return; if (!snapshot.microsoft.configured) { navigate("settings"); setToast({ text: "Ajoute d’abord l’AppID Microsoft BG LANDS.", error: true }); return; } setBusy(true); void api.connectMicrosoft().catch(() => setToast({ text: "Connexion Microsoft impossible.", error: true })).finally(() => setBusy(false)); window.setTimeout(() => setBusy(false), 450); };
  const primaryAction = () => { if (snapshot.phase === "unconfigured") { navigate("settings"); return; } if (snapshot.phase === "signed-out") { navigate("play"); connect(); return; } if (snapshot.phase === "ready") { navigate("play"); void action(() => api!.play()); return; } if (snapshot.phase === "error" && snapshot.account) { navigate("play"); void action(() => api!.repair()); return; } navigate("play"); };
  const content = useMemo(() => { const common = { snapshot }; switch (page) { case "home": return <HomePage {...common} navigate={navigate} primaryAction={primaryAction}/>; case "play": return <PlayPage {...common} navigate={navigate} onConnect={connect} onDisconnect={() => void action(() => api!.disconnectMicrosoft(), "Compte déconnecté.")} onPlay={() => void action(() => api!.play())} onRepair={() => void action(() => api!.repair(), "Réparation lancée.")} onLogs={() => void action(() => api!.openLogs())}/>; case "news": return <NewsPage/>; case "servers": return <ServersPage {...common}/>; case "clans": return <ClansPage {...common}/>; case "rankings": return <RankingsPage {...common}/>; case "shop": return <ShopPage/>; case "modules": return <ModulesPage settings={snapshot.settings} busy={busy} onSave={(settings) => void action(() => api!.saveSettings(settings), "Modules appliqués au prochain lancement.")}/>; case "rules": return <RulesPage onSupport={() => void action(() => api!.openSupport())}/>; case "settings": return <SettingsPage {...common} busy={busy} onSave={(settings) => void action(() => api!.saveSettings(settings), "Paramètres enregistrés.")} onConfigureMicrosoft={(id) => void action(() => api!.configureMicrosoft(id), "Application Microsoft configurée.")} onMicrosoftSetup={() => void action(() => api!.openMicrosoftSetup())} onRepair={() => void action(() => api!.repair())} onLogs={() => void action(() => api!.openLogs())}/>; } }, [page, snapshot, busy, api]);
  return <div className="launcher-shell" data-phase={snapshot.phase}><header className="topbar"><div className="brand"><img src="../resources/assets/logo.png" alt="BG LANDS"/><span><strong>LAUNCHER OFFICIEL</strong><small>JAVA EDITION · v{snapshot.launcherVersion}</small></span></div><div className="topbar__right"><StatusPill snapshot={snapshot}/><span className="security-badge"><Icon name="shield" size={16}/>Distribution contrôlée</span><button className="profile-chip" type="button" onClick={() => navigate(snapshot.account ? "settings" : "play")}><span className="avatar">{snapshot.account ? initials(snapshot.account.displayName) : <Icon name="user" size={17}/>}</span><span><small>Compte</small><strong>{snapshot.account?.displayName ?? "Se connecter"}</strong></span></button></div></header><div className="app-body"><aside className="sidebar"><nav aria-label="Navigation principale">{NAVIGATION.map((item) => <button type="button" key={item.id} className={page === item.id ? "is-active" : ""} aria-current={page === item.id ? "page" : undefined} onClick={() => navigate(item.id)}><Icon name={item.icon}/><span>{item.label}</span>{item.id === "modules" && <b>{Object.values(snapshot.settings.modules).filter(Boolean).length}</b>}</button>)}</nav><div className="sidebar-footer"><span className={`network-health ${snapshot.server.online === true ? "is-online" : ""}`}><i/><span><small>Réseau Java</small><strong>{snapshot.server.online === true ? "Opérationnel" : snapshot.server.online === false ? "Hors ligne" : "Vérification"}</strong></span></span></div></aside><main id="main-content" tabIndex={-1}><AnimatePresence mode="wait" initial={false}><motion.div key={page} initial={reducedMotion ? false : { opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={reducedMotion ? undefined : { opacity: 0, y: -5 }} transition={{ duration: reducedMotion ? 0 : 0.2, ease: [0.22, 1, 0.36, 1] }}>{content}</motion.div></AnimatePresence></main></div><footer className="app-footer"><span><i/>Instance isolée · Fichiers signés</span><span>Produit indépendant, non approuvé ni associé à Mojang ou Microsoft.</span><button type="button" onClick={() => void action(() => api!.openSupport())}>Support Discord</button></footer>{snapshot.phase === "booting" && <div className="boot-screen"><img src="../resources/assets/logo.png" alt="BG LANDS"/><span><i/><i/><i/></span><p>Préparation de ton aventure…</p></div>}<AnimatePresence>{snapshot.phase === "device-code" && <DeviceCodeDialog snapshot={snapshot} onOpen={() => void action(() => api!.openMicrosoftLogin())} onCancel={() => void action(() => api!.cancelMicrosoftLogin())} onCopy={() => { if (snapshot.deviceCode) void navigator.clipboard.writeText(snapshot.deviceCode.userCode).then(() => setToast({ text: "Code Microsoft copié.", error: false })); }}/>}</AnimatePresence><InstallOverlay snapshot={snapshot}/><AnimatePresence>{toast && <motion.div className={`toast ${toast.error ? "toast--error" : ""}`} role={toast.error ? "alert" : "status"} initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 8 }}><i/>{toast.text}</motion.div>}</AnimatePresence></div>;
}

const root = document.getElementById("root");
if (!root) throw new Error("Renderer root is missing");
createRoot(root).render(<App/>);
