/* Browser integration only: inputs, HUD, menus, audio and frame scheduling. */
(function (D) {
  'use strict';
  const $ = (id) => document.getElementById(id);
  const stage = $('stage'), canvas = $('game');
  const fmt = (value) => Math.round(Number(value) || 0).toLocaleString('ja-JP');
  const yen = (value) => '¥' + fmt(value);
  const game = new D.Game();
  const renderer = new D.Renderer(canvas);
  const sound = new D.Audio();
  const narrator = new D.Narrator(({ text, active }) => {
    $('narration').hidden = !active;
    $('narration-text').textContent = text || '';
    sound.setDucked(active);
  });
  const pointer = { x: 480, y: 270, inside: false, down: false };
  let lastTime = performance.now(), lastMode = '', invoiceShown = false, startingBest = 0;
  let displayScore = 0, previousMessage = '', bestScore = 0, previousBill = 0;
  let queuedBossWarning = false;
  let bossBombNoticeUntil = 0;
  const weapons = [...document.querySelectorAll('[data-weapon]')];
  const storage = {
    get(key, fallback) { try { const v = localStorage.getItem(key); return v === null ? fallback : v; } catch (_) { return fallback; } },
    set(key, value) { try { localStorage.setItem(key, value); } catch (_) {} }
  };
  bestScore = Number(storage.get('danchi-best', '0')) || 0;
  const savedVolume = Number(storage.get('danchi-volume', '45'));
  $('volume').value = Number.isFinite(savedVolume) ? savedVolume : 45;
  sound.setVolume(Number($('volume').value) / 100);
  sound.setMuted(storage.get('danchi-muted', 'false') === 'true');
  updateSoundUI();

  function updateSoundUI() {
    const muted = sound.muted || sound.volume === 0;
    narrator.setVolume(sound.volume);
    narrator.setMuted(muted);
    $('mute-button').setAttribute('aria-pressed', String(muted));
    $('mute-button').setAttribute('aria-label', muted ? '音を有効にする' : '消音する');
    $('sound-symbol').textContent = muted ? '◖×' : '◖))';
    $('sound-label').textContent = muted ? 'SOUND OFF' : 'SOUND ON';
  }
  function toggleSound() {
    sound.unlock();
    if (sound.volume === 0) { sound.setVolume(.45); $('volume').value = 45; sound.setMuted(false); }
    else sound.setMuted(!sound.muted);
    storage.set('danchi-muted', sound.muted);
    storage.set('danchi-volume', $('volume').value);
    updateSoundUI();
  }
  function begin() {
    sound.unlock();
    pointer.down = false;
    queuedBossWarning = false;
    bossBombNoticeUntil = 0;
    game.reset();
    game.state.best = bestScore;
    startingBest = bestScore;
    game.start();
    sound.updateCrowd(game.state);
    invoiceShown = false;
    displayScore = 0;
    previousBill = 0;
    canvas.focus({ preventScroll: true });
    updateHUD();
    narrator.start();
  }
  function pause(value) {
    if (!['playing', 'boss', 'case', 'rocket', 'ending'].includes(game.state.mode)) return;
    game.state.paused = value;
    sound.updateCrowd(game.state);
    narrator.setPaused(value);
    pointer.down = false;
    $('pause-screen').hidden = !value;
    if (value) $('resume-button').focus({ preventScroll: true });
    else { sound.unlock(); canvas.focus({ preventScroll: true }); }
  }
  function coordinates(event) {
    const rect = canvas.getBoundingClientRect();
    pointer.x = (event.clientX - rect.left) / rect.width * D.DATA.width;
    pointer.y = (event.clientY - rect.top) / rect.height * D.DATA.height;
    pointer.inside = pointer.x >= 0 && pointer.y >= 0 && pointer.x <= D.DATA.width && pointer.y <= D.DATA.height;
  }
  function fire() {
    const s = game.state;
    if (s.paused) return;
    const box = D.DATA.caseRect || { x: 748, y: 455, w: 194, h: 69 };
    if (s.mode === 'case' && pointer.x >= box.x && pointer.x <= box.x + box.w && pointer.y >= box.y && pointer.y <= box.y + box.h) {
      game.openCase();
      pointer.down = false;
    } else if (['playing', 'boss', 'rocket'].includes(s.mode)) game.shoot(pointer.x, pointer.y);
  }
  canvas.addEventListener('pointermove', coordinates);
  canvas.addEventListener('pointerenter', coordinates);
  canvas.addEventListener('pointerleave', () => { pointer.inside = false; pointer.down = false; });
  canvas.addEventListener('pointerdown', (event) => {
    if (event.button !== 0) return;
    event.preventDefault();
    coordinates(event);
    if (game.state.mode === 'title') return;
    sound.unlock();
    canvas.focus({ preventScroll: true });
    pointer.down = true;
    canvas.setPointerCapture?.(event.pointerId);
    fire();
  });
  const release = () => { pointer.down = false; };
  window.addEventListener('pointerup', release);
  window.addEventListener('pointercancel', release);
  canvas.addEventListener('lostpointercapture', release);
  canvas.addEventListener('contextmenu', (event) => event.preventDefault());
  stage.addEventListener('wheel', (event) => {
    if (game.state.paused || !['playing', 'boss'].includes(game.state.mode)) return;
    event.preventDefault();
    if (event.deltaY) game.cycleWeapon(Math.sign(event.deltaY));
  }, { passive: false });
  document.addEventListener('keydown', (event) => {
    if (event.target instanceof HTMLInputElement) return;
    if (event.repeat) return;
    if (event.key === 'Escape' || event.code === 'KeyP') { event.preventDefault(); pause(!game.state.paused); }
    if (['Digit1', 'Digit2', 'Digit3', 'Numpad1', 'Numpad2', 'Numpad3'].includes(event.code)) {
      if (!game.state.paused) game.selectWeapon(Number(event.code.slice(-1)) - 1);
    }
    if (event.code === 'KeyM') toggleSound();
    if (event.code === 'KeyR' && game.state.mode === 'result') begin();
    if ((event.code === 'Space' || event.code === 'Enter') && game.state.mode === 'title' && event.target === canvas) { event.preventDefault(); begin(); }
  });
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) pause(true);
    lastTime = performance.now();
  });
  window.addEventListener('blur', () => { release(); pause(true); });
  $('start-button').addEventListener('click', begin);
  $('resume-button').addEventListener('click', () => pause(false));
  $('restart-button').addEventListener('click', begin);
  $('replay-button').addEventListener('click', begin);
  $('pause-button').addEventListener('click', () => pause(true));
  $('skip-narration').addEventListener('click', () => { narrator.stop(); canvas.focus({ preventScroll: true }); });
  window.addEventListener('pagehide', () => { narrator.stop(); sound.stopCrowd(); });
  $('mute-button').addEventListener('click', toggleSound);
  $('volume').addEventListener('input', () => {
    sound.unlock(); sound.setVolume(Number($('volume').value) / 100);
    if (sound.volume > 0) sound.setMuted(false);
    storage.set('danchi-volume', $('volume').value); storage.set('danchi-muted', sound.muted); updateSoundUI();
  });
  $('fullscreen-button').addEventListener('click', async () => {
    try {
      if (document.fullscreenElement) await document.exitFullscreen();
      else if (stage.requestFullscreen) await stage.requestFullscreen();
    } catch (_) { /* Browser may restrict fullscreen in embeds. */ }
  });
  document.addEventListener('fullscreenchange', () => {
    $('fullscreen-button').setAttribute('aria-label', document.fullscreenElement ? '全画面を終了する' : '全画面にする');
  });
  weapons.forEach((button, index) => button.addEventListener('click', () => {
    sound.unlock(); game.selectWeapon(index); pointer.down = false; canvas.focus({ preventScroll: true });
  }));

  function makeInvoice(s) {
    narrator.stop();
    invoiceShown = true;
    const collapsed = s.endingReason === 'collapse';
    $('result-eyebrow').textContent = collapsed ? 'CLEAR / NOTHING LEFT TO COMPLAIN ABOUT.' : 'CLEAR / A VERY LOUD AFTERNOON.';
    $('result-heading-top').textContent = collapsed ? '全壊' : 'お騒がせ';
    $('result-heading-bottom').textContent = collapsed ? 'クリア。' : 'しました。';
    $('result-copy').textContent = collapsed ? '建物ごと、騒ぎは解決。\n再建費まで、あなた持ちです。' : '団地に、平穏が戻りました。\nたぶん。';
    document.querySelector('.invoice-heading h3').textContent = collapsed ? '再建費御請求書' : '修繕費御請求書';
    document.querySelector('.invoice-table-head>span:last-child').textContent = collapsed ? '修繕・再建費' : '修繕費';
    document.querySelector('.complaint p').textContent = collapsed ? '団地を更地にしないでください。\n建て直しの費用も請求します。' : 'ベランダでロケットランチャーを\n使用しないでください。';
    $('result-score').textContent = fmt(s.score);
    $('result-stats').textContent = '撃退 ' + fmt(s.kills) + '体　/　発砲 ' + fmt(s.shots) + '発\nヘッドショット ' + fmt(s.headshots || 0) + '　/　警告射撃 ' + fmt(s.warningShots || 0) + '\n最大連続命中 ' + fmt(s.maxCombo || 0) + ' HIT';
    $('result-boss-loss').hidden = !(s.bossBombHits > 0);
    $('result-boss-loss').textContent = '爆弾被弾 ' + fmt(s.bossBombHits || 0) + '回　/　スコア -' + fmt(s.bossScoreLost || 0);
    $('new-record').hidden = s.score <= startingBest;
    const entries = s.log || [];
    $('invoice-total').textContent = yen(entries.reduce((sum, item) => sum + Number(item.cost), 0));
    $('invoice-number').textContent = (collapsed ? '再建費 第' : '修繕費 第') + String((s.seed || 1) % 1000).padStart(3, '0') + '号';
    const list = $('invoice-lines'); list.replaceChildren();
    // Preserve cause ordering, even when a delayed consequence lands after another accident.
    const entryIds = new Set(entries.map((e) => e.id));
    const children = new Map();
    const roots = [];
    entries.forEach((entry) => {
      if (entry.causeId && entryIds.has(entry.causeId)) {
        if (!children.has(entry.causeId)) children.set(entry.causeId, []);
        children.get(entry.causeId).push(entry);
      } else roots.push(entry);
    });
    const shown = new Set();
    function append(entry, depth) {
      if (shown.has(entry.id)) return;
      shown.add(entry.id);
      const row = document.createElement('li');
      if (depth) row.className = 'causal';
      const label = document.createElement('span');
      const location = document.createElement('span'); location.className = 'location';
      location.textContent = (depth ? '↳ ' : '') + (entry.cellId || '共用部');
      label.append(location, document.createTextNode(entry.label || '修繕作業'));
      const cost = document.createElement('span'); cost.className = 'invoice-price'; cost.textContent = entry.cost > 0 ? yen(entry.cost) : '—';
      row.append(label, cost); list.append(row);
      (children.get(entry.id) || []).forEach((child) => append(child, depth + 1));
    }
    roots.forEach((entry) => append(entry, 0));
    entries.forEach((entry) => append(entry, 0));
    document.querySelector('.invoice-footnote').textContent = '全' + entries.length + '件 ↓ スクロールで確認　/　↳ は前の事故からの連鎖';
    if (!entries.length) {
      const li = document.createElement('li'); li.textContent = '本日の修繕記録はありません。'; list.append(li);
    }
    $('result-screen').scrollTop = 0;
    list.scrollTop = 0;
    $('replay-button').focus({ preventScroll: true });
  }
  function updateHUD() {
    const s = game.state;
    if (s.score > bestScore) { bestScore = s.score; storage.set('danchi-best', bestScore); }
    s.best = bestScore;
    if (lastMode !== s.mode) {
      lastMode = s.mode;
      stage.dataset.mode = s.mode;
      $('title-screen').hidden = s.mode !== 'title';
      $('hud').hidden = ['title', 'result'].includes(s.mode);
      $('result-screen').hidden = s.mode !== 'result';
      if (s.mode === 'result' && !invoiceShown) makeInvoice(s);
    }
    $('pause-screen').hidden = !s.paused;
    displayScore += (s.score - displayScore) * .23;
    if (Math.abs(s.score - displayScore) < 1) displayScore = s.score;
    $('score').textContent = fmt(displayScore);
    $('best').textContent = fmt(bestScore);
    $('bill').textContent = yen(s.invoiceTotal);
    if (s.invoiceTotal > previousBill && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      const board = $('bill-board');
      board.getAnimations().forEach(animation => animation.cancel());
      board.animate([{ transform: 'scale(1.08)', color: '#fff2bc' }, { transform: 'scale(1)', color: '#ffd19b' }], { duration: 420, easing: 'ease-out' });
    }
    previousBill = s.invoiceTotal;
    const integrity = s.structure ? Math.max(0, Math.min(1, s.structure.hp / s.structure.maxHp)) : 1;
    const integrityPercent = Math.ceil(integrity * 1000) / 10;
    $('structure-percent').textContent = integrityPercent;
    $('structure-fill').style.width = integrity * 100 + '%';
    $('structure-meter').setAttribute('aria-valuenow', String(integrityPercent));
    $('structure-board').dataset.condition = integrity === 0 ? 'collapsed' : integrity <= .25 ? 'critical' : integrity <= .6 ? 'damaged' : 'sound';
    $('structure-condition').textContent = integrity === 0 ? '全壊。請求、確定。' : integrity <= .25 ? '倒壊寸前！' : integrity <= .6 ? 'あちこち、ガタガタ。' : 'まだ、建っています。';
    $('combo').textContent = '×' + (s.multiplier || 1);
    $('combo-hits').textContent = (s.combo || 0) + ' HIT';
    const comboDuration = D.DATA.scoring.comboWindow;
    $('combo-fill').style.width = Math.min(100, Math.max(0, s.comboTimer / comboDuration * 100)) + '%';
    const phases = D.DATA.phases;
    const phase = phases[Math.min(s.phase, phases.length - 1)];
    const nextPhase = phases[s.phase + 1];
    const bossMode = ['boss', 'case', 'rocket', 'ending'].includes(s.mode);
    $('phase-name').textContent = bossMode ? ({ boss: 'FINAL / 団地のヌシ', case: '非常用ケース 解禁', rocket: '最後の一発', ending: s.endingReason === 'collapse' ? 'CLEAR / 建物全壊' : '本日、閉店。' })[s.mode] : String(s.phase + 1).padStart(2, '0') + ' / ' + (phase?.name || 'いつもの午後');
    const threshold = nextPhase?.threshold || D.DATA.scoring.bossThreshold;
    const previous = phase?.threshold || 0;
    const percentage = bossMode ? 100 : (s.progress - previous) / Math.max(1, threshold - previous) * 100;
    $('progress-fill').style.width = Math.max(0, Math.min(100, percentage)) + '%';
    $('next-unlock').textContent = bossMode ? s.mode === 'boss' ? '弾数無制限 · 1 / 2 / 3 で武器変更' : '弾数無制限' : 'NEXT: ' + (nextPhase?.name || '団地のヌシ');
    $('boss-bar').hidden = s.mode !== 'boss' || !s.boss || s.boss.hp <= 0;
    if (s.boss) {
      const bossMaxHp = s.boss.maxHp || D.DATA.enemies.boss.hp, hp = Math.max(0, Math.min(bossMaxHp, s.boss.hp));
      $('boss-fill').style.width = hp / bossMaxHp * 100 + '%';
      $('boss-hp-value').textContent = 'HP ' + fmt(hp) + ' / ' + fmt(bossMaxHp);
      $('boss-hp-meter').setAttribute('aria-valuemax', String(bossMaxHp));
      $('boss-hp-meter').setAttribute('aria-valuenow', String(hp));
      const rage = s.boss.rage || {}, duration = rage.duration || D.DATA.bossFight?.rageDuration || 18;
      const elapsed = Math.max(0, Math.min(duration, rage.elapsed || 0));
      const remaining = Math.max(0, Math.ceil(duration - elapsed));
      const ratio = Math.max(0, Math.min(1, rage.ratio ?? elapsed / duration));
      $('boss-rage-fill').style.width = ratio * 100 + '%';
      $('boss-rage').setAttribute('aria-valuenow', String(Math.round(ratio * 100)));
      $('boss-rage').setAttribute('aria-valuetext', '爆弾投擲まで ' + remaining + '秒');
      $('boss-rage-time').textContent = '爆弾投擲まで ' + remaining + '秒';
      $('boss-bar').dataset.warning = String(!!rage.warning || duration - elapsed <= 4);
      $('boss-target-note').textContent = '頭はダメージ' + (D.DATA.bossFight?.headshotDamageMultiplier || 2) + '倍。赤い爆弾は一撃＋高額請求。';
      $('boss-bomb-risk').textContent = '投げられた爆弾に被弾するとスコア -' + fmt(D.DATA.bossFight?.bombScorePenalty || 25000);
    }
    $('boss-bomb-notice').hidden = s.time >= bossBombNoticeUntil || ['title', 'ending', 'result'].includes(s.mode);
    const message = s.messageTimer > 0 ? s.message : '';
    if (message !== previousMessage) {
      previousMessage = message;
      $('announcement').textContent = message || '';
      $('announcement').classList.toggle('show', !!message);
    }
    $('case-hint').hidden = !['case', 'rocket'].includes(s.mode);
    if (s.mode === 'case') $('case-hint').innerHTML = '非常事態です。<strong>右下のケースをクリック ↘</strong>';
    else if (s.mode === 'rocket') $('case-hint').innerHTML = '遠慮は、ここまで。<strong>向かいの団地をクリック！</strong>';
    weapons.forEach((button, index) => {
      const locked = !s.unlockedWeapons[index];
      button.disabled = locked || !['playing', 'boss'].includes(s.mode);
      button.classList.toggle('locked', locked);
      button.classList.toggle('selected', index === s.weapon);
      button.setAttribute('aria-pressed', String(index === s.weapon));
      button.querySelector('small').textContent = locked ? 'まだお預け' : ['正確な一発', 'まとめて一掃', '押しっぱなしで連射'][index];
    });
  }
  function frame(now) {
    const dt = Math.max(0, Math.min(.05, (now - lastTime) / 1000));
    lastTime = now;
    game.update(dt);
    if (pointer.down && pointer.inside && !game.state.paused && D.DATA.weapons[game.state.weapon]?.auto && ['playing', 'boss'].includes(game.state.mode)) fire();
    let structureWarning = false, endingStarted = false;
    for (const event of game.drainEvents()) {
      if (event.type === 'boss' && event.spawned) queuedBossWarning = true;
      if (event.type === 'structureWarning' && event.level === 1) structureWarning = true;
      if (event.type === 'boss' && event.defeated) {
        queuedBossWarning = false;
        if (narrator.clipId === 'bossWarning') narrator.stop();
      }
      if (event.type === 'collapse' && event.started) { endingStarted = true; pointer.down = false; }
      if (event.type === 'bossBomb') {
        bossBombNoticeUntil = game.state.time + 2.2;
        $('boss-bomb-penalty').textContent = event.penalty > 0 ? '被弾 -' + fmt(event.penalty) : '被弾！';
        $('boss-bomb-consequence').textContent = event.penalty > 0 ? 'スコア減少・コンボ解除' : 'スコア0・コンボ解除';
        displayScore = game.state.score;
      }
      sound.play(event);
    }
    if (endingStarted) { queuedBossWarning = false; narrator.stop(); }
    else if (structureWarning && ['playing', 'boss'].includes(game.state.mode)) narrator.start('structureWarning');
    if (!endingStarted && queuedBossWarning && game.state.boss && !game.state.boss.dead && !(narrator.active && narrator.clipId === 'structureWarning')) {
      queuedBossWarning = false;
      narrator.start('bossWarning');
    }
    sound.update(game.state);
    renderer.render(game.state, pointer);
    updateHUD();
    requestAnimationFrame(frame);
  }
  // Intentionally expose only the game, not privileged browser APIs. Useful for balancing/debugging.
  D.app = { game, renderer, sound, narrator, pointer };
  requestAnimationFrame(frame);
})(globalThis.Danchi = globalThis.Danchi || {});
