import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
import { mkdirSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { DatabaseSync } from "node:sqlite";

const SESSION_LIFETIME_MS = 30 * 24 * 60 * 60 * 1000;
const MAX_JSON_BYTES = 1024 * 1024;
const AUTH_WINDOW_MS = 15 * 60 * 1000;
const AUTH_ATTEMPTS_PER_WINDOW = 20;
const authAttempts = new Map();

function apiError(message, status = 400) {
  const error = new Error(message);
  error.status = status;
  return error;
}

function sendJson(response, status, body) {
  response.statusCode = status;
  response.setHeader("Content-Type", "application/json; charset=utf-8");
  response.setHeader("Cache-Control", "no-store");
  response.setHeader("X-Content-Type-Options", "nosniff");
  response.end(JSON.stringify(body));
}

function configuredCorsOrigins() {
  return String(process.env.CORS_ORIGINS ?? "")
    .split(",")
    .map((origin) => origin.trim())
    .filter(Boolean);
}

function setCorsHeaders(request, response) {
  const origin = request.headers.origin;
  if (!origin) return;
  const configured = configuredCorsOrigins();
  if (!configured.includes("*") && !configured.includes(origin)) return;
  response.setHeader("Access-Control-Allow-Origin", origin);
  response.setHeader("Access-Control-Allow-Headers", "Authorization,Content-Type");
  response.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
  response.setHeader("Access-Control-Max-Age", "86400");
  response.setHeader("Vary", "Origin");
}

async function readJson(request) {
  const chunks = [];
  let size = 0;
  for await (const chunk of request) {
    size += chunk.length;
    if (size > MAX_JSON_BYTES) throw apiError("Request body is too large.", 413);
    chunks.push(chunk);
  }
  try {
    return JSON.parse(Buffer.concat(chunks).toString("utf8"));
  } catch {
    throw apiError("Request body must be valid JSON.");
  }
}

function canonicalUsername(value) {
  return String(value ?? "").trim().toLocaleLowerCase();
}

function validateUsername(value) {
  const username = String(value ?? "").trim();
  if (!/^[A-Za-z0-9_]{3,20}$/.test(username)) {
    throw apiError("Username must be 3–20 letters, numbers, or underscores.");
  }
  return username;
}

function validatePassword(value) {
  const password = String(value ?? "");
  if (password.length < 10 || password.length > 128) {
    throw apiError("Password must be 10–128 characters.");
  }
  return password;
}

function passwordDigest(password, salt) {
  return scryptSync(password, salt, 64).toString("hex");
}

function verifyPassword(password, account) {
  const actual = Buffer.from(passwordDigest(password, account.passwordSalt), "hex");
  const expected = Buffer.from(account.passwordHash, "hex");
  return actual.length === expected.length && timingSafeEqual(actual, expected);
}

function tokenHash(token) {
  return createHash("sha256").update(token).digest("hex");
}

function bearerToken(request) {
  const authorization = String(request.headers.authorization ?? "");
  return authorization.startsWith("Bearer ") ? authorization.slice(7).trim() : "";
}

function createSession(database, accountId) {
  const token = randomBytes(32).toString("base64url");
  const expiresAt = new Date(Date.now() + SESSION_LIFETIME_MS).toISOString();
  database.prepare(`
    INSERT INTO sessions (account_id, token_hash, expires_at)
    VALUES (?, ?, ?)
  `).run(accountId, tokenHash(token), expiresAt);
  return token;
}

function currentSession(database, request) {
  const token = bearerToken(request);
  if (!token) return null;
  return database.prepare(`
    SELECT accounts.id AS accountId, accounts.username
    FROM sessions
    JOIN accounts ON accounts.id = sessions.account_id
    WHERE sessions.token_hash = ? AND sessions.expires_at > CURRENT_TIMESTAMP
  `).get(tokenHash(token)) ?? null;
}

function requireSession(database, request) {
  const session = currentSession(database, request);
  if (!session) throw apiError("Sign in required.", 401);
  return session;
}

function clientAddress(request) {
  return request.socket?.remoteAddress ?? "unknown";
}

function enforceAuthRateLimit(request) {
  const now = Date.now();
  const key = clientAddress(request);
  const existing = authAttempts.get(key);
  const bucket = existing && now - existing.startedAt < AUTH_WINDOW_MS
    ? existing
    : { startedAt: now, count: 0 };
  bucket.count += 1;
  authAttempts.set(key, bucket);
  if (bucket.count > AUTH_ATTEMPTS_PER_WINDOW) {
    throw apiError("Too many authentication attempts. Try again later.", 429);
  }
}

function register(database, payload) {
  const username = validateUsername(payload?.username);
  const password = validatePassword(payload?.password);
  const canonical = canonicalUsername(username);
  if (database.prepare("SELECT id FROM accounts WHERE canonical_username = ?").get(canonical)) {
    throw apiError("Account already exists.", 409);
  }
  const salt = randomBytes(16).toString("hex");
  const result = database.prepare(`
    INSERT INTO accounts (username, canonical_username, password_hash, password_salt)
    VALUES (?, ?, ?, ?)
  `).run(username, canonical, passwordDigest(password, salt), salt);
  const accountId = Number(result.lastInsertRowid);
  return { account: { id: accountId, username }, token: createSession(database, accountId) };
}

function login(database, payload) {
  const canonical = canonicalUsername(payload?.username);
  const password = String(payload?.password ?? "");
  const account = database.prepare(`
    SELECT id, username, password_hash AS passwordHash, password_salt AS passwordSalt
    FROM accounts WHERE canonical_username = ?
  `).get(canonical);
  if (!account || !verifyPassword(password, account)) {
    throw apiError("Username or password is incorrect.", 401);
  }
  return {
    account: { id: account.id, username: account.username },
    token: createSession(database, account.id),
  };
}

function validateSlotId(value) {
  const slotId = Number(value);
  if (!Number.isInteger(slotId) || slotId < 1 || slotId > 3) throw apiError("Invalid save slot.");
  return slotId;
}

function validateSave(value, slotId) {
  const schemaVersion = Number(value?.schemaVersion);
  if (!value || typeof value !== "object" || schemaVersion !== 5 && schemaVersion !== 6) {
    throw apiError("Save snapshot is invalid.");
  }
  if (Number(value.slotId) !== slotId) throw apiError("Save slot does not match request.");
  if (typeof value.hunterName !== "string" || !value.hunterName.trim()) {
    throw apiError("Save snapshot has no hunter name.");
  }
  return { ...value, schemaVersion: 6 };
}

function normalizeNonNegativeInteger(value) {
  const number = Math.floor(Number(value));
  return Number.isFinite(number) ? Math.max(0, number) : 0;
}

function normalizeNonNegativeNumber(value) {
  const number = Number(value);
  return Number.isFinite(number) ? Math.max(0, number) : 0;
}

function mergeLeaderboardHighWater(database, accountId, slotId, save) {
  const blockbreaker = database.prepare(`
    SELECT highest_bricks AS highestBricks, longest_seconds AS longestSeconds, highest_score AS highestScore
    FROM blockbreaker_records WHERE account_id = ? AND slot_id = ?
  `).get(accountId, slotId);
  const aether = database.prepare(`
    SELECT highest_score AS highestScore, wave_at_best AS waveAtBest, duration_at_best AS durationAtBest
    FROM aether_assault_records WHERE account_id = ? AND slot_id = ?
  `).get(accountId, slotId);
  const stats = save.stats && typeof save.stats === "object" ? save.stats : {};
  const candidateAether = {
    score: normalizeNonNegativeInteger(stats.highestAetherAssaultScore),
    wave: normalizeNonNegativeInteger(stats.highestAetherAssaultWaveAtBest),
    duration: normalizeNonNegativeNumber(stats.longestAetherAssaultSecondsAtBest),
  };
  const storedAether = {
    score: normalizeNonNegativeInteger(aether?.highestScore),
    wave: normalizeNonNegativeInteger(aether?.waveAtBest),
    duration: normalizeNonNegativeNumber(aether?.durationAtBest),
  };
  const aetherRecord = candidateAether.score > storedAether.score
    || candidateAether.score === storedAether.score && candidateAether.wave > storedAether.wave
    || candidateAether.score === storedAether.score && candidateAether.wave === storedAether.wave && candidateAether.duration > storedAether.duration
    ? candidateAether
    : storedAether;
  return {
    ...save,
    schemaVersion: 6,
    stats: {
      ...stats,
      highestBlockbreakerBricks: Math.max(
        normalizeNonNegativeInteger(stats.highestBlockbreakerBricks),
        normalizeNonNegativeInteger(blockbreaker?.highestBricks),
      ),
      longestBlockbreakerSeconds: Math.max(
        normalizeNonNegativeNumber(stats.longestBlockbreakerSeconds),
        normalizeNonNegativeNumber(blockbreaker?.longestSeconds),
      ),
      highestBlockbreakerScore: Math.max(
        normalizeNonNegativeInteger(stats.highestBlockbreakerScore),
        normalizeNonNegativeInteger(blockbreaker?.highestScore),
      ),
      highestAetherAssaultScore: aetherRecord.score,
      highestAetherAssaultWaveAtBest: aetherRecord.wave,
      longestAetherAssaultSecondsAtBest: aetherRecord.duration,
    },
  };
}

function syncLeaderboardStats(database, accountId, slotId, save) {
  database.prepare("DELETE FROM boss_kill_records WHERE account_id = ? AND slot_id = ?").run(accountId, slotId);
  const insertBoss = database.prepare(`
    INSERT INTO boss_kill_records (account_id, slot_id, boss_id, kills, updated_at)
    VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
  `);
  const bossKills = save.stats?.bossKills && typeof save.stats.bossKills === "object"
    ? save.stats.bossKills
    : {};
  for (const [bossId, rawKills] of Object.entries(bossKills)) {
    if (!/^[a-z0-9-]{1,64}$/.test(bossId)) continue;
    const kills = normalizeNonNegativeInteger(rawKills);
    if (kills > 0) insertBoss.run(accountId, slotId, bossId, kills);
  }
  const highestRound = normalizeNonNegativeInteger(save.stats?.highestRoguelikeRound);
  database.prepare(`
    INSERT INTO roguelike_records (account_id, slot_id, highest_round, updated_at)
    VALUES (?, ?, ?, CURRENT_TIMESTAMP)
    ON CONFLICT(account_id, slot_id) DO UPDATE SET
      highest_round = excluded.highest_round,
      updated_at = CURRENT_TIMESTAMP
  `).run(accountId, slotId, highestRound);
  const highestEndlessKills = normalizeNonNegativeInteger(save.stats?.highestRogueTrialsEndlessKills);
  database.prepare(`
    INSERT INTO rogue_trials_endless_records (account_id, slot_id, highest_boss_kills, updated_at)
    VALUES (?, ?, ?, CURRENT_TIMESTAMP)
    ON CONFLICT(account_id, slot_id) DO UPDATE SET
      highest_boss_kills = excluded.highest_boss_kills,
      updated_at = CURRENT_TIMESTAMP
  `).run(accountId, slotId, highestEndlessKills);
  const highestHockeyReturns = normalizeNonNegativeInteger(save.stats?.highestHockeyHealingReturns);
  const hockeyDurationSeconds = normalizeNonNegativeNumber(save.stats?.longestHockeyHealingSecondsAtBest);
  database.prepare(`
    INSERT INTO hockey_healing_records (account_id, slot_id, highest_returns, duration_seconds, updated_at)
    VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
    ON CONFLICT(account_id, slot_id) DO UPDATE SET
      highest_returns = excluded.highest_returns,
      duration_seconds = excluded.duration_seconds,
    updated_at = CURRENT_TIMESTAMP
  `).run(accountId, slotId, highestHockeyReturns, hockeyDurationSeconds);

  const hockeyPvpWins = normalizeNonNegativeInteger(save.stats?.hockeyHealingPvpWins);
  const hockeyPvpLosses = normalizeNonNegativeInteger(save.stats?.hockeyHealingPvpLosses);
  const hockeyPvpBossKills = normalizeNonNegativeInteger(save.stats?.hockeyHealingPvpBossKills);
  database.prepare(`
    INSERT INTO hockey_pvp_records (account_id, slot_id, wins, losses, boss_kills, updated_at)
    VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
    ON CONFLICT(account_id, slot_id) DO UPDATE SET
      wins = excluded.wins,
      losses = excluded.losses,
      boss_kills = excluded.boss_kills,
      updated_at = CURRENT_TIMESTAMP
  `).run(accountId, slotId, hockeyPvpWins, hockeyPvpLosses, hockeyPvpBossKills);

  const highestBlockbreakerBricks = normalizeNonNegativeInteger(save.stats?.highestBlockbreakerBricks);
  const longestBlockbreakerSeconds = normalizeNonNegativeNumber(save.stats?.longestBlockbreakerSeconds);
  const highestBlockbreakerScore = normalizeNonNegativeInteger(save.stats?.highestBlockbreakerScore);
  database.prepare(`
    INSERT INTO blockbreaker_records (
      account_id, slot_id,
      highest_bricks, bricks_achieved_at,
      longest_seconds, time_achieved_at,
      highest_score, score_achieved_at,
      updated_at
    ) VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, CURRENT_TIMESTAMP, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
    ON CONFLICT(account_id, slot_id) DO UPDATE SET
      bricks_achieved_at = CASE
        WHEN excluded.highest_bricks > blockbreaker_records.highest_bricks THEN CURRENT_TIMESTAMP
        ELSE blockbreaker_records.bricks_achieved_at
      END,
      highest_bricks = MAX(blockbreaker_records.highest_bricks, excluded.highest_bricks),
      time_achieved_at = CASE
        WHEN excluded.longest_seconds > blockbreaker_records.longest_seconds THEN CURRENT_TIMESTAMP
        ELSE blockbreaker_records.time_achieved_at
      END,
      longest_seconds = MAX(blockbreaker_records.longest_seconds, excluded.longest_seconds),
      score_achieved_at = CASE
        WHEN excluded.highest_score > blockbreaker_records.highest_score THEN CURRENT_TIMESTAMP
        ELSE blockbreaker_records.score_achieved_at
      END,
      highest_score = MAX(blockbreaker_records.highest_score, excluded.highest_score),
      updated_at = CURRENT_TIMESTAMP
  `).run(accountId, slotId, highestBlockbreakerBricks, longestBlockbreakerSeconds, highestBlockbreakerScore);

  const highestAetherScore = normalizeNonNegativeInteger(save.stats?.highestAetherAssaultScore);
  const aetherWaveAtBest = normalizeNonNegativeInteger(save.stats?.highestAetherAssaultWaveAtBest);
  const aetherDurationAtBest = normalizeNonNegativeNumber(save.stats?.longestAetherAssaultSecondsAtBest);
  database.prepare(`
    INSERT INTO aether_assault_records (
      account_id, slot_id, highest_score, wave_at_best, duration_at_best, score_achieved_at, updated_at
    ) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
    ON CONFLICT(account_id, slot_id) DO UPDATE SET
      highest_score = excluded.highest_score,
      wave_at_best = excluded.wave_at_best,
      duration_at_best = excluded.duration_at_best,
      score_achieved_at = CASE
        WHEN excluded.highest_score > aether_assault_records.highest_score
          OR excluded.highest_score = aether_assault_records.highest_score
            AND excluded.wave_at_best > aether_assault_records.wave_at_best
        THEN CURRENT_TIMESTAMP
        ELSE aether_assault_records.score_achieved_at
      END,
      updated_at = CURRENT_TIMESTAMP
    WHERE excluded.highest_score > aether_assault_records.highest_score
      OR excluded.highest_score = aether_assault_records.highest_score
        AND excluded.wave_at_best > aether_assault_records.wave_at_best
      OR excluded.highest_score = aether_assault_records.highest_score
        AND excluded.wave_at_best = aether_assault_records.wave_at_best
        AND excluded.duration_at_best > aether_assault_records.duration_at_best
  `).run(accountId, slotId, highestAetherScore, aetherWaveAtBest, aetherDurationAtBest);
}

function writeSave(database, accountId, slotId, rawSave) {
  const validatedSave = validateSave(rawSave, slotId);
  database.exec("BEGIN IMMEDIATE");
  try {
    const save = mergeLeaderboardHighWater(database, accountId, slotId, validatedSave);
    const serialized = JSON.stringify(save);
    if (Buffer.byteLength(serialized) > MAX_JSON_BYTES) throw apiError("Save snapshot is too large.", 413);
    database.prepare(`
      INSERT INTO hunter_saves (account_id, slot_id, hunter_name, save_json, updated_at)
      VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
      ON CONFLICT(account_id, slot_id) DO UPDATE SET
        hunter_name = excluded.hunter_name,
        save_json = excluded.save_json,
        updated_at = CURRENT_TIMESTAMP
    `).run(accountId, slotId, save.hunterName.trim().slice(0, 20), serialized);
    syncLeaderboardStats(database, accountId, slotId, save);
    database.exec("COMMIT");
    return save;
  } catch (error) {
    database.exec("ROLLBACK");
    throw error;
  }
}

function readSave(database, accountId, slotId) {
  const row = database.prepare(`
    SELECT save_json AS saveJson FROM hunter_saves WHERE account_id = ? AND slot_id = ?
  `).get(accountId, slotId);
  if (!row) return null;
  try {
    const save = JSON.parse(row.saveJson);
    return mergeLeaderboardHighWater(database, accountId, slotId, save);
  } catch {
    return null;
  }
}

function listSaves(database, accountId) {
  return database.prepare(`
    SELECT slot_id AS slotId, save_json AS saveJson, updated_at AS updatedAt
    FROM hunter_saves WHERE account_id = ? ORDER BY slot_id
  `).all(accountId).flatMap((row) => {
    try {
      const save = mergeLeaderboardHighWater(database, accountId, row.slotId, JSON.parse(row.saveJson));
      return [{ slotId: row.slotId, save, updatedAt: row.updatedAt }];
    }
    catch { return []; }
  });
}

function leaderboardEntry(row, valueKey, secondaryValueKey) {
  return {
    rank: row.rank,
    username: row.username,
    hunterName: row.hunterName,
    slotId: row.slotId,
    value: row[valueKey],
    ...(secondaryValueKey ? { secondaryValue: row[secondaryValueKey] } : {}),
  };
}

function bossLeaderboard(database, accountId, slotId, bossId) {
  if (!/^[a-z0-9-]{1,64}$/.test(bossId)) throw apiError("Invalid boss.");
  const rows = database.prepare(`
    WITH ranked AS (
      SELECT
        RANK() OVER (ORDER BY records.kills DESC) AS rank,
        records.account_id AS accountId,
        records.slot_id AS slotId,
        records.kills,
        accounts.username,
        saves.hunter_name AS hunterName,
        records.updated_at AS updatedAt
      FROM boss_kill_records records
      JOIN accounts ON accounts.id = records.account_id
      JOIN hunter_saves saves ON saves.account_id = records.account_id AND saves.slot_id = records.slot_id
      WHERE records.boss_id = ?
    )
    SELECT * FROM ranked ORDER BY kills DESC, updatedAt ASC, accountId ASC, slotId ASC
  `).all(bossId);
  const current = rows.find((row) => row.accountId === accountId && row.slotId === slotId) ?? null;
  return {
    kind: "boss",
    bossId,
    top: rows.slice(0, 5).map((row) => leaderboardEntry(row, "kills")),
    current: current ? leaderboardEntry(current, "kills") : null,
  };
}

function roguelikeLeaderboard(database, accountId, slotId) {
  const rows = database.prepare(`
    WITH ranked AS (
      SELECT
        RANK() OVER (ORDER BY records.highest_round DESC) AS rank,
        records.account_id AS accountId,
        records.slot_id AS slotId,
        records.highest_round AS highestRound,
        accounts.username,
        saves.hunter_name AS hunterName,
        records.updated_at AS updatedAt
      FROM roguelike_records records
      JOIN accounts ON accounts.id = records.account_id
      JOIN hunter_saves saves ON saves.account_id = records.account_id AND saves.slot_id = records.slot_id
      WHERE records.highest_round > 0
    )
    SELECT * FROM ranked ORDER BY highestRound DESC, updatedAt ASC, accountId ASC, slotId ASC
  `).all();
  const current = rows.find((row) => row.accountId === accountId && row.slotId === slotId) ?? null;
  return {
    kind: "roguelike",
    top: rows.slice(0, 5).map((row) => leaderboardEntry(row, "highestRound")),
    current: current ? leaderboardEntry(current, "highestRound") : null,
  };
}

function rogueTrialsEndlessLeaderboard(database, accountId, slotId) {
  const rows = database.prepare(`
    WITH ranked AS (
      SELECT
        RANK() OVER (ORDER BY records.highest_boss_kills DESC) AS rank,
        records.account_id AS accountId,
        records.slot_id AS slotId,
        records.highest_boss_kills AS highestBossKills,
        accounts.username,
        saves.hunter_name AS hunterName,
        records.updated_at AS updatedAt
      FROM rogue_trials_endless_records records
      JOIN accounts ON accounts.id = records.account_id
      JOIN hunter_saves saves ON saves.account_id = records.account_id AND saves.slot_id = records.slot_id
      WHERE records.highest_boss_kills > 0
    )
    SELECT * FROM ranked ORDER BY highestBossKills DESC, updatedAt ASC, accountId ASC, slotId ASC
  `).all();
  const current = rows.find((row) => row.accountId === accountId && row.slotId === slotId) ?? null;
  return {
    kind: "rogue-trials-endless",
    top: rows.slice(0, 5).map((row) => leaderboardEntry(row, "highestBossKills")),
    current: current ? leaderboardEntry(current, "highestBossKills") : null,
  };
}

function hockeyHealingLeaderboard(database, accountId, slotId) {
  const rows = database.prepare(`
    WITH ranked AS (
      SELECT
        RANK() OVER (ORDER BY records.highest_returns DESC, records.duration_seconds DESC) AS rank,
        records.account_id AS accountId,
        records.slot_id AS slotId,
        records.highest_returns AS highestReturns,
        records.duration_seconds AS durationSeconds,
        accounts.username,
        saves.hunter_name AS hunterName,
        records.updated_at AS updatedAt
      FROM hockey_healing_records records
      JOIN accounts ON accounts.id = records.account_id
      JOIN hunter_saves saves ON saves.account_id = records.account_id AND saves.slot_id = records.slot_id
      WHERE records.highest_returns > 0 OR records.duration_seconds > 0
    )
    SELECT * FROM ranked
    ORDER BY highestReturns DESC, durationSeconds DESC, updatedAt ASC, accountId ASC, slotId ASC
  `).all();
  const current = rows.find((row) => row.accountId === accountId && row.slotId === slotId) ?? null;
  return {
    kind: "hockey-healing",
    top: rows.slice(0, 5).map((row) => leaderboardEntry(row, "highestReturns", "durationSeconds")),
    current: current ? leaderboardEntry(current, "highestReturns", "durationSeconds") : null,
  };
}

function hockeyPvpLeaderboard(database, accountId, slotId, kind) {
  const winsBoard = kind === "hockey-pvp-wins";
  const order = winsBoard
    ? "records.wins DESC, records.losses ASC"
    : "records.boss_kills DESC, records.wins DESC";
  const rows = database.prepare(`
    WITH ranked AS (
      SELECT
        RANK() OVER (ORDER BY ${order}) AS rank,
        records.account_id AS accountId,
        records.slot_id AS slotId,
        records.wins,
        records.losses,
        records.boss_kills AS bossKills,
        accounts.username,
        saves.hunter_name AS hunterName,
        records.updated_at AS updatedAt
      FROM hockey_pvp_records records
      JOIN accounts ON accounts.id = records.account_id
      JOIN hunter_saves saves ON saves.account_id = records.account_id AND saves.slot_id = records.slot_id
      WHERE ${winsBoard ? "records.wins > 0 OR records.losses > 0" : "records.boss_kills > 0"}
    )
    SELECT * FROM ranked
    ORDER BY ${winsBoard ? "wins DESC, losses ASC" : "bossKills DESC, wins DESC"}, updatedAt ASC, accountId ASC, slotId ASC
  `).all();
  const current = rows.find((row) => row.accountId === accountId && row.slotId === slotId) ?? null;
  const valueKey = winsBoard ? "wins" : "bossKills";
  const secondaryKey = winsBoard ? "losses" : undefined;
  return {
    kind,
    top: rows.slice(0, 5).map((row) => leaderboardEntry(row, valueKey, secondaryKey)),
    current: current ? leaderboardEntry(current, valueKey, secondaryKey) : null,
  };
}

function blockbreakerLeaderboard(database, accountId, slotId, kind) {
  const boards = {
    "blockbreaker-bricks": { column: "highest_bricks", valueKey: "highestBricks", achieved: "bricks_achieved_at" },
    "blockbreaker-time": { column: "longest_seconds", valueKey: "longestSeconds", achieved: "time_achieved_at" },
    "blockbreaker-score": { column: "highest_score", valueKey: "highestScore", achieved: "score_achieved_at" },
  };
  const board = boards[kind];
  if (!board) throw apiError("Invalid Blockbreaker leaderboard.");
  const rows = database.prepare(`
    WITH ranked AS (
      SELECT
        RANK() OVER (ORDER BY records.${board.column} DESC) AS rank,
        records.account_id AS accountId,
        records.slot_id AS slotId,
        records.${board.column} AS ${board.valueKey},
        records.${board.achieved} AS achievedAt,
        accounts.username,
        saves.hunter_name AS hunterName
      FROM blockbreaker_records records
      JOIN accounts ON accounts.id = records.account_id
      JOIN hunter_saves saves ON saves.account_id = records.account_id AND saves.slot_id = records.slot_id
      WHERE records.${board.column} > 0
    )
    SELECT * FROM ranked
    ORDER BY ${board.valueKey} DESC, achievedAt ASC, accountId ASC, slotId ASC
  `).all();
  const current = rows.find((row) => row.accountId === accountId && row.slotId === slotId) ?? null;
  return {
    kind,
    top: rows.slice(0, 5).map((row) => leaderboardEntry(row, board.valueKey)),
    current: current ? leaderboardEntry(current, board.valueKey) : null,
  };
}

function aetherAssaultLeaderboard(database, accountId, slotId) {
  const rows = database.prepare(`
    WITH ranked AS (
      SELECT
        RANK() OVER (
          ORDER BY records.highest_score DESC, records.wave_at_best DESC,
            records.score_achieved_at ASC, records.account_id ASC, records.slot_id ASC
        ) AS rank,
        records.account_id AS accountId,
        records.slot_id AS slotId,
        records.highest_score AS highestScore,
        records.wave_at_best AS waveAtBest,
        records.score_achieved_at AS achievedAt,
        accounts.username,
        saves.hunter_name AS hunterName
      FROM aether_assault_records records
      JOIN accounts ON accounts.id = records.account_id
      JOIN hunter_saves saves ON saves.account_id = records.account_id AND saves.slot_id = records.slot_id
      WHERE records.highest_score > 0
    )
    SELECT * FROM ranked
    ORDER BY highestScore DESC, waveAtBest DESC, achievedAt ASC, accountId ASC, slotId ASC
  `).all();
  const current = rows.find((row) => row.accountId === accountId && row.slotId === slotId) ?? null;
  return {
    kind: "aether-assault",
    top: rows.slice(0, 5).map((row) => leaderboardEntry(row, "highestScore", "waveAtBest")),
    current: current ? leaderboardEntry(current, "highestScore", "waveAtBest") : null,
  };
}

export function createGameApiHandler(options = {}) {
  const dataDirectory = resolve(options.dataDirectory ?? process.env.DATA_DIR ?? "data");
  mkdirSync(dataDirectory, { recursive: true });
  const database = new DatabaseSync(resolve(dataDirectory, "game.db"));
  database.exec(readFileSync(new URL("../db/schema.sql", import.meta.url), "utf8"));
  const hockeyPvpTickets = new Map();
  const hockeyPvpMatches = new Map();

  function queueResult(ticket) {
    const match = ticket.matchId ? hockeyPvpMatches.get(ticket.matchId) : null;
    if (!match) return { ticketId: ticket.id, status: "waiting" };
    const opponentSide = ticket.side === "host" ? "guest" : "host";
    const opponent = match.players[opponentSide];
    return {
      ticketId: ticket.id,
      status: "matched",
      match: {
        id: match.id,
        seed: match.seed,
        opponentName: opponent.hunterName,
        role: ticket.side,
      },
    };
  }

  function joinHockeyPvpQueue(session, payload) {
    const slotId = validateSlotId(payload?.slotId);
    const hunterName = String(payload?.hunterName ?? "").trim().slice(0, 20);
    if (!hunterName) throw apiError("Hunter name is required.");
    const existing = [...hockeyPvpTickets.values()].find((ticket) =>
      ticket.accountId === session.accountId && !ticket.matchId && !ticket.cancelled && !ticket.completed);
    if (existing) return queueResult(existing);

    const now = Date.now();
    for (const ticket of hockeyPvpTickets.values()) {
      if (!ticket.matchId && now - ticket.createdAt > 30_000) {
        ticket.cancelled = true;
        ticket.completed = true;
      }
    }
    const opponent = [...hockeyPvpTickets.values()].find((ticket) =>
      !ticket.matchId && !ticket.cancelled && !ticket.completed && ticket.accountId !== session.accountId);
    const ticket = {
      id: randomBytes(18).toString("base64url"),
      accountId: session.accountId,
      username: session.username,
      slotId,
      hunterName,
      createdAt: now,
      matchId: null,
      side: null,
      cancelled: false,
      completed: false,
    };
    hockeyPvpTickets.set(ticket.id, ticket);
    if (!opponent) return queueResult(ticket);

    const matchId = randomBytes(18).toString("base64url");
    const match = {
      id: matchId,
      seed: randomBytes(4).readUInt32BE(0) || 1,
      createdAt: now,
      players: { host: opponent, guest: ticket },
      snapshots: { host: null, guest: null },
    };
    opponent.matchId = matchId;
    opponent.side = "host";
    ticket.matchId = matchId;
    ticket.side = "guest";
    hockeyPvpMatches.set(matchId, match);
    return queueResult(ticket);
  }

  function requireHockeyPvpTicket(session, ticketId) {
    const ticket = hockeyPvpTickets.get(ticketId);
    if (!ticket || ticket.accountId !== session.accountId || ticket.cancelled) throw apiError("Queue ticket not found.", 404);
    return ticket;
  }

  function requireHockeyPvpMatch(session, matchId) {
    const match = hockeyPvpMatches.get(matchId);
    if (!match) throw apiError("PVP match not found.", 404);
    const side = match.players.host.accountId === session.accountId
      ? "host"
      : match.players.guest.accountId === session.accountId
        ? "guest"
        : null;
    if (!side) throw apiError("PVP match access denied.", 403);
    return { match, side };
  }

  async function handle(request, response, next) {
    if (!request.url?.startsWith("/api/")) return next();
    setCorsHeaders(request, response);
    if (request.method === "OPTIONS") {
      response.statusCode = 204;
      return response.end();
    }
    try {
      database.prepare("DELETE FROM sessions WHERE expires_at <= CURRENT_TIMESTAMP").run();
      const url = new URL(request.url, "http://localhost");
      const path = url.pathname;

      if (path === "/api/health" && request.method === "GET") {
        return sendJson(response, 200, { ok: true, database: "ready" });
      }
      if (path === "/api/auth/register" && request.method === "POST") {
        enforceAuthRateLimit(request);
        return sendJson(response, 201, register(database, await readJson(request)));
      }
      if (path === "/api/auth/login" && request.method === "POST") {
        enforceAuthRateLimit(request);
        return sendJson(response, 200, login(database, await readJson(request)));
      }
      if (path === "/api/auth/session" && request.method === "GET") {
        const session = currentSession(database, request);
        return sendJson(response, session ? 200 : 401, session
          ? { account: { id: session.accountId, username: session.username } }
          : { error: "Sign in required." });
      }
      if (path === "/api/auth/logout" && request.method === "POST") {
        const token = bearerToken(request);
        if (token) database.prepare("DELETE FROM sessions WHERE token_hash = ?").run(tokenHash(token));
        return sendJson(response, 200, { ok: true });
      }

      const session = requireSession(database, request);
      if (path === "/api/hockey-pvp/queue" && request.method === "POST") {
        return sendJson(response, 200, joinHockeyPvpQueue(session, await readJson(request)));
      }
      const queueMatch = path.match(/^\/api\/hockey-pvp\/queue\/([A-Za-z0-9_-]+)$/);
      if (queueMatch && request.method === "GET") {
        return sendJson(response, 200, queueResult(requireHockeyPvpTicket(session, queueMatch[1])));
      }
      if (queueMatch && request.method === "DELETE") {
        const ticket = requireHockeyPvpTicket(session, queueMatch[1]);
        if (ticket.matchId) throw apiError("Matched queue cannot be cancelled.", 409);
        ticket.cancelled = true;
        ticket.completed = true;
        return sendJson(response, 200, { ok: true });
      }
      const pvpStateMatch = path.match(/^\/api\/hockey-pvp\/matches\/([A-Za-z0-9_-]+)\/state$/);
      if (pvpStateMatch && request.method === "PUT") {
        const { match, side } = requireHockeyPvpMatch(session, pvpStateMatch[1]);
        const payload = await readJson(request);
        if (!payload?.snapshot || typeof payload.snapshot !== "object") throw apiError("PVP snapshot is invalid.");
        match.snapshots[side] = payload.snapshot;
        return sendJson(response, 200, {
          opponentSnapshot: match.snapshots[side === "host" ? "guest" : "host"],
          hostSnapshot: match.snapshots.host,
        });
      }
      if (path === "/api/saves" && request.method === "GET") {
        return sendJson(response, 200, { slots: listSaves(database, session.accountId) });
      }
      const saveMatch = path.match(/^\/api\/saves\/([1-3])$/);
      if (saveMatch && request.method === "GET") {
        return sendJson(response, 200, { save: readSave(database, session.accountId, validateSlotId(saveMatch[1])) });
      }
      if (saveMatch && request.method === "PUT") {
        const slotId = validateSlotId(saveMatch[1]);
        const payload = await readJson(request);
        return sendJson(response, 200, { save: writeSave(database, session.accountId, slotId, payload?.save) });
      }
      const bossMatch = path.match(/^\/api\/leaderboards\/boss\/([a-z0-9-]+)$/);
      if (bossMatch && request.method === "GET") {
        const slotId = validateSlotId(url.searchParams.get("slot"));
        return sendJson(response, 200, bossLeaderboard(database, session.accountId, slotId, bossMatch[1]));
      }
      if (path === "/api/leaderboards/roguelike" && request.method === "GET") {
        const slotId = validateSlotId(url.searchParams.get("slot"));
        return sendJson(response, 200, roguelikeLeaderboard(database, session.accountId, slotId));
      }
      if (path === "/api/leaderboards/rogue-trials-endless" && request.method === "GET") {
        const slotId = validateSlotId(url.searchParams.get("slot"));
        return sendJson(response, 200, rogueTrialsEndlessLeaderboard(database, session.accountId, slotId));
      }
      if (path === "/api/leaderboards/hockey-healing" && request.method === "GET") {
        const slotId = validateSlotId(url.searchParams.get("slot"));
        return sendJson(response, 200, hockeyHealingLeaderboard(database, session.accountId, slotId));
      }
      if (path === "/api/leaderboards/hockey-pvp-wins" && request.method === "GET") {
        const slotId = validateSlotId(url.searchParams.get("slot"));
        return sendJson(response, 200, hockeyPvpLeaderboard(database, session.accountId, slotId, "hockey-pvp-wins"));
      }
      if (path === "/api/leaderboards/hockey-pvp-boss-kills" && request.method === "GET") {
        const slotId = validateSlotId(url.searchParams.get("slot"));
        return sendJson(response, 200, hockeyPvpLeaderboard(database, session.accountId, slotId, "hockey-pvp-boss-kills"));
      }
      if (path === "/api/leaderboards/blockbreaker-bricks" && request.method === "GET") {
        const slotId = validateSlotId(url.searchParams.get("slot"));
        return sendJson(response, 200, blockbreakerLeaderboard(database, session.accountId, slotId, "blockbreaker-bricks"));
      }
      if (path === "/api/leaderboards/blockbreaker-time" && request.method === "GET") {
        const slotId = validateSlotId(url.searchParams.get("slot"));
        return sendJson(response, 200, blockbreakerLeaderboard(database, session.accountId, slotId, "blockbreaker-time"));
      }
      if (path === "/api/leaderboards/blockbreaker-score" && request.method === "GET") {
        const slotId = validateSlotId(url.searchParams.get("slot"));
        return sendJson(response, 200, blockbreakerLeaderboard(database, session.accountId, slotId, "blockbreaker-score"));
      }
      if (path === "/api/leaderboards/aether-assault" && request.method === "GET") {
        const slotId = validateSlotId(url.searchParams.get("slot"));
        return sendJson(response, 200, aetherAssaultLeaderboard(database, session.accountId, slotId));
      }
      return sendJson(response, 404, { error: "API route not found." });
    } catch (error) {
      const status = Number(error?.status) || 500;
      const message = status >= 500 ? "Server error." : error.message;
      if (status >= 500) console.error(error);
      return sendJson(response, status, { error: message });
    }
  }

  return {
    handle,
    close: () => {
      hockeyPvpTickets.clear();
      hockeyPvpMatches.clear();
      database.close();
    },
  };
}
