import assert from 'node:assert/strict';

import { PNG } from 'pngjs';

import { selectServerRetailBundles } from '../public/js/importer.js';

export const CRITICAL_ENGINE_LOG_PATTERNS = Object.freeze([
  String.raw`(?:^|\s)(?:Aborted\(|Sys_Error:|Fatal error:|ERROR:)`,
  String.raw`InvalidGUI:`,
  String.raw`Couldn't load (?:gui|model|image|cube image|anim|animation|sound|material|shader)\b`,
  String.raw`(?:Could not|Failed to) load (?:gui|model|image|cube image|anim|animation|sound|material|shader)\b`,
  String.raw`Couldn't find GLSL sources\b`,
  String.raw`Starting defaulted sound sample\b`,
  String.raw`Modern GL program .+ is missing\b`,
  String.raw`(?:shader|GLSL program).*(?:compile|link).*(?:failed|failure|error)\b`,
]);

const CRITICAL_ENGINE_LOG_REGEXPS = CRITICAL_ENGINE_LOG_PATTERNS
  .map((pattern) => new RegExp(pattern, 'i'));

export function criticalEngineLog(line) {
  const normalized = String(line).replace(/\^[0-9]/g, '');
  return CRITICAL_ENGINE_LOG_REGEXPS.some((pattern) => pattern.test(normalized));
}

export function preferredMapMode(map) {
  const modes = Array.isArray(map?.modes) ? map.modes.map(String) : [];
  assert.ok(modes.length > 0, `multiplayer map ${map?.id || '<unknown>'} has no supported modes`);
  return modes.includes('DM') ? 'DM' : modes[0];
}

export function bundleScenarioPhase(map, mode) {
  const mapId = String(map || '');
  const modeId = String(mode || '');
  assert.ok(mapId && modeId, 'bundle scenario phase requires a map and mode');
  assert.ok(!mapId.includes('::') && !modeId.includes('::'),
    'bundle scenario map and mode must not contain the phase separator');
  return `${mapId}::${modeId}`;
}

export function buildBundleScenarioPlan(catalog, menuMaps, { requireCompleteCoverage = true } = {}) {
  assert.ok(Array.isArray(catalog?.maps) && catalog.maps.length > 0,
    'retail bundle manifest has no maps');
  assert.ok(Array.isArray(menuMaps) && menuMaps.length > 0,
    'multiplayer menu catalog has no maps');

  const menuById = new Map(menuMaps.map((map) => [String(map?.id || ''), map]));
  assert.equal(menuById.size, menuMaps.length, 'multiplayer menu catalog contains duplicate map ids');
  const bundleMapIds = catalog.maps.map(({ id }) => String(id));
  assert.equal(new Set(bundleMapIds).size, bundleMapIds.length,
    'retail bundle manifest contains duplicate map ids');

  for (const id of bundleMapIds) {
    assert.ok(menuById.has(id), `bundled map ${id} is absent from the multiplayer menu`);
  }
  if (requireCompleteCoverage) {
    assert.deepEqual(
      [...bundleMapIds].sort(),
      [...menuById.keys()].sort(),
      'retail bundle manifest does not cover the complete multiplayer menu'
    );
  }

  const installed = new Set();
  const scenarios = [];
  for (const menuMap of menuMaps.filter(({ id }) => bundleMapIds.includes(id))) {
    const modes = Array.isArray(menuMap.modes) ? menuMap.modes.map(String) : [];
    assert.ok(modes.length > 0, `multiplayer map ${menuMap.id} has no supported modes`);
    assert.equal(new Set(modes).size, modes.length,
      `multiplayer map ${menuMap.id} contains duplicate modes`);
    const selection = selectServerRetailBundles(catalog, menuMap.id);

    for (let modeIndex = 0; modeIndex < modes.length; modeIndex += 1) {
      const firstForMap = modeIndex === 0;
      const newBundles = firstForMap
        ? selection.bundles.filter(({ id }) => !installed.has(id))
        : [];
      if (firstForMap) {
        for (const id of selection.bundleIds) installed.add(id);
      }
      const installedBundleIds = [...installed].sort();
      const installedMapIds = catalog.maps
        .filter((map) => map.bundleIds.every((bundleId) => installed.has(bundleId)))
        .map(({ id }) => id)
        .sort();
      const mode = modes[modeIndex];
      scenarios.push({
        scenario: bundleScenarioPhase(menuMap.id, mode),
        id: menuMap.id,
        label: menuMap.label || menuMap.id,
        mode,
        firstForMap,
        selection,
        newBundles,
        installedBundleIds,
        installedMapIds,
      });
    }
  }
  return scenarios;
}

export const buildBundleMapPlan = buildBundleScenarioPlan;

export function requestSummary(request, phase = request.phase) {
  return {
    phase,
    url: request.url,
    method: request.method,
    range: request.range,
    ifRange: request.ifRange,
  };
}

export function assertPayloadBatch(requests, expectedBundles, baseUrl, label) {
  const expectedByUrl = new Map(expectedBundles.map((bundle) => [
    new URL(bundle.url, `${String(baseUrl).replace(/\/$/, '')}/`).href,
    bundle,
  ]));
  const actualUrls = [...new Set(requests.map(({ url }) => url))].sort();
  assert.deepEqual(actualUrls, [...expectedByUrl.keys()].sort(),
    `${label}: unexpected retail payload set`);

  for (const [url, bundle] of expectedByUrl) {
    const ranges = requests
      .filter((request) => request.url === url)
      .map((request) => {
        assert.equal(request.method, 'GET', `${label}: ${bundle.id} payload must use GET`);
        assert.equal(request.ifRange, `\"${bundle.sha256}\"`,
          `${label}: ${bundle.id} was not bound to its SHA-256 ETag`);
        const match = /^bytes=(\d+)-(\d+)$/.exec(request.range || '');
        assert.ok(match, `${label}: ${bundle.id} request has no bounded Range`);
        return { start: Number(match[1]), end: Number(match[2]) };
      })
      .sort((left, right) => left.start - right.start || left.end - right.end);
    assert.ok(ranges.length > 0, `${label}: ${bundle.id} was not downloaded`);
    let cursor = 0;
    for (const range of ranges) {
      assert.equal(range.start, cursor, `${label}: ${bundle.id} ranges contain a gap or duplicate`);
      assert.ok(range.end >= range.start && range.end < bundle.size,
        `${label}: ${bundle.id} range is outside its manifest size`);
      cursor = range.end + 1;
    }
    assert.equal(cursor, bundle.size, `${label}: ${bundle.id} was not downloaded exactly once`);
  }
}

export function assertNoPayload(requests, label) {
  assert.deepEqual(requests.map((request) => requestSummary(request)), [],
    `${label}: cached map fetched retail payload again`);
}

export function assertRenderEvidence(evidence, label = evidence?.label || 'canvas') {
  assert.ok(evidence && typeof evidence === 'object' && !Array.isArray(evidence),
    `${label}: render evidence is missing`);
  assert.ok(evidence.width >= 640 && evidence.height >= 360,
    `${label}: canvas is too small`);
  assert.ok(Number.isSafeInteger(evidence.samples) && evidence.samples > 0,
    `${label}: render sample count is invalid`);
  assert.ok(evidence.opaque > evidence.samples * 0.98,
    `${label}: canvas is transparent`);
  assert.ok(evidence.nonDark > evidence.samples * 0.02,
    `${label}: canvas is blank or black`);
  assert.ok(evidence.colors > 30 && evidence.deviation > 4,
    `${label}: canvas is flat`);
  return evidence;
}

export function renderedPngEvidence(buffer, label) {
  const png = PNG.sync.read(buffer);
  const colors = new Set();
  const region = {
    left: Math.floor(png.width * 0.2),
    top: Math.floor(png.height * 0.18),
    right: Math.ceil(png.width * 0.8),
    bottom: Math.ceil(png.height * 0.82),
  };
  const regionPixels = (region.right - region.left) * (region.bottom - region.top);
  const stride = Math.max(1, Math.floor(regionPixels / 60_000));
  let regionPixel = 0;
  let samples = 0;
  let opaque = 0;
  let nonDark = 0;
  let luminanceSum = 0;
  let luminanceSquareSum = 0;

  for (let y = region.top; y < region.bottom; y += 1) {
    for (let x = region.left; x < region.right; x += 1, regionPixel += 1) {
      if (regionPixel % stride !== 0) continue;
      const offset = (y * png.width + x) * 4;
      const red = png.data[offset];
      const green = png.data[offset + 1];
      const blue = png.data[offset + 2];
      const alpha = png.data[offset + 3];
      if (alpha > 240) opaque += 1;
      const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
      if (luminance > 12) nonDark += 1;
      luminanceSum += luminance;
      luminanceSquareSum += luminance * luminance;
      colors.add(`${red >> 3}:${green >> 3}:${blue >> 3}`);
      samples += 1;
    }
  }
  const mean = luminanceSum / samples;
  const evidence = {
    label,
    width: png.width,
    height: png.height,
    region,
    samples,
    opaque,
    nonDark,
    colors: colors.size,
    mean,
    deviation: Math.sqrt(Math.max(0, luminanceSquareSum / samples - mean * mean)),
  };
  return assertRenderEvidence(evidence, label);
}
