import fs from "node:fs";
import path from "node:path";
import type { Game, SystemId } from "@/lib/types";
import gamesFallback from "../../../data/games.json";
import userGamesFallback from "../../../data/user-games.json";

const root = process.cwd();
const gamesPath = path.join(/* turbopackIgnore: true */ root, "data", "games.json");
const userPath = path.join(/* turbopackIgnore: true */ root, "data", "user-games.json");
const romsRoot = path.join(/* turbopackIgnore: true */ root, "public", "roms");
const coversRoot = path.join(/* turbopackIgnore: true */ root, "public", "covers");

/**
 * Vercel/serverless serves `public/` from the CDN — the Node function often
 * cannot `fs.existsSync` those ROM files. Skip disk filtering there and trust
 * the catalog (seed ROMs still play via static URLs).
 */
function canCheckRomDisk(): boolean {
  if (process.env.VERCEL) return false;
  if (process.env.AWS_LAMBDA_FUNCTION_NAME) return false;
  if (process.env.HOLLOWCADE_SKIP_ROM_DISK_CHECK === "1") return false;
  return true;
}

export const EXT_TO_SYSTEM: Record<string, SystemId> = {
  ".nes": "nes",
  ".sfc": "snes",
  ".smc": "snes",
  ".gb": "gb",
  ".gbc": "gbc",
  ".gba": "gba",
  ".md": "genesis",
  ".gen": "genesis",
  ".n64": "n64",
  ".z64": "n64",
  ".v64": "n64",
  ".nds": "nds",
  ".iso": "psp",
  ".cso": "psp",
  ".pbp": "psp",
  ".sms": "sms",
  ".gg": "gg",
  ".a26": "atari2600",
  ".vb": "vb",
  ".chd": "psx",
  ".cue": "psx",
  ".bin": "psx",
  ".zip": "arcade",
};

/** Soft cap to keep uploads from blowing the disk (override via env). */
export function maxUploadBytes(): number {
  const raw = process.env.HOLLOWCADE_MAX_UPLOAD_MB;
  const mb = raw ? Number(raw) : 256;
  return (Number.isFinite(mb) && mb > 0 ? mb : 256) * 1024 * 1024;
}

export function slugify(name: string): string {
  return name
    .replace(/\.[^.]+$/, "")
    .replace(/\(.*?\)/g, "")
    .replace(/\[.*?\]/g, "")
    .replace(/[^a-zA-Z0-9]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-|-$/g, "")
    .toLowerCase()
    .slice(0, 80);
}

function readGamesFile(filePath: string, fallback: Game[]): Game[] {
  try {
    if (fs.existsSync(/* turbopackIgnore: true */ filePath)) {
      return JSON.parse(
        fs.readFileSync(/* turbopackIgnore: true */ filePath, "utf8"),
      ) as Game[];
    }
  } catch {
    /* fall through to bundled JSON */
  }
  return fallback;
}

export function romPathOnDisk(romUrl: string): string | null {
  if (!romUrl.startsWith("/roms/")) return null;
  const rel = romUrl.replace(/^\//, "");
  return path.join(/* turbopackIgnore: true */ root, "public", rel);
}

export function romExists(romUrl: string): boolean {
  if (!canCheckRomDisk()) return true;
  const abs = romPathOnDisk(romUrl);
  return Boolean(abs && fs.existsSync(/* turbopackIgnore: true */ abs));
}

export function filterPlayable(games: Game[]): Game[] {
  if (!canCheckRomDisk()) return games;
  return games.filter((g) => romExists(g.rom));
}

export function loadSharedGames(playableOnly = true): Game[] {
  const list = readGamesFile(gamesPath, gamesFallback as Game[]);
  return playableOnly ? filterPlayable(list) : list;
}

export function loadLocalGames(playableOnly = true): Game[] {
  const list = readGamesFile(userPath, userGamesFallback as Game[]);
  return playableOnly ? filterPlayable(list) : list;
}

export function loadAllGames(playableOnly = true): Game[] {
  const local = loadLocalGames(playableOnly);
  const shared = loadSharedGames(playableOnly);
  const seen = new Set<string>();
  const out: Game[] = [];
  for (const g of [...local, ...shared]) {
    if (seen.has(g.slug)) continue;
    seen.add(g.slug);
    out.push(g);
  }
  return out;
}

export function getGameBySlugServer(
  slug: string,
  playableOnly = true,
): Game | undefined {
  return loadAllGames(playableOnly).find((g) => g.slug === slug);
}

function placeholderCover(slug: string, title: string): string {
  const png = path.join(coversRoot, `${slug}.png`);
  if (fs.existsSync(png)) return `/covers/${slug}.png`;
  const dest = path.join(coversRoot, `${slug}.svg`);
  if (fs.existsSync(dest)) return `/covers/${slug}.svg`;
  const safe = title.replace(/[<>&]/g, "");
  fs.mkdirSync(coversRoot, { recursive: true });
  fs.writeFileSync(
    dest,
    `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400">
  <rect width="400" height="400" fill="#1c1814"/>
  <text x="24" y="360" fill="#f0a030" font-family="monospace" font-size="18">${safe.slice(0, 28)}</text>
</svg>`,
  );
  return `/covers/${slug}.svg`;
}

function writeSharedGames(games: Game[]) {
  fs.mkdirSync(path.dirname(gamesPath), { recursive: true });
  fs.writeFileSync(gamesPath, JSON.stringify(games, null, 2) + "\n");
}

function writeLocalGames(games: Game[]) {
  fs.mkdirSync(path.dirname(userPath), { recursive: true });
  fs.writeFileSync(userPath, JSON.stringify(games, null, 2) + "\n");
}

export type ContributeInput = {
  title?: string;
  system?: SystemId;
  slug?: string;
  author?: string;
  description?: string;
  license?: string;
  sourceUrl?: string;
  featured?: boolean;
  year?: number;
  /** Original filename used for extension + default title */
  fileName: string;
  /** ROM bytes */
  buffer: Buffer;
};

export type ContributeResult =
  | { ok: true; game: Game }
  | { ok: false; error: string };

/**
 * Persist a ROM under public/roms and append to the shared catalog.
 * No copyright / legal gate — host and uploader are responsible.
 */
export function contributeSharedRom(input: ContributeInput): ContributeResult {
  const ext = path.extname(input.fileName).toLowerCase();
  if (!ext || ext.length > 8) {
    return { ok: false, error: "ROM file needs a recognizable extension." };
  }

  const system =
    input.system ||
    EXT_TO_SYSTEM[ext] ||
    (undefined as SystemId | undefined);
  if (!system) {
    return {
      ok: false,
      error: "Could not detect system — pick one manually.",
    };
  }

  const title =
    input.title?.trim() ||
    input.fileName.replace(/\.[^.]+$/, "") ||
    "Untitled";
  let slug = (input.slug?.trim() || slugify(title)).slice(0, 80);
  if (!slug) slug = slugify(input.fileName) || `game-${Date.now()}`;

  const shared = readGamesFile(gamesPath, gamesFallback as Game[]);
  if (shared.some((g) => g.slug === slug)) {
    return { ok: false, error: `Slug already in shared catalog: ${slug}` };
  }

  if (input.buffer.byteLength > maxUploadBytes()) {
    return {
      ok: false,
      error: `File too large (max ${maxUploadBytes() / (1024 * 1024)} MB).`,
    };
  }

  const safeBase = path
    .basename(input.fileName)
    .replace(/[^\w.\- ()[\]]+/g, "_")
    .slice(0, 180);
  const destDir = path.join(/* turbopackIgnore: true */ romsRoot, system);
  fs.mkdirSync(destDir, { recursive: true });
  const dest = path.join(/* turbopackIgnore: true */ destDir, safeBase);
  fs.writeFileSync(/* turbopackIgnore: true */ dest, input.buffer);

  const romRel = `/roms/${system}/${safeBase}`.replaceAll("\\", "/");
  const cover = placeholderCover(slug, title);
  const entry: Game = {
    slug,
    title,
    system,
    description:
      input.description?.trim() ||
      `${title} — added to the HollowCade shared catalog.`,
    rom: romRel,
    cover,
    featured: Boolean(input.featured),
    legal: false,
  };
  if (input.author?.trim()) entry.author = input.author.trim();
  if (input.license?.trim()) entry.license = input.license.trim();
  if (input.sourceUrl?.trim()) entry.sourceUrl = input.sourceUrl.trim();
  if (input.year && Number.isFinite(input.year)) entry.year = input.year;

  shared.push(entry);
  writeSharedGames(shared);
  return { ok: true, game: entry };
}

/** Promote a user-games entry into the shared catalog (keeps ROM path). */
export function promoteLocalToShared(opts: {
  fromUser: string;
  title?: string;
  license?: string;
  sourceUrl?: string;
  author?: string;
  description?: string;
  featured?: boolean;
}): ContributeResult {
  const local = readGamesFile(userPath, userGamesFallback as Game[]);
  const idx = local.findIndex((g) => g.slug === opts.fromUser);
  if (idx < 0) {
    return { ok: false, error: `No local entry with slug "${opts.fromUser}"` };
  }
  const entry = local[idx];
  const shared = readGamesFile(gamesPath, gamesFallback as Game[]);
  if (shared.some((g) => g.slug === entry.slug)) {
    return { ok: false, error: `Slug already in shared catalog: ${entry.slug}` };
  }
  if (!romExists(entry.rom)) {
    return {
      ok: false,
      error: `ROM file missing on disk for ${entry.rom}`,
    };
  }

  const promoted: Game = {
    ...entry,
    title: opts.title?.trim() || entry.title,
    description:
      opts.description?.trim() ||
      entry.description ||
      `${entry.title} — added to the HollowCade shared catalog.`,
    license: opts.license?.trim() || entry.license,
    sourceUrl: opts.sourceUrl?.trim() || entry.sourceUrl,
    author: opts.author?.trim() || entry.author,
    featured: opts.featured ?? entry.featured,
    legal: false,
  };
  for (const k of Object.keys(promoted) as (keyof Game)[]) {
    if (promoted[k] === undefined) delete promoted[k];
  }

  local.splice(idx, 1);
  writeLocalGames(local);
  shared.push(promoted);
  writeSharedGames(shared);
  return { ok: true, game: promoted };
}

const VALID_SYSTEM_FOLDERS = new Set<string>([
  "nes",
  "snes",
  "gb",
  "gbc",
  "gba",
  "genesis",
  "n64",
  "psx",
  "arcade",
  "nds",
  "psp",
  "sms",
  "gg",
  "saturn",
  "segacd",
  "atari2600",
  "vb",
]);

function walkRomFiles(dir: string, out: string[] = []): string[] {
  if (!fs.existsSync(/* turbopackIgnore: true */ dir)) return out;
  for (const ent of fs.readdirSync(/* turbopackIgnore: true */ dir, {
    withFileTypes: true,
  })) {
    const p = path.join(dir, ent.name);
    if (ent.isDirectory()) walkRomFiles(p, out);
    else if (!/^(readme\.md|\.gitkeep)$/i.test(ent.name)) out.push(p);
  }
  return out;
}

/**
 * Scan public/roms for files missing from both catalogs; append to user-games.json.
 * Same behavior as `npm run scan-roms`, runnable inside the Docker app process.
 */
export function scanRomsToLocalCatalog(): {
  scanned: number;
  added: Game[];
  skippedCueBins: number;
} {
  const shared = readGamesFile(gamesPath, gamesFallback as Game[]);
  const local = readGamesFile(userPath, userGamesFallback as Game[]);
  const knownRoms = new Set(
    [...shared, ...local].map((g) => decodeURIComponent(g.rom)),
  );
  const knownSlugs = new Set([...shared, ...local].map((g) => g.slug));
  const files = walkRomFiles(romsRoot);
  const added: Game[] = [];
  let skippedCueBins = 0;

  for (const file of files) {
    const rel =
      "/" +
      path
        .relative(path.join(/* turbopackIgnore: true */ root, "public"), file)
        .replaceAll("\\", "/");
    if (knownRoms.has(rel)) continue;

    const ext = path.extname(file).toLowerCase();
    const base = path.basename(file);
    const folder = path.basename(path.dirname(file)).toLowerCase();

    if (ext === ".bin") {
      const cue = file.replace(/\.bin$/i, ".cue");
      if (fs.existsSync(/* turbopackIgnore: true */ cue)) {
        skippedCueBins += 1;
        continue;
      }
      // Auto-create .cue so PCSX/Sega CD can load; catalog the .cue path
      if (
        folder === "psx" ||
        folder === "segacd" ||
        folder === "saturn" ||
        EXT_TO_SYSTEM[ext] === "psx"
      ) {
        ensureCueForBin(file);
        // Fall through using the new .cue as the catalog entry
        const cueRel =
          "/" +
          path
            .relative(
              path.join(/* turbopackIgnore: true */ root, "public"),
              file.replace(/\.bin$/i, ".cue"),
            )
            .replaceAll("\\", "/");
        if (knownRoms.has(cueRel)) continue;
        const cueBase = path.basename(file.replace(/\.bin$/i, ".cue"));
        const system = (
          (VALID_SYSTEM_FOLDERS.has(folder) ? folder : null) ||
          "psx"
        ) as SystemId;
        let slug = slugify(cueBase);
        if (knownSlugs.has(slug)) slug = `${slug}-${system}`;
        if (knownSlugs.has(slug)) continue;
        const title =
          cueBase
            .replace(/\.[^.]+$/, "")
            .replace(/\s*\(.*?\)\s*/g, " ")
            .replace(/\s*\[.*?\]\s*/g, " ")
            .replace(/\s+/g, " ")
            .trim() || slug;
        const entry: Game = {
          slug,
          title,
          system,
          description: `Local ROM: ${cueBase} (+ bin). Added by HollowCade scan.`,
          rom: cueRel,
          cover: placeholderCover(slug, title),
          featured: false,
          legal: false,
        };
        local.push(entry);
        knownRoms.add(cueRel);
        knownRoms.add(rel);
        knownSlugs.add(slug);
        added.push(entry);
        continue;
      }
    }

    const system = (
      (VALID_SYSTEM_FOLDERS.has(folder) ? folder : null) ||
      EXT_TO_SYSTEM[ext] ||
      folder
    ) as SystemId;
    if (!VALID_SYSTEM_FOLDERS.has(system)) continue;

    let slug = slugify(base);
    if (knownSlugs.has(slug)) slug = `${slug}-${system}`;
    if (knownSlugs.has(slug)) continue;

    const title =
      base
        .replace(/\.[^.]+$/, "")
        .replace(/\s*\(.*?\)\s*/g, " ")
        .replace(/\s*\[.*?\]\s*/g, " ")
        .replace(/\s+/g, " ")
        .trim() || slug;

    const entry: Game = {
      slug,
      title,
      system,
      description: `Local ROM: ${base}. Added by HollowCade scan.`,
      rom: rel,
      cover: placeholderCover(slug, title),
      featured: false,
      legal: false,
    };
    local.push(entry);
    knownRoms.add(rel);
    knownSlugs.add(slug);
    added.push(entry);
  }

  if (added.length) writeLocalGames(local);
  return { scanned: files.length, added, skippedCueBins };
}

/**
 * PCSX-ReARMed needs a .cue that points at the .bin. Loading a raw .bin alone
 * usually dumps you into the RetroArch menu (PCSX-ReARMed banner).
 * Prefer an existing sibling .cue, or write a minimal one next to the .bin.
 */
export function ensureCueForBin(binAbs: string): string {
  const cueAbs = binAbs.replace(/\.bin$/i, ".cue");
  if (fs.existsSync(/* turbopackIgnore: true */ cueAbs)) return cueAbs;

  const size = fs.statSync(/* turbopackIgnore: true */ binAbs).size;
  const mode =
    size % 2352 === 0
      ? "MODE2/2352"
      : size % 2048 === 0
        ? "MODE1/2048"
        : "MODE2/2352";
  const binName = path.basename(binAbs);
  const body = `FILE "${binName}" BINARY\n  TRACK 01 ${mode}\n    INDEX 01 00:00:00\n`;
  fs.writeFileSync(/* turbopackIgnore: true */ cueAbs, body, "utf8");
  return cueAbs;
}

/**
 * Rewrite catalog ROM path into what EmulatorJS should actually load.
 * Disc images (.bin) → .cue companion.
 */
export function resolvePlayableRom(game: Game): string {
  const rom = game.rom;
  if (!rom.startsWith("/roms/")) return rom;

  const abs = romPathOnDisk(rom);
  if (!abs || !fs.existsSync(/* turbopackIgnore: true */ abs)) return rom;

  const ext = path.extname(abs).toLowerCase();
  if (ext === ".bin" && (game.system === "psx" || game.system === "segacd" || game.system === "saturn")) {
    const cueAbs = ensureCueForBin(abs);
    const rel =
      "/" +
      path
        .relative(path.join(/* turbopackIgnore: true */ root, "public"), cueAbs)
        .replaceAll("\\", "/");
    return rel;
  }

  if (ext === ".cue") {
    // Ensure referenced .bin exists; if catalog pointed at cue already, fine
    const binAbs = abs.replace(/\.cue$/i, ".bin");
    if (!fs.existsSync(/* turbopackIgnore: true */ binAbs)) {
      // leave cue; EmulatorJS will fail clearly
      return rom;
    }
  }

  return rom;
}

const PSX_BIOS_CANDIDATES = [
  "scph5501.bin", // US
  "scph5502.bin", // EU
  "scph5500.bin", // JP
  "PSXONPSP660.bin",
  "scph7001.bin",
  "scph101.bin",
  "scph1001.bin",
];

/**
 * Optional real PSX BIOS for better compatibility (place under public/bios/).
 * Matches common names case-insensitively; any scph*.bin also works.
 */
export function resolveBiosUrl(system: SystemId): string | null {
  if (system !== "psx") return null;
  const biosRoot = path.join(/* turbopackIgnore: true */ root, "public", "bios");
  if (!fs.existsSync(/* turbopackIgnore: true */ biosRoot)) return null;

  const files = fs.readdirSync(/* turbopackIgnore: true */ biosRoot);
  const lower = new Map(files.map((f) => [f.toLowerCase(), f]));

  for (const name of PSX_BIOS_CANDIDATES) {
    const hit = lower.get(name.toLowerCase());
    if (hit) return `/api/bios/${encodeURIComponent(hit)}`;
  }

  // Any scph*.bin / psxonpsp*
  for (const f of files) {
    const l = f.toLowerCase();
    if (l.endsWith(".bin") && (l.startsWith("scph") || l.startsWith("psxonpsp"))) {
      return `/api/bios/${encodeURIComponent(f)}`;
    }
  }
  return null;
}

/** Debug helper — what the play page will use for a game. */
export function describePlayAssets(game: Game): {
  catalogRom: string;
  playRom: string;
  biosUrl: string | null;
  binExists: boolean;
  cueExists: boolean;
  biosFiles: string[];
} {
  const playRom = resolvePlayableRom(game);
  const abs = romPathOnDisk(playRom.endsWith(".cue") ? playRom : game.rom);
  const binAbs = abs
    ? abs.replace(/\.cue$/i, ".bin").replace(/\.bin$/i, ".bin")
    : null;
  const cueAbs = abs ? abs.replace(/\.bin$/i, ".cue") : null;
  const biosRoot = path.join(/* turbopackIgnore: true */ root, "public", "bios");
  let biosFiles: string[] = [];
  try {
    biosFiles = fs.readdirSync(/* turbopackIgnore: true */ biosRoot).filter(
      (f) => !f.startsWith(".") && f !== "README.md",
    );
  } catch {
    biosFiles = [];
  }
  return {
    catalogRom: game.rom,
    playRom,
    biosUrl: resolveBiosUrl(game.system),
    binExists: Boolean(binAbs && fs.existsSync(binAbs)),
    cueExists: Boolean(cueAbs && fs.existsSync(cueAbs)),
    biosFiles,
  };
}

/** Whether contribute writes are allowed on this deploy. */
export function contributeWriteAllowed(providedToken: string | null): {
  ok: boolean;
  error?: string;
} {
  const token = process.env.HOLLOWCADE_UPLOAD_TOKEN;
  const open = process.env.HOLLOWCADE_OPEN_CONTRIBUTE === "1";

  if (token) {
    if (providedToken !== token) {
      return { ok: false, error: "Invalid or missing upload token." };
    }
    return { ok: true };
  }

  if (process.env.NODE_ENV === "production" && !open) {
    return {
      ok: false,
      error:
        "Uploads disabled. Set HOLLOWCADE_UPLOAD_TOKEN or HOLLOWCADE_OPEN_CONTRIBUTE=1 on a host with persistent disk.",
    };
  }

  return { ok: true };
}
