import fs from 'node:fs';
import path from 'node:path';
import { pipeline } from 'node:stream';

const MIME = new Map([
  ['.html', 'text/html; charset=utf-8'],
  ['.htm', 'text/html; charset=utf-8'],
  ['.js', 'text/javascript; charset=utf-8'],
  ['.mjs', 'text/javascript; charset=utf-8'],
  ['.css', 'text/css; charset=utf-8'],
  ['.json', 'application/json; charset=utf-8'],
  ['.map', 'application/json; charset=utf-8'],
  ['.wasm', 'application/wasm'],
  ['.data', 'application/octet-stream'],
  ['.mem', 'application/octet-stream'],
  ['.pk4', 'application/octet-stream'],
  ['.pak', 'application/octet-stream'],
  ['.bin', 'application/octet-stream'],
  ['.gz', 'application/gzip'],
  ['.xz', 'application/x-xz'],
  ['.rtf', 'application/rtf'],
  ['.sha256', 'text/plain; charset=utf-8'],
  ['.q4sd', 'application/octet-stream'],
  ['.demo', 'application/octet-stream'],
  ['.ndmo', 'application/octet-stream'],
  ['.roq', 'video/x-id-roq'],
  ['.ogg', 'audio/ogg'],
  ['.opus', 'audio/ogg'],
  ['.wav', 'audio/wav'],
  ['.png', 'image/png'],
  ['.jpg', 'image/jpeg'],
  ['.jpeg', 'image/jpeg'],
  ['.webp', 'image/webp'],
  ['.gif', 'image/gif'],
  ['.svg', 'image/svg+xml'],
  ['.ico', 'image/x-icon'],
  ['.webmanifest', 'application/manifest+json'],
  ['.woff2', 'font/woff2'],
  ['.woff', 'font/woff'],
  ['.ttf', 'font/ttf'],
  ['.txt', 'text/plain; charset=utf-8'],
  ['.cfg', 'text/plain; charset=utf-8'],
]);

export function commonHeaders(config, { crossOrigin = false } = {}) {
  const headers = {
    'X-Content-Type-Options': 'nosniff',
    'Referrer-Policy': 'same-origin',
    'Cross-Origin-Resource-Policy': crossOrigin ? 'cross-origin' : 'same-origin',
  };
  if (config.crossOriginIsolation) {
    headers['Cross-Origin-Opener-Policy'] = 'same-origin';
    headers['Cross-Origin-Embedder-Policy'] = 'require-corp';
  }
  return headers;
}

export function sendJson(req, res, status, value, config, extraHeaders = {}) {
  const body = Buffer.from(JSON.stringify(value));
  res.writeHead(status, {
    ...commonHeaders(config, { crossOrigin: true }),
    'Content-Type': 'application/json; charset=utf-8',
    'Content-Length': body.length,
    'Cache-Control': 'no-store',
    ...extraHeaders,
  });
  if (req.method === 'HEAD') return res.end();
  res.end(body);
}

export function sendText(req, res, status, text, config, extraHeaders = {}) {
  const body = Buffer.from(text);
  res.writeHead(status, {
    ...commonHeaders(config),
    'Content-Type': 'text/plain; charset=utf-8',
    'Content-Length': body.length,
    'Cache-Control': 'no-store',
    ...extraHeaders,
  });
  if (req.method === 'HEAD') return res.end();
  res.end(body);
}

export function cachePolicy(file, mount = 'public') {
  const name = path.basename(file).toLowerCase();
  const ext = path.extname(name);
  if (ext === '.html' || name === 'sw.js' || ext === '.webmanifest') return 'no-cache';
  if (mount === 'public' && (ext === '.js' || ext === '.css')) return 'no-cache';
  if (/[._-][a-f0-9]{8,}[._-]/i.test(name)) return 'public, max-age=31536000, immutable';
  if (mount === 'engine' || mount === 'content') return 'public, max-age=3600, must-revalidate';
  return 'public, max-age=3600, must-revalidate';
}

function within(root, candidate) {
  return candidate === root || candidate.startsWith(root + path.sep);
}

export function resolveFile(root, rawRelative) {
  let decoded;
  try {
    decoded = decodeURIComponent(rawRelative);
  } catch {
    return null;
  }
  if (!decoded || decoded.includes('\0') || decoded.includes('\\')) return null;
  const parts = decoded.split('/');
  if (parts.some((part) => !part || part === '.' || part === '..')) return null;
  const rootResolved = path.resolve(root);
  const candidate = path.resolve(rootResolved, ...parts);
  if (!within(rootResolved, candidate)) return null;

  let stat;
  try {
    stat = fs.statSync(candidate);
    if (!stat.isFile()) return null;
    const realRoot = fs.realpathSync(rootResolved);
    const realCandidate = fs.realpathSync(candidate);
    if (!within(realRoot, realCandidate)) return null;
    return { path: realCandidate, stat };
  } catch {
    return null;
  }
}

function etag(stat) {
  return `W/"${stat.size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`;
}

function etagMatches(header, tag) {
  return String(header || '').split(',').map((value) => value.trim()).some((value) => value === '*' || value === tag);
}

function notModifiedSince(header, mtimeMs) {
  const stamp = Date.parse(String(header || ''));
  return Number.isFinite(stamp) && Math.floor(mtimeMs / 1000) <= Math.floor(stamp / 1000);
}

function parseRange(header, size) {
  if (!header) return null;
  if (size <= 0 || header.includes(',')) return false;
  const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
  if (!match || (!match[1] && !match[2])) return false;

  let start;
  let end;
  if (!match[1]) {
    const suffix = Number.parseInt(match[2], 10);
    if (!Number.isSafeInteger(suffix) || suffix <= 0) return false;
    start = Math.max(0, size - suffix);
    end = size - 1;
  } else {
    start = Number.parseInt(match[1], 10);
    end = match[2] ? Number.parseInt(match[2], 10) : size - 1;
    if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || start >= size || end < start) return false;
    end = Math.min(end, size - 1);
  }
  return { start, end };
}

export function sendFile(req, res, file, config, options = {}) {
  const stat = options.stat || fs.statSync(file);
  const tag = options.etag || etag(stat);
  const modified = stat.mtime.toUTCString();
  const headers = {
    ...commonHeaders(config),
    'Content-Type': options.contentType || MIME.get(path.extname(file).toLowerCase()) || 'application/octet-stream',
    'Cache-Control': options.cacheControl || cachePolicy(file, options.mount),
    'Accept-Ranges': 'bytes',
    ETag: tag,
    'Last-Modified': modified,
  };
  if (options.contentDisposition) headers['Content-Disposition'] = options.contentDisposition;

  if (!req.headers.range && (etagMatches(req.headers['if-none-match'], tag) ||
      (!req.headers['if-none-match'] && notModifiedSince(req.headers['if-modified-since'], stat.mtimeMs)))) {
    res.writeHead(304, headers);
    return res.end();
  }

  let range = null;
  const ifRange = req.headers['if-range'];
  if (!ifRange || ifRange === tag || ifRange === modified) range = parseRange(req.headers.range, stat.size);
  if (range === false) {
    res.writeHead(416, { ...headers, 'Content-Range': `bytes */${stat.size}`, 'Content-Length': 0 });
    return res.end();
  }

  const status = range ? 206 : 200;
  const start = range?.start ?? 0;
  const end = range?.end ?? Math.max(0, stat.size - 1);
  headers['Content-Length'] = range ? end - start + 1 : stat.size;
  if (range) headers['Content-Range'] = `bytes ${start}-${end}/${stat.size}`;
  res.writeHead(status, headers);
  if (req.method === 'HEAD' || stat.size === 0) return res.end();

  const streamOptions = range || options.fd !== undefined
    ? {
        start,
        end,
        ...(options.fd !== undefined ? { fd: options.fd, autoClose: false } : {}),
      }
    : undefined;
  const stream = fs.createReadStream(file, streamOptions);
  pipeline(stream, res, (error) => {
    if (error && !res.destroyed) res.destroy(error);
  });
}
