import { EventEmitter } from "node:events";
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import { totalmem } from "node:os";
import { dirname } from "node:path";
import { readInstalledReleaseState } from "../../../launcher-manifest/src/installed-state.ts";
import type { LauncherSnapshot, LauncherSettings as UiSettings } from "../shared/contracts.js";
import { LauncherAuth, type DeviceCodeNotice } from "./auth.js";
import { persistMicrosoftClientId, type LauncherConfig, type LauncherPaths, validateSettings } from "./config.js";
import type { LauncherLogger } from "./logger.js";
import { fetchCommunitySnapshot, unavailableCommunity } from "./community.js";
import { MinecraftManager } from "./minecraft.js";
import { ClientReleaseManager, type ReleaseProgress } from "./release.js";
import { probeMinecraftServer } from "./server-status.js";

function safeErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message.slice(0, 1_000);
  return "Une erreur inconnue s'est produite";
}

async function loadSettings(path: string, defaults: UiSettings): Promise<UiSettings> {
  try {
    return validateSettings(JSON.parse(await readFile(path, "utf8")) as unknown);
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
      // Invalid user settings are safely reset to pinned defaults.
    }
    return { ...defaults };
  }
}

async function persistSettings(path: string, settings: UiSettings): Promise<void> {
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
  const temporary = `${path}.tmp-${process.pid}`;
  await rm(temporary, { force: true });
  await writeFile(temporary, `${JSON.stringify(settings, null, 2)}\n`, { flag: "wx", mode: 0o600 });
  await rename(temporary, path);
}

export class LauncherOrchestrator extends EventEmitter {
  private readonly auth: LauncherAuth;
  private readonly releases: ClientReleaseManager;
  private readonly minecraft: MinecraftManager;
  private activeOperation: Promise<void> | undefined;
  private activeController: AbortController | undefined;
  private loginUrl: string | undefined;
  private loginCancelled = false;
  private serverProbeActive = false;
  private communityProbeActive = false;
  private snapshot: LauncherSnapshot;

  constructor(
    private readonly config: LauncherConfig,
    private readonly paths: LauncherPaths,
    private readonly logger: LauncherLogger,
  ) {
    super();
    this.auth = new LauncherAuth(config, paths.msalCache, logger);
    this.releases = new ClientReleaseManager(config, paths, logger);
    this.minecraft = new MinecraftManager(config, paths, logger);
    this.snapshot = {
      phase: "booting",
      launcherVersion: config.launcherVersion,
      gameVersion: "—",
      statusMessage: "Initialisation sécurisée du launcher…",
      account: null,
      deviceCode: null,
      progress: null,
      error: null,
      server: {
        host: config.server.host,
        port: config.server.port,
        online: null,
        playersOnline: null,
        playersMaximum: null,
      },
      client: {
        state: "missing",
        version: null,
        installedAt: null,
        components: 0,
      },
      microsoft: {
        configured: config.microsoft.clientId !== "",
        clientId: config.microsoft.clientId,
        source: config.microsoft.source,
      },
      community: { ...unavailableCommunity(), state: "loading" },
      settings: { ...config.defaults },
    };
  }

  getSnapshot(): LauncherSnapshot {
    return structuredClone(this.snapshot);
  }

  getMicrosoftLoginUrl(): string | undefined { return this.loginUrl; }

  private publish(patch: Partial<LauncherSnapshot>): void {
    this.snapshot = { ...this.snapshot, ...patch };
    this.emit("snapshot", this.getSnapshot());
  }

  async initialize(): Promise<void> {
    await Promise.all([
      mkdir(this.paths.instance, { recursive: true, mode: 0o700 }),
      mkdir(this.paths.minecraft, { recursive: true, mode: 0o700 }),
      mkdir(this.paths.cache, { recursive: true, mode: 0o700 }),
      mkdir(this.paths.logs, { recursive: true, mode: 0o700 }),
      mkdir(this.paths.runtime, { recursive: true, mode: 0o700 }),
    ]);
    this.snapshot.settings = await loadSettings(this.paths.settings, this.config.defaults);
    try {
      const installed = await readInstalledReleaseState(this.paths.instance);
      if (installed) {
        this.snapshot.client = {
          state: "installed",
          version: installed.version,
          installedAt: installed.installedAt,
          components: installed.artifacts.length,
        };
      }
    } catch (error) {
      await this.logger.error("État local du client illisible; une réparation sera proposée", error);
    }
    if (!this.auth.configured) {
      this.publish({
        phase: "unconfigured",
        statusMessage: "Connexion Microsoft à finaliser par l'administrateur BG LANDS",
      });
    } else {
      const account = await this.auth.restore();
      this.publish({
        phase: account ? "ready" : "signed-out",
        account: account ? { displayName: account.name } : null,
        statusMessage: account ? `Prêt à jouer avec ${account.name}` : "Connecte ton compte Microsoft officiel",
      });
    }
    void this.refreshServerStatus();
    void this.refreshCommunity();
    setInterval(() => void this.refreshServerStatus(), 30_000).unref();
    setInterval(() => void this.refreshCommunity(), 30_000).unref();
  }

  private async refreshCommunity(): Promise<void> {
    if (this.communityProbeActive) return;
    this.communityProbeActive = true;
    try {
      const community = await fetchCommunitySnapshot(this.config.communityUrl);
      this.publish({ community });
    } catch (error) {
      if (this.snapshot.community.state !== "live") this.publish({ community: unavailableCommunity() });
      await this.logger.error("Flux communautaire indisponible", error);
    } finally {
      this.communityProbeActive = false;
    }
  }

  private async refreshServerStatus(): Promise<void> {
    if (this.serverProbeActive) return;
    this.serverProbeActive = true;
    try {
      const status = await probeMinecraftServer(this.config.server.host, this.config.server.port);
      this.publish({ server: { ...this.snapshot.server, ...status } });
    } finally {
      this.serverProbeActive = false;
    }
  }

  private runExclusive(operation: () => Promise<void>): Promise<void> {
    if (this.activeOperation) return Promise.reject(new Error("Une opération est déjà en cours"));
    const task = operation().finally(() => {
      if (this.activeOperation === task) this.activeOperation = undefined;
      this.activeController = undefined;
    });
    this.activeOperation = task;
    return task;
  }

  connectMicrosoft(): Promise<void> {
    return this.runExclusive(async () => {
      this.loginCancelled = false;
      this.publish({ phase: "device-code", statusMessage: "Préparation de la connexion Microsoft…", error: null });
      try {
        const account = await this.auth.signIn((code) => this.handleDeviceCode(code));
        this.loginUrl = undefined;
        this.publish({
          phase: "ready",
          account: { displayName: account.name },
          deviceCode: null,
          statusMessage: `Compte Java vérifié : ${account.name}`,
        });
      } catch (error) {
        if (this.loginCancelled) {
          this.publish({ phase: "signed-out", deviceCode: null, error: null, statusMessage: "Connexion annulée" });
          return;
        }
        this.fail("Connexion impossible", error);
        throw error;
      }
    });
  }

  private handleDeviceCode(code: DeviceCodeNotice): void {
    this.loginUrl = code.verificationUri;
    this.publish({
      phase: "device-code",
      statusMessage: "Saisis ce code sur la page Microsoft officielle",
      deviceCode: {
        userCode: code.userCode,
        verificationUri: code.verificationUri,
        expiresAt: Date.parse(code.expiresAt),
      },
      error: null,
    });
  }

  cancelMicrosoftLogin(): void {
    this.loginCancelled = true;
    this.auth.cancelSignIn();
    this.loginUrl = undefined;
    this.publish({ phase: "signed-out", deviceCode: null, statusMessage: "Connexion annulée" });
  }

  async disconnectMicrosoft(): Promise<void> {
    if (this.minecraft.isRunning) throw new Error("Déconnexion indisponible pendant que Minecraft tourne");
    await this.auth.signOut();
    this.publish({ phase: "signed-out", account: null, deviceCode: null, statusMessage: "Compte déconnecté" });
  }

  async saveSettings(raw: UiSettings): Promise<void> {
    if (this.minecraft.isRunning) throw new Error("Les réglages ne peuvent pas changer pendant le jeu");
    const settings = validateSettings(raw);
    const availableMb = Math.floor(totalmem() / 1024 / 1024);
    if (settings.maximumMemoryMb > Math.max(2_048, availableMb - 1_024)) {
      throw new Error("La RAM demandée laisserait moins de 1 Go à Windows");
    }
    await persistSettings(this.paths.settings, settings);
    this.publish({ settings, statusMessage: "Réglages enregistrés" });
  }

  configureMicrosoft(raw: string): Promise<void> {
    return this.runExclusive(async () => {
      if (this.minecraft.isRunning) throw new Error("La configuration Microsoft ne peut pas changer pendant le jeu");
      const clientId = await persistMicrosoftClientId(this.paths.microsoftConfig, raw);
      await this.auth.configure(clientId);
      this.config.microsoft.source = "local";
      this.publish({
        phase: "signed-out",
        account: null,
        deviceCode: null,
        error: null,
        microsoft: { configured: true, clientId, source: "local" },
        statusMessage: "Application Microsoft configurée — tu peux maintenant connecter ton compte",
      });
    });
  }

  repair(): Promise<void> {
    return this.runExclusive(async () => {
      if (!this.snapshot.account) throw new Error("Connecte d'abord un compte possédant Java Edition");
      this.activeController = new AbortController();
      try {
        // Fail closed before any licensed game file is requested.
        await this.auth.acquireMinecraftSession();
        const manifest = await this.releases.synchronize((progress) => this.handleReleaseProgress(progress), this.activeController.signal);
        this.publish({ gameVersion: manifest.payload.game.minecraft });
        await this.minecraft.prepare(manifest, (progress) => this.publish({
          phase: "installing",
          statusMessage: progress.message,
          progress: { label: progress.message, detail: progress.stage, completedBytes: 0, totalBytes: 0, percent: 0 },
        }), this.activeController.signal);
        this.publish({
          phase: this.snapshot.account ? "ready" : "signed-out",
          progress: null,
          error: null,
          statusMessage: "Installation vérifiée et réparée",
          client: {
            state: "verified",
            version: manifest.payload.version,
            installedAt: new Date().toISOString(),
            components: manifest.payload.artifacts.length,
          },
        });
      } catch (error) {
        this.fail("Réparation impossible", error);
        throw error;
      }
    });
  }

  play(): Promise<void> {
    return this.runExclusive(async () => {
      if (!this.snapshot.account) throw new Error("Connecte d'abord ton compte Microsoft");
      if (this.minecraft.isRunning) throw new Error("Minecraft est déjà lancé");
      this.activeController = new AbortController();
      try {
        // Entitlement and profile are revalidated before downloading licensed game files.
        const session = await this.auth.acquireMinecraftSession();
        const manifest = await this.releases.synchronize((progress) => this.handleReleaseProgress(progress), this.activeController.signal);
        this.publish({ gameVersion: manifest.payload.game.minecraft });
        const prepared = await this.minecraft.prepare(manifest, (progress) => this.publish({
          phase: "installing",
          statusMessage: progress.message,
          progress: { label: progress.message, detail: progress.stage, completedBytes: 0, totalBytes: 0, percent: 0 },
        }), this.activeController.signal);
        this.publish({
          client: {
            state: "verified",
            version: manifest.payload.version,
            installedAt: new Date().toISOString(),
            components: manifest.payload.artifacts.length,
          },
        });
        this.publish({ phase: "launching", progress: null, statusMessage: "Ouverture de Minecraft…" });
        await this.minecraft.launch(prepared, session, this.snapshot.settings, (code) => {
          if (code === 0 || code === null) {
            this.publish({ phase: "ready", statusMessage: "Minecraft est fermé — prêt à rejouer" });
          } else {
            this.publish({
              phase: "error",
              statusMessage: `Minecraft s'est arrêté avec le code ${code}`,
              error: { title: "Minecraft s'est fermé", message: "Consulte les logs ou lance une réparation.", recoverable: true },
            });
          }
        });
        this.publish({ phase: "running", statusMessage: `En jeu sur ${this.config.server.host}`, error: null });
      } catch (error) {
        this.fail("Lancement impossible", error);
        throw error;
      }
    });
  }

  private handleReleaseProgress(progress: ReleaseProgress): void {
    const total = progress.total ?? 0;
    const current = progress.current ?? 0;
    this.publish({
      phase: progress.stage === "download" ? "downloading" : progress.stage === "manifest" ? "verifying" : "installing",
      statusMessage: progress.message,
      progress: {
        label: progress.message,
        detail: progress.stage,
        completedBytes: current,
        totalBytes: total,
        percent: total > 0 ? Math.min(100, Math.round(current / total * 100)) : 0,
      },
      error: null,
    });
  }

  private fail(title: string, error: unknown): void {
    const message = safeErrorMessage(error);
    void this.logger.error(title, error);
    this.publish({
      phase: "error",
      statusMessage: message,
      progress: null,
      error: { title, message, recoverable: true },
    });
  }
}
