import {
  subyDirectRedirect,
  subyNick,
  subyNickSet,
  subyPlayBeacon
} from './identity.js';
import {
  RetailImporter,
  RetailImportError,
  formatBytes,
  makeRevisionId,
  validateRetailFiles
} from './importer.js';
import { EngineLoader, EngineLaunchError } from './engine-loader.js';
import {
  BrowserInputSink,
  GamepadController,
  InputRouter,
  TouchController,
  installKeyboardInput
} from './input.js';
const directRoute = subyDirectRedirect();

const byId = (id) => document.getElementById(id);
const canvas = byId('gameCanvas');
const runtimePanel = byId('runtimePanel');
const runtimeBadge = byId('runtimeBadge');
const runtimeKicker = byId('runtimeKicker');
const runtimeTitle = byId('runtimeTitle');
const runtimeMessage = byId('runtimeMessage');
const runtimeDetails = byId('runtimeDetails');
const primaryAction = byId('primaryAction');
const retryButton = byId('retryButton');
const touchOverlay = byId('touchOverlay');
const importer = new RetailImporter();
const engineLoader = new EngineLoader();
const inputRouter = new InputRouter(new BrowserInputSink({ target: canvas }));
let voiceHud = null;
const voiceHudReady = import('./voicehud.js')
  .then(({ installQ4VoiceHud }) => {
    voiceHud = installQ4VoiceHud({ notify: toast });
    return voiceHud;
  })
  .catch((error) => {
    console.warn('Voice chat module is unavailable', error);
    return null;
  });

let primaryHandler = () => startEngine({ type: 'singleplayer' });
let retryHandler = null;
let selectedFiles = [];
let selectedValidation = null;
let importController = null;
let launching = false;
let currentLaunch = null;
let initialLinkHandled = false;

function drawShellCanvas() {
  if (document.body.classList.contains('engine-active')) return;
  const ratio = Math.min(window.devicePixelRatio || 1, 2);
  const width = Math.max(1, Math.floor(canvas.clientWidth * ratio));
  const height = Math.max(1, Math.floor(canvas.clientHeight * ratio));
  if (canvas.width !== width) canvas.width = width;
  if (canvas.height !== height) canvas.height = height;
}

function setBadge(label, level = 'idle') {
  runtimeBadge.textContent = label;
  runtimeBadge.dataset.level = level;
}

function setRuntime({
  kicker = 'OPENQ4 / WEBASSEMBLY',
  title,
  message,
  level = 'idle',
  badge = 'GOTOWY',
  primaryLabel = null,
  onPrimary = null,
  retryLabel = null,
  onRetry = null,
  details = null
}) {
  document.body.classList.remove('engine-active');
  runtimePanel.hidden = false;
  runtimeKicker.textContent = kicker;
  runtimeTitle.textContent = title;
  runtimeMessage.textContent = message;
  setBadge(badge, level);

  primaryAction.hidden = !primaryLabel;
  if (primaryLabel) primaryAction.textContent = primaryLabel;
  primaryHandler = onPrimary || (() => {});

  retryButton.hidden = !retryLabel;
  if (retryLabel) retryButton.textContent = retryLabel;
  retryHandler = onRetry;

  runtimeDetails.hidden = !details;
  runtimeDetails.textContent = details || '';
  drawShellCanvas();
}

function errorDetails(error) {
  const lines = [];
  if (error?.code) lines.push(error.code);
  if (error?.details && Object.keys(error.details).length) {
    try {
      lines.push(JSON.stringify(error.details, null, 2));
    } catch (_ignored) {
      // Error details are diagnostic only.
    }
  }
  return lines.join('\n');
}

function toast(message, type = 'info') {
  const element = document.createElement('div');
  element.className = `toast ${type}`;
  element.textContent = message;
  byId('toastRegion').append(element);
  window.setTimeout(() => element.remove(), 4800);
}

function renderRetailProgress(state) {
  const ratio = state.totalBytes ? state.completedBytes / state.totalBytes : 0;
  const verb = state.state === 'verify-retail' ? 'Sprawdzanie' : 'Pobieranie';
  const file = state.file ? ` · ${state.file}` : '';
  runtimeTitle.textContent = state.state === 'verify-retail'
    ? 'Sprawdzanie danych gry'
    : state.state === 'ready'
      ? 'Dane gry są gotowe'
      : 'Pobieranie danych gry';
  runtimeMessage.textContent = `${verb} danych gry: ${Math.floor(ratio * 100)}% ` +
    `(${formatBytes(state.completedBytes)} / ${formatBytes(state.totalBytes)})${file}`;
  setBadge('DANE', 'loading');
}

function openDialog(dialog) {
  if (!dialog) return;
  for (const open of document.querySelectorAll('dialog[open]')) {
    if (open !== dialog) open.close();
  }
  if (!dialog.open) dialog.showModal();
}

function updateUrlForLaunch(launch) {
  const url = new URL(location.href);
  url.searchParams.delete('connect');
  url.searchParams.delete('replay');
  if (launch.type === 'multiplayer') url.searchParams.set('connect', String(launch.port));
  if (launch.type === 'replay') url.searchParams.set('replay', launch.replay.id);
  history.replaceState({ q4: launch.type }, '', url);
}

function launchFromUrl() {
  const params = new URLSearchParams(location.search);
  const replay = params.get('replay');
  if (replay && /^[A-Za-z0-9_-]{1,512}$/.test(replay)) return { type: 'replay', replayId: replay };
  const connect = params.get('connect');
  if (connect) {
    const match = /(?:^|:)(\d{4,5})$/.exec(connect);
    const port = Number(match?.[1] || connect);
    if (Number.isInteger(port) && port >= 1024 && port <= 65535) return { type: 'multiplayer', port };
  }
  return null;
}

async function replayMetadata(id) {
  const response = await fetch(`/api/replays/${encodeURIComponent(id)}/meta`, { cache: 'no-store' });
  if (!response.ok) throw new EngineLaunchError('REPLAY_NOT_FOUND', 'Wybrana powtórka nie istnieje na serwerze.');
  return response.json();
}

async function allocateReplay(replay) {
  const id = String(replay?.id || '');
  if (!/^[A-Za-z0-9_-]{1,512}$/.test(id)) {
    throw new EngineLaunchError('REPLAY_INVALID', 'Powtórka ma nieprawidłowy identyfikator.');
  }
  const response = await fetch(`/api/replays/${encodeURIComponent(id)}/watch`, {
    method: 'POST',
    headers: { Accept: 'application/json' }
  });
  if (!response.ok) {
    const detail = await response.text().catch(() => '');
    throw new EngineLaunchError(
      'REPLAY_START_FAILED',
      detail || `Nie udało się uruchomić serwera powtórki (HTTP ${response.status}).`
    );
  }
  const watch = await response.json();
  const port = Number(watch?.port);
  if (!Number.isInteger(port) || port < 1024 || port > 65535) {
    throw new EngineLaunchError('REPLAY_START_FAILED', 'Serwer powtórki zwrócił nieprawidłowy port.');
  }
  return { type: 'replay', port, replay, watch };
}

async function startReplay(replay) {
  const launch = await allocateReplay(replay);
  return startEngine(launch);
}

async function handleInitialLink() {
  if (initialLinkHandled || launching) return;
  const link = launchFromUrl();
  if (!link) return;
  initialLinkHandled = true;
  if (link.type === 'replay') {
    try {
      const replay = await replayMetadata(link.replayId);
      await startReplay(replay);
    } catch (error) {
      initialLinkHandled = false;
      renderEngineError(error, null, false, () => handleInitialLink());
    }
    return;
  }
  await startEngine(link);
}

async function readiness() {
  if (await directRoute) return;
  if (launching || document.body.classList.contains('engine-active')) return;
  if (!importer.supported) {
    setRuntime({
      kicker: 'PAMIĘĆ PRZEGLĄDARKI',
      title: 'OPFS jest niedostępny',
      message: 'Uruchom grę w aktualnym Chrome, Edge lub innej przeglądarce obsługującej Origin Private File System.',
      badge: 'BŁĄD',
      level: 'error',
      details: 'OPFS_UNSUPPORTED'
    });
    return;
  }

  setRuntime({
    kicker: 'QUAKE 4 / DANE GRY',
    title: 'Sprawdzanie instalacji',
    message: 'Sprawdzanie silnika i manifestu danych gry…',
    badge: 'DANE',
    level: 'loading'
  });

  try {
    const [catalog, manifest] = await Promise.all([
      importer.serverRetailCatalog(),
      engineLoader.discover()
    ]);
    const cached = await importer.getRevisionManifest(catalog.revision);
    const ready = cached?.state === 'ready' && cached?.source === 'operator-server' &&
      cached?.serverFingerprint === catalog.fingerprint;
    if (launching || document.body.classList.contains('engine-active')) return;
    setRuntime({
      kicker: 'OPENQ4 / GOTOWY',
      title: ready ? 'Silnik i dane są gotowe' : 'Silnik jest gotowy',
      message: ready
        ? `Dane gry są zapisane w tej przeglądarce. Kompilacja ${manifest.build || manifest.script}.`
        : `Przy pierwszym uruchomieniu gra automatycznie pobierze i sprawdzi ${formatBytes(catalog.totalBytes)} danych.`,
      badge: 'GOTOWY',
      level: 'ready',
      primaryLabel: 'Uruchom kampanię',
      onPrimary: () => startEngine({ type: 'singleplayer' })
    });
    await handleInitialLink();
  } catch (error) {
    renderEngineError(error, null, false, () => readiness());
  }
}

function renderEngineError(error, launch = currentLaunch, retryLaunch = true, customRetry = null) {
  const missingEngine = error?.code === 'ENGINE_MISSING';
  const missingRetail = error?.code === 'RETAIL_MISSING';
  const title = missingEngine
    ? 'Brak kompilacji silnika'
    : missingRetail
      ? 'Brak danych gry'
      : 'Nie udało się uruchomić gry';
  const retryAction = customRetry
    || (retryLaunch && launch ? () => startEngine(launch) : null)
    || (missingEngine ? () => readiness() : null);
  setRuntime({
    kicker: 'START ZATRZYMANY',
    title,
    message: error?.message || 'Nieznany błąd uruchamiania.',
    badge: 'BŁĄD',
    level: 'error',
    primaryLabel: missingRetail ? 'Wybierz pliki gry' : null,
    onPrimary: missingRetail ? () => openImportDialog() : null,
    retryLabel: retryAction ? (missingEngine ? 'Sprawdź ponownie' : 'Spróbuj ponownie') : null,
    onRetry: retryAction,
    details: errorDetails(error)
  });
}

async function startEngine(launch) {
  if (await directRoute) return;
  if (launching || document.body.classList.contains('engine-active')) return;
  launch = { ...launch, nick: subyNick() || 'Gracz' };
  launching = true;
  currentLaunch = launch;
  if (launch.type === 'multiplayer') await voiceHudReady;
  for (const dialog of document.querySelectorAll('dialog[open]')) dialog.close();
  setRuntime({
    kicker: 'OPENQ4 / START',
    title: 'Uruchamianie silnika',
    message: 'Przygotowanie lokalnych danych gry…',
    badge: 'START',
    level: 'loading'
  });

  try {
    await importer.ensureServerRetail({ onProgress: renderRetailProgress });
    await engineLoader.launch({
      canvas,
      launch,
      importer,
      onStatus: ({ message }) => {
        if (!message) return;
        runtimeMessage.textContent = message;
        setBadge('START', 'loading');
      },
      onReady: () => {
        document.body.classList.add('engine-active');
        runtimePanel.hidden = true;
        touchOverlay.setAttribute('aria-hidden', 'false');
        setBadge('ONLINE', 'ready');
        updateUrlForLaunch(launch);
        canvas.focus({ preventScroll: true });
        if (launch.type !== 'replay') subyPlayBeacon('q4', subyNick());
      }
    });
  } catch (error) {
    renderEngineError(error, launch);
  } finally {
    launching = false;
  }
}

async function refreshImports() {
  const activeRevision = byId('activeRevision');
  const list = byId('revisionList');
  list.replaceChildren();
  try {
    const state = await importer.listImports();
    activeRevision.textContent = state.activeRevision || 'Brak';
    if (!state.manifests.length) {
      const empty = document.createElement('p');
      empty.className = 'empty-state';
      empty.textContent = 'Brak lokalnych rewizji.';
      list.append(empty);
      return;
    }

    for (const manifest of state.manifests) {
      const row = document.createElement('div');
      row.className = 'revision-item';
      const copy = document.createElement('div');
      const name = document.createElement('strong');
      name.textContent = manifest.revision;
      const meta = document.createElement('span');
      const stateLabel = {
        ready: 'gotowa', importing: 'przerwana', cancelled: 'anulowana', error: 'błąd'
      }[manifest.state] || manifest.state;
      meta.textContent = `${stateLabel} · ${formatBytes(manifest.completedBytes)} / ${formatBytes(manifest.totalBytes)}`;
      copy.append(name, meta);

      const action = document.createElement('button');
      if (manifest.state === 'ready' && state.activeRevision !== manifest.revision) {
        action.textContent = 'Użyj';
        action.addEventListener('click', async () => {
          await importer.activate(manifest.revision);
          await refreshImports();
          await readiness();
        });
      } else {
        action.textContent = 'Usuń';
        action.addEventListener('click', async () => {
          if (!window.confirm(`Usunąć lokalną rewizję ${manifest.revision}?`)) return;
          await importer.remove(manifest.revision);
          await refreshImports();
          await readiness();
        });
      }
      row.append(copy, action);
      list.append(row);
    }
  } catch (error) {
    activeRevision.textContent = 'Błąd';
    const empty = document.createElement('p');
    empty.className = 'empty-state';
    empty.textContent = error.message || String(error);
    list.append(empty);
  }
}

async function updateSelection(files) {
  selectedFiles = Array.from(files || []);
  selectedValidation = validateRetailFiles(selectedFiles);
  const summary = byId('selectionSummary');
  const message = byId('validationMessage');
  const start = byId('startImportButton');
  summary.hidden = false;
  byId('selectedFileCount').textContent = String(selectedValidation.files.length);
  byId('selectedBytes').textContent = formatBytes(selectedValidation.totalBytes);
  message.className = 'validation-message';
  start.disabled = true;

  let previous = null;
  try {
    previous = await importer.getRevisionManifest(makeRevisionId(selectedValidation));
    const quota = await importer.quota(selectedValidation, previous);
    byId('availableBytes').textContent = formatBytes(quota.available);
    if (!selectedValidation.ok) {
      message.classList.add('error');
      if (selectedValidation.duplicates.length) {
        message.textContent = `Powtórzone nazwy: ${selectedValidation.duplicates.map((entry) => entry.name).join(', ')}`;
      } else if (selectedValidation.invalid.length) {
        message.textContent = `Puste pakiety: ${selectedValidation.invalid.join(', ')}`;
      } else {
        message.textContent = `Brakuje: ${selectedValidation.missing.join(', ')}`;
      }
      return;
    }
    if (!quota.ok) {
      message.classList.add('error');
      message.textContent = `Za mało miejsca: potrzeba ${formatBytes(quota.required)}, dostępne ${formatBytes(quota.available)}.`;
      return;
    }
    message.classList.add('ok');
    message.textContent = previous?.state === 'ready'
      ? 'Ta rewizja jest już zapisana. Import sprawdzi i aktywuje istniejące dane.'
      : previous
        ? 'Znaleziono niedokończoną rewizję. Import zostanie wznowiony.'
        : 'Zestaw plików jest kompletny.';
    start.disabled = false;
  } catch (error) {
    byId('availableBytes').textContent = '—';
    message.classList.add('error');
    message.textContent = error.message || String(error);
  }
}

async function beginImport() {
  if (!selectedValidation?.ok || importController) return;
  importController = new AbortController();
  const progressArea = byId('importProgressArea');
  const progress = byId('importProgress');
  const progressText = byId('importProgressText');
  const progressLabel = byId('importProgressLabel');
  const cancel = byId('cancelImportButton');
  const start = byId('startImportButton');
  progressArea.hidden = false;
  cancel.hidden = false;
  start.disabled = true;

  try {
    const result = await importer.import(selectedFiles, {
      signal: importController.signal,
      onProgress: (state) => {
        const ratio = state.totalBytes ? state.completedBytes / state.totalBytes : 0;
        progress.value = Math.max(0, Math.min(1, ratio));
        progressText.textContent = `${Math.floor(ratio * 100)}%`;
        progressLabel.textContent = state.file || (state.state === 'ready' ? 'Gotowe' : 'Importowanie');
      }
    });
    progress.value = 1;
    progressText.textContent = '100%';
    toast(result.duplicate ? 'Istniejąca rewizja została aktywowana.' : 'Pliki gry zostały zapisane.', 'info');
    await refreshImports();
    await readiness();
  } catch (error) {
    if (error?.name === 'AbortError') toast('Import został anulowany.', 'info');
    else {
      toast(error.message || String(error), 'error');
      byId('validationMessage').className = 'validation-message error';
      byId('validationMessage').textContent = error.message || String(error);
    }
    await refreshImports();
  } finally {
    importController = null;
    cancel.hidden = true;
    start.disabled = !selectedValidation?.ok;
  }
}

async function openImportDialog() {
  openDialog(byId('importDialog'));
  await refreshImports();
}

async function openMultiplayer() {
  openDialog(byId('multiplayerDialog'));
  const row = byId('serverStatus');
  row.dataset.online = 'false';
  byId('serverStatusText').textContent = 'Sprawdzanie serwera…';
  byId('serverPlayers').textContent = '—';
  try {
    const response = await fetch('/api/status', { cache: 'no-store' });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const status = await response.json();
    row.dataset.online = String(Boolean(status.dedicated?.running));
    byId('serverStatusText').textContent = status.dedicated?.running ? 'Serwer działa' : 'Serwer gry jest zatrzymany';
    byId('serverPlayers').textContent = `${Number(status.players) || 0} gr.`;
    if (status.udp?.defaultPort) byId('serverPort').value = String(status.udp.defaultPort);
  } catch (_error) {
    byId('serverStatusText').textContent = 'Status jest niedostępny';
  }
}

async function openReplays() {
  openDialog(byId('replayDialog'));
  const list = byId('replayList');
  list.replaceChildren();
  const loading = document.createElement('p');
  loading.className = 'empty-state';
  loading.textContent = 'Ładowanie…';
  list.append(loading);
  try {
    const response = await fetch('/api/replays', { cache: 'no-store' });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const payload = await response.json();
    list.replaceChildren();
    if (!payload.replays?.length) {
      const empty = document.createElement('p');
      empty.className = 'empty-state';
      empty.textContent = 'Brak zapisanych powtórek.';
      list.append(empty);
      return;
    }
    for (const replay of payload.replays) {
      const row = document.createElement('div');
      row.className = 'replay-item';
      const copy = document.createElement('div');
      const name = document.createElement('strong');
      name.textContent = replay.name;
      const meta = document.createElement('span');
      meta.textContent = `${formatBytes(replay.size)} · ${new Date(replay.modifiedAt).toLocaleString('pl-PL')}`;
      copy.append(name, meta);
      const play = document.createElement('button');
      play.textContent = 'Odtwórz';
      play.addEventListener('click', async () => {
        try {
          await startReplay(replay);
        } catch (error) {
          renderEngineError(error, null, false, () => startReplay(replay));
        }
      });
      row.append(copy, play);
      list.append(row);
    }
  } catch (error) {
    list.replaceChildren();
    const empty = document.createElement('p');
    empty.className = 'empty-state';
    empty.textContent = `Nie udało się pobrać listy: ${error.message || error}`;
    list.append(empty);
  }
}

function installUi() {
  primaryAction.addEventListener('click', () => primaryHandler());
  retryButton.addEventListener('click', () => retryHandler?.());
  byId('campaignButton').addEventListener('click', () => startEngine({ type: 'singleplayer' }));
  byId('multiplayerButton').addEventListener('click', () => openMultiplayer());
  byId('replayButton').addEventListener('click', () => openReplays());
  byId('importButton').addEventListener('click', () => openImportDialog());
  byId('menuButton').addEventListener('click', () => openDialog(byId('menuDialog')));
  byId('refreshImportsButton').addEventListener('click', () => refreshImports());

  const nickInput = byId('nickInput');
  nickInput.value = subyNick() || subyNickSet('Gracz');
  nickInput.addEventListener('change', () => {
    const nick = subyNickSet(nickInput.value) || 'Gracz';
    nickInput.value = nick;
  });

  const directoryInput = byId('retailDirectory');
  const fileInput = byId('retailFiles');
  byId('chooseDirectoryButton').addEventListener('click', () => {
    directoryInput.value = '';
    directoryInput.click();
  });
  byId('chooseFilesButton').addEventListener('click', () => {
    fileInput.value = '';
    fileInput.click();
  });
  directoryInput.addEventListener('change', () => updateSelection(directoryInput.files));
  fileInput.addEventListener('change', () => updateSelection(fileInput.files));

  const dropTarget = byId('dropTarget');
  dropTarget.addEventListener('dragover', (event) => {
    event.preventDefault();
    dropTarget.dataset.drag = 'true';
  });
  dropTarget.addEventListener('dragleave', () => { dropTarget.dataset.drag = 'false'; });
  dropTarget.addEventListener('drop', (event) => {
    event.preventDefault();
    dropTarget.dataset.drag = 'false';
    updateSelection(event.dataTransfer.files);
  });
  byId('startImportButton').addEventListener('click', () => beginImport());
  byId('cancelImportButton').addEventListener('click', () => importController?.abort());

  byId('joinServerButton').addEventListener('click', () => {
    const port = Number(byId('serverPort').value);
    if (!Number.isInteger(port) || port < 1024 || port > 65535) {
      toast('Nieprawidłowy port serwera.', 'error');
      return;
    }
    startEngine({ type: 'multiplayer', port });
  });

  byId('fullscreenButton').addEventListener('click', async () => {
    byId('menuDialog').close();
    try {
      if (document.fullscreenElement) await document.exitFullscreen();
      else await document.documentElement.requestFullscreen({ navigationUI: 'hide' });
    } catch (error) {
      toast(error.message || String(error), 'error');
    }
  });
  byId('pointerLockButton').addEventListener('click', async () => {
    byId('menuDialog').close();
    try {
      await canvas.requestPointerLock?.({ unadjustedMovement: true });
    } catch (_error) {
      canvas.requestPointerLock?.();
    }
  });
  byId('aboutButton').addEventListener('click', () => {
    byId('menuDialog').close();
    openDialog(byId('aboutDialog'));
  });
}

function installInput() {
  new TouchController({
    overlay: touchOverlay,
    moveZone: byId('moveZone'),
    moveKnob: byId('moveKnob'),
    lookZone: byId('lookZone'),
    router: inputRouter
  }).install();
  installKeyboardInput({
    canvas,
    router: inputRouter,
    active: () => document.body.classList.contains('engine-active')
  });
  new GamepadController({
    router: inputRouter,
    active: () => document.body.classList.contains('engine-active')
  }).start();
}

async function registerServiceWorker() {
  if (!('serviceWorker' in navigator) || !window.isSecureContext) return;
  try {
    await navigator.serviceWorker.register('/sw.js', { scope: '/' });
  } catch (error) {
    console.warn('PWA service worker registration failed', error);
  }
}

installUi();
installInput();
window.addEventListener('resize', drawShellCanvas, { passive: true });
window.addEventListener('orientationchange', () => window.setTimeout(drawShellCanvas, 120), { passive: true });
drawShellCanvas();
registerServiceWorker();
readiness().catch((error) => {
  const normalized = error instanceof RetailImportError ? error : new RetailImportError('READINESS_FAILED', error.message || String(error));
  renderEngineError(normalized, null, false, () => readiness());
});

window.Q4App = Object.freeze({
  importer,
  engineLoader,
  startEngine,
  startReplay,
  input: inputRouter,
  get voice() { return voiceHud?.session || null; },
  readiness
});
