import fs from 'node:fs';
import path from 'node:path';
import { resolveFile, sendFile, sendJson, sendText } from './files.js';

export const REPLAY_CLAIM_HEADER = 'x-q4-replay-claim';
export const REPLAY_CLAIM_PATTERN = /^[a-f0-9]{64}$/;

export function replayClaimToken(req) {
  const token = req.headers[REPLAY_CLAIM_HEADER];
  return typeof token === 'string' && REPLAY_CLAIM_PATTERN.test(token) ? token : null;
}

export function replayId(name) {
  return Buffer.from(name, 'utf8').toString('base64url');
}

export function replayName(id) {
  if (!/^[A-Za-z0-9_-]{1,512}$/.test(id)) return null;
  try {
    const name = Buffer.from(id, 'base64url').toString('utf8');
    if (!name || name.startsWith('.') || name.includes('/') || name.includes('\\') || replayId(name) !== id) return null;
    return name;
  } catch {
    return null;
  }
}

function isReplayName(name) {
  return typeof name === 'string' && !name.startsWith('.') && /\.q4sd$/i.test(name);
}

function metadataFor(dir, name) {
  const sidecar = resolveFile(dir, encodeURIComponent(`${name}.meta.json`));
  if (!sidecar || sidecar.stat.size > 64 * 1024) return {};
  try {
    const value = JSON.parse(fs.readFileSync(sidecar.path, 'utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
  } catch {
    return {};
  }
}

export function isFinalizedReplay(filePath, stat, metadata) {
  if (!stat?.isFile?.() || stat.size < 20 || metadata?.format !== 'openq4-server-demo' ||
      metadata.complete !== true || !Number.isInteger(metadata.version) ||
      !Number.isInteger(metadata.durationMs) || metadata.durationMs < 0 ||
      metadata.durationMs > 0x7fffffff) {
    return false;
  }

  const header = Buffer.allocUnsafe(8);
  const trailer = Buffer.allocUnsafe(12);
  let descriptor;
  try {
    descriptor = fs.openSync(filePath, 'r');
    if (fs.readSync(descriptor, header, 0, header.length, 0) !== header.length ||
        fs.readSync(descriptor, trailer, 0, trailer.length, stat.size - trailer.length) !== trailer.length) {
      return false;
    }
  } catch {
    return false;
  } finally {
    if (descriptor !== undefined) {
      try { fs.closeSync(descriptor); } catch {}
    }
  }

  return header.toString('ascii', 0, 4) === 'Q4SD' &&
    header.readInt32LE(4) === metadata.version &&
    trailer.readInt32LE(0) === 3 &&
    trailer.readInt32LE(4) === metadata.durationMs &&
    trailer.readInt32LE(8) === 0;
}

function replayCandidates(dir) {
  let entries = [];
  try {
    entries = fs.readdirSync(dir, { withFileTypes: true });
  } catch {
    return [];
  }
  return entries
    .filter((entry) => entry.isFile() && isReplayName(entry.name))
    .map((entry) => {
      const file = resolveFile(dir, encodeURIComponent(entry.name));
      if (!file) return null;
      return { name: entry.name, path: file.path, stat: file.stat };
    })
    .filter(Boolean)
    .sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs || a.name.localeCompare(b.name));
}

function finalizedReplays(dir) {
  return replayCandidates(dir)
    .map((replay) => {
      const metadata = metadataFor(dir, replay.name);
      return isFinalizedReplay(replay.path, replay.stat, metadata) ? { ...replay, metadata } : null;
    })
    .filter(Boolean);
}

export function resolveReplay(dir, id) {
  const name = replayName(id);
  const file = name && resolveFile(dir, encodeURIComponent(name));
  if (!name || !isReplayName(name) || !file) return null;
  const metadata = metadataFor(dir, name);
  if (!isFinalizedReplay(file.path, file.stat, metadata)) return null;
  return {
    id,
    name,
    path: file.path,
    stat: file.stat,
    metadata,
  };
}

function publicReplay(replay) {
  return {
    id: replay.id,
    name: replay.name,
    size: replay.stat.size,
    modifiedAt: replay.stat.mtime.toISOString(),
    metadata: replay.metadata,
    downloadUrl: `/api/replays/${replay.id}`,
    metadataUrl: `/api/replays/${replay.id}/meta`,
    watchUrl: `/api/replays/${replay.id}/watch`,
    deepLink: `/?replay=${encodeURIComponent(replay.id)}`,
  };
}

export function pruneReplays(dir, limit = 300) {
  const keep = Math.max(0, Number.isSafeInteger(limit) ? limit : 300);
  const files = replayCandidates(dir);
  const removed = [];
  for (const replay of files.slice(keep)) {
    try {
      fs.unlinkSync(replay.path);
      removed.push(replay.name);
    } catch {
      continue;
    }
    try {
      fs.unlinkSync(path.join(dir, `${replay.name}.meta.json`));
    } catch {}
  }
  return removed;
}

export function listReplays(dir, limit = 300) {
  pruneReplays(dir, limit);
  return finalizedReplays(dir).map(({ name, path: filePath, stat, metadata }) => publicReplay({
    id: replayId(name),
    name,
    path: filePath,
    stat,
    metadata,
  }));
}

export async function handleReplayRequest(req, res, pathname, config, replayManager) {
  if (pathname === '/api/replays') {
    if (req.method !== 'GET' && req.method !== 'HEAD') {
      sendText(req, res, 405, 'method not allowed', config, { Allow: 'GET, HEAD' });
      return true;
    }
    sendJson(req, res, 200, { replays: listReplays(config.replayDir, config.replayRetention) }, config);
    return true;
  }
  const match = /^\/api\/replays\/([A-Za-z0-9_-]+)(?:\/(meta|watch))?$/.exec(pathname);
  if (!match) return false;
  const replay = resolveReplay(config.replayDir, match[1]);
  if (match[2] === 'watch') {
    if (req.method !== 'POST' && req.method !== 'DELETE') {
      sendText(req, res, 405, 'method not allowed', config, { Allow: 'POST, DELETE' });
      return true;
    }
    if (!replayManager) {
      sendJson(req, res, 503, { error: 'server replay playback is unavailable', code: 'REPLAY_UNAVAILABLE' }, config);
      return true;
    }
    const claimToken = replayClaimToken(req);
    if (!claimToken) {
      sendJson(req, res, 400, {
        error: 'a valid replay claim token is required',
        code: 'REPLAY_CLAIM_TOKEN_INVALID',
      }, config);
      return true;
    }
    if (req.method === 'DELETE') {
      if (!replayName(match[1])) {
        sendText(req, res, 404, 'replay not found', config);
        return true;
      }
      try {
        const released = replayManager.cancelClaim(match[1], claimToken);
        sendJson(req, res, 200, { ok: true, released }, config);
      } catch (error) {
        sendJson(req, res, error.statusCode || 500, {
          error: error.message || 'replay claim cancellation failed',
          code: error.code || 'REPLAY_CLAIM_CANCEL_FAILED',
        }, config);
      }
      return true;
    }
    if (!replay) {
      sendText(req, res, 404, 'replay not found', config);
      return true;
    }
    try {
      const { instance, reused } = await replayManager.watch(replay, claimToken);
      sendJson(req, res, 200, {
        port: instance.port,
        connectUrl: `/game/${instance.port}`,
        replay: replay.name,
        replayId: replay.id,
        map: replay.metadata?.map || null,
        reused,
      }, config);
    } catch (error) {
      sendJson(req, res, error.statusCode || 500, {
        error: error.message || 'replay instance failed',
        code: error.code || 'REPLAY_INSTANCE_FAILED',
      }, config);
    }
    return true;
  }
  if (!replay) {
    sendText(req, res, 404, 'replay not found', config);
    return true;
  }
  if (req.method !== 'GET' && req.method !== 'HEAD') {
    sendText(req, res, 405, 'method not allowed', config, { Allow: 'GET, HEAD' });
    return true;
  }
  if (match[2] === 'meta') {
    sendJson(req, res, 200, publicReplay(replay), config);
    return true;
  }
  sendFile(req, res, replay.path, config, {
    stat: replay.stat,
    mount: 'replay',
    cacheControl: 'private, no-cache',
    contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(replay.name)}`,
  });
  return true;
}
