import { MinecraftFolder, Version, launch, type ResolvedVersion } from "@xmcl/core";
import {
  createDefaultNodeInstallRuntime,
  createFileTransferInstallDownload,
  createFabricInstallWorkflow,
  createJavaRuntimeInstallWorkflow,
  executeInstallManifest,
  executeInstallWorkflow,
  getVersionList,
  resolveAssetMetadataInstallManifest,
  resolveAssetObjectInstallFiles,
  resolveJava,
  resolveLibraryInstallFiles,
  resolveMinecraftJarInstallFile,
  resolveMinecraftVersionJsonInstallFile,
  type InstallFile,
  type JavaRuntimes,
  type JavaRuntimeTarget,
} from "@xmcl/installer";
import { access, mkdir, open } from "node:fs/promises";
import { join } from "node:path";
import type { ChildProcess } from "node:child_process";
import type { ReleaseManifest } from "../../../launcher-manifest/src/types.ts";
import type { LauncherConfig, LauncherPaths, LauncherSettings } from "./config.js";
import type { MinecraftSession } from "./auth.js";
import type { LauncherLogger } from "./logger.js";
import { redact } from "./logger.js";

export interface GameInstallProgress {
  stage: "minecraft" | "fabric" | "java";
  message: string;
  current?: number;
  total?: number;
}

export interface PreparedGame {
  versionId: string;
  javaPath: string;
}

const OFFICIAL_VERSION_MANIFEST = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
const OFFICIAL_RUNTIME_MANIFEST = "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";
const DOWNLOAD_HOSTS = new Set([
  "launchermeta.mojang.com",
  "launcher.mojang.com",
  "piston-meta.mojang.com",
  "piston-data.mojang.com",
  "libraries.minecraft.net",
  "resources.download.minecraft.net",
  "meta.fabricmc.net",
  "maven.fabricmc.net",
  "repo1.maven.org",
  "repo.maven.apache.org",
]);

function officialFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
  const raw = typeof input === "string" || input instanceof URL ? input.toString() : input.url;
  const url = new URL(raw);
  if (url.protocol !== "https:" || !DOWNLOAD_HOSTS.has(url.hostname) || url.username || url.password) {
    return Promise.reject(new Error(`Hôte d'installation Minecraft non autorisé: ${url.hostname}`));
  }
  return fetch(url, { ...init, redirect: "follow" }).then((response) => {
    const finalUrl = new URL(response.url);
    if (finalUrl.protocol !== "https:" || !DOWNLOAD_HOSTS.has(finalUrl.hostname)) {
      throw new Error(`Redirection d'installation non autorisée: ${finalUrl.hostname}`);
    }
    return response;
  });
}

function validateInstallFiles(files: InstallFile[]): void {
  for (const file of files) {
    if (file.urls.length === 0) throw new Error(`Aucune source pour ${file.path}`);
    for (const raw of file.urls) {
      const url = new URL(raw);
      if (url.protocol !== "https:" || !DOWNLOAD_HOSTS.has(url.hostname) || url.username || url.password) {
        throw new Error(`Source de fichier Minecraft non autorisée: ${url.hostname}`);
      }
    }
  }
}

async function executeFiles(
  id: string,
  files: InstallFile[],
  runtime: ReturnType<typeof createDefaultNodeInstallRuntime>,
  signal?: AbortSignal,
): Promise<void> {
  if (files.length === 0) return;
  await executeInstallManifest({ schemaVersion: 1, tasks: [{ id, type: "files", files }] }, runtime, { signal });
}

async function exists(path: string): Promise<boolean> {
  try { await access(path); return true; } catch { return false; }
}

function javaExecutable(destination: string): string {
  return join(destination, "bin", process.platform === "win32" ? "java.exe" : "java");
}

async function selectMojangRuntime(component: string, signal?: AbortSignal): Promise<JavaRuntimeTarget> {
  const response = await officialFetch(OFFICIAL_RUNTIME_MANIFEST, { signal });
  if (!response.ok) throw new Error(`Index Java Mojang indisponible (${response.status})`);
  const index = await response.json() as JavaRuntimes;
  const platform = process.platform === "win32"
    ? (process.arch === "arm64" ? "windows-arm64" : "windows-x64")
    : process.platform === "darwin"
      ? (process.arch === "arm64" ? "mac-os-arm64" : "mac-os")
      : "linux";
  const target = index[platform][component]?.[0];
  if (!target) throw new Error(`Runtime Mojang '${component}' absent pour ${platform}`);
  return target;
}

export class MinecraftManager {
  private running: ChildProcess | undefined;

  constructor(
    private readonly config: LauncherConfig,
    private readonly paths: LauncherPaths,
    private readonly logger: LauncherLogger,
  ) {}

  get isRunning(): boolean { return this.running !== undefined; }

  async prepare(
    release: ReleaseManifest,
    onProgress: (progress: GameInstallProgress) => void,
    signal?: AbortSignal,
  ): Promise<PreparedGame> {
    if (release.payload.game.loader.id !== "fabric") throw new Error("Seul Fabric est pris en charge par ce launcher");
    await Promise.all([
      mkdir(this.paths.minecraft, { recursive: true, mode: 0o700 }),
      mkdir(this.paths.instance, { recursive: true, mode: 0o700 }),
      mkdir(this.paths.runtime, { recursive: true, mode: 0o700 }),
    ]);
    const minecraft = MinecraftFolder.from(this.paths.minecraft);
    const baseDownload = createFileTransferInstallDownload({ signal, maxConcurrency: 8 });
    const runtime = createDefaultNodeInstallRuntime({
      signal,
      maxConcurrency: 8,
      download: async (files) => {
        validateInstallFiles(files);
        await baseDownload(files);
      },
    });

    onProgress({ stage: "minecraft", message: `Préparation de Minecraft ${release.payload.game.minecraft}` });
    const versions = await getVersionList({ fetch: officialFetch, signal, remote: OFFICIAL_VERSION_MANIFEST });
    const base = versions.versions.find((entry) => entry.id === release.payload.game.minecraft);
    if (!base) throw new Error(`Minecraft ${release.payload.game.minecraft} est absent du manifeste officiel`);
    await executeFiles(
      "minecraft-version-json",
      [resolveMinecraftVersionJsonInstallFile(base, minecraft, { signal })],
      runtime,
      signal,
    );
    let resolved = await Version.parse(minecraft, base.id);
    const jar = resolveMinecraftJarInstallFile(resolved, { side: "client", signal });
    await executeFiles(
      "minecraft-core",
      [...(jar ? [jar] : []), ...resolveLibraryInstallFiles(resolved.libraries, minecraft, { signal, strict: true })],
      runtime,
      signal,
    );
    await executeInstallManifest(resolveAssetMetadataInstallManifest(resolved, minecraft, { abortSignal: signal, fetch: officialFetch }), runtime, { signal });
    await executeFiles(
      "minecraft-assets",
      await resolveAssetObjectInstallFiles(resolved, minecraft, { abortSignal: signal, fetch: officialFetch, strict: true }),
      runtime,
      signal,
    );

    onProgress({ stage: "fabric", message: `Installation de Fabric ${release.payload.game.loader.version}` });
    const versionId = await executeInstallWorkflow(createFabricInstallWorkflow({
      minecraftVersion: release.payload.game.minecraft,
      version: release.payload.game.loader.version,
      minecraft,
      side: "client",
      fetch: officialFetch,
      signal,
    }), runtime, { signal });
    resolved = await Version.parse(minecraft, versionId);
    await executeFiles(
      "fabric-libraries",
      resolveLibraryInstallFiles(resolved.libraries, minecraft, { signal, strict: true }),
      runtime,
      signal,
    );

    const component = resolved.javaVersion.component;
    const runtimeDestination = join(this.paths.runtime, component.replace(/[^a-zA-Z0-9._-]/g, "_"));
    const executable = javaExecutable(runtimeDestination);
    let java = await resolveJava(executable);
    if (!java || java.majorVersion < release.payload.game.javaMajor) {
      onProgress({ stage: "java", message: `Installation du Java officiel requis (${release.payload.game.javaMajor})` });
      const target = await selectMojangRuntime(component, signal);
      await executeInstallWorkflow(createJavaRuntimeInstallWorkflow({ target, destination: runtimeDestination }), runtime, { signal });
      java = await resolveJava(executable);
    }
    if (!java || java.majorVersion < release.payload.game.javaMajor) {
      throw new Error(`Runtime Java ${release.payload.game.javaMajor}+ introuvable après installation`);
    }
    await this.logger.info("Minecraft prêt", { versionId, javaMajor: java.majorVersion });
    return { versionId, javaPath: executable };
  }

  async launch(
    prepared: PreparedGame,
    session: MinecraftSession,
    settings: LauncherSettings,
    onExit: (code: number | null, signal: NodeJS.Signals | null) => void,
  ): Promise<ChildProcess> {
    if (this.running) throw new Error("Minecraft est déjà lancé");
    const gameLogPath = join(this.paths.logs, "minecraft.log");
    const gameLog = await open(gameLogPath, "a", 0o600);
    await gameLog.write(`\n${new Date().toISOString()} --- Démarrage Minecraft ---\n`);
    const child = await launch({
      gameProfile: { id: session.profile.id, name: session.profile.name },
      accessToken: session.accessToken,
      userType: "mojang",
      launcherName: "BG LANDS Launcher",
      launcherBrand: "bglands",
      gamePath: this.paths.instance,
      resourcePath: this.paths.minecraft,
      javaPath: prepared.javaPath,
      minMemory: settings.minimumMemoryMb,
      maxMemory: settings.maximumMemoryMb,
      version: prepared.versionId,
      quickPlayMultiplayer: settings.autoConnect
        ? `${this.config.server.host}:${this.config.server.port}`
        : undefined,
      resolution: { width: settings.width, height: settings.height },
      extraJVMArgs: [
        "-XX:+UseG1GC",
        "-Dfile.encoding=UTF-8",
        "-Dbglands.launcher=true",
        ...Object.entries(settings.modules).map(([name, enabled]) =>
          `-Dbglands.module.${name}=${enabled ? "true" : "false"}`,
        ),
      ],
      extraExecOption: {
        cwd: this.paths.instance,
        windowsHide: true,
        stdio: ["ignore", "pipe", "pipe"],
      },
    });
    this.running = child;
    const writeOutput = (chunk: Buffer | string): void => {
      void gameLog.write(redact(chunk.toString()));
    };
    child.stdout?.on("data", writeOutput);
    child.stderr?.on("data", writeOutput);
    child.once("error", (error) => {
      void this.logger.error("Minecraft n'a pas pu démarrer", error);
    });
    child.once("exit", (code, exitSignal) => {
      this.running = undefined;
      void gameLog.write(`${new Date().toISOString()} --- Arrêt (${code ?? "?"}/${exitSignal ?? "-"}) ---\n`).finally(() => gameLog.close());
      onExit(code, exitSignal);
    });
    await this.logger.info("Processus Minecraft lancé", { pid: child.pid, profile: session.profile.name });
    return child;
  }
}
