// ============================================
// ARROWS ESCAPE v3 - "Free The Green Arrow" style
// Thin dense lines, tap-to-extract (any order), one green target
// ============================================

const DIR_DELTA = { N:[-1,0], S:[1,0], E:[0,1], W:[0,-1] };

// ---------- Audio ----------
const Audio_ = {
  ctx: null,
  ensureCtx() {
    if (!this.ctx) {
      const AC = window.AudioContext || window.webkitAudioContext;
      if (AC) this.ctx = new AC();
    }
    return this.ctx;
  },
  tone(freq, durationMs, type='sine', gain=0.15) {
    if (Storage.getMuted() || !GAME_CONFIG.audioEnabled) return;
    const ctx = this.ensureCtx();
    if (!ctx) return;
    if (ctx.state === 'suspended') ctx.resume();
    const osc = ctx.createOscillator();
    const g = ctx.createGain();
    osc.type = type; osc.frequency.value = freq; g.gain.value = gain;
    osc.connect(g); g.connect(ctx.destination);
    const now = ctx.currentTime;
    g.gain.setValueAtTime(gain, now);
    g.gain.exponentialRampToValueAtTime(0.001, now + durationMs/1000);
    osc.start(now); osc.stop(now + durationMs/1000);
  },
  click()   { this.tone(480, 60, 'square', 0.07); },
  extract() { this.tone(700, 80); setTimeout(() => this.tone(950, 100), 65); },
  target()  { this.tone(784,140); setTimeout(()=>this.tone(988,140),100); setTimeout(()=>this.tone(1318,220),200); },
  blocked() { this.tone(160, 220, 'sawtooth', 0.13); },
  success() { this.tone(523,120); setTimeout(()=>this.tone(659,120),110); setTimeout(()=>this.tone(784,200),220); },
  fail()    { this.tone(180, 250, 'sawtooth', 0.15); },
  coin()    { this.tone(988,80,'triangle',0.12); setTimeout(()=>this.tone(1318,100,'triangle',0.12),65); },
};

// ---------- Router ----------
const Router = {
  current: 'screen-home',

  // Actually swap which .screen has the 'active' class. Never touches
  // history -- callers use show()/goHome() for that.
  render(id) {
    document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
    const el = document.getElementById(id);
    if (el) el.classList.add('active');
    this.current = id;
    // a screen change (especially via back-navigation) should never
    // leave a stale popup floating over the newly-shown screen
    if (typeof Game !== 'undefined' && Game.hidePopups) Game.hidePopups();

    // Defensive self-check: this should be structurally impossible
    // given the querySelectorAll/classList calls above, but if
    // anything external ever mutates the DOM between those two
    // lines (e.g. a stray event handler), guarantee only the
    // intended screen stays active rather than silently allowing
    // two at once.
    const activeScreens = document.querySelectorAll('.screen.active');
    if (activeScreens.length > 1) {
      activeScreens.forEach(s => { if (s.id !== id) s.classList.remove('active'); });
    }
  },

  // Normal in-app navigation (tapping Play, a level, back-arrow, etc).
  // Pushes a history entry so the phone's system back button has
  // something to pop back through, landing on home rather than
  // leaving the app / going to the previous website.
  show(id) {
    this.render(id);
    if (id === 'screen-home') {
      // treat "returning home" as the base of the stack, replace
      // rather than piling up entries
      history.replaceState({ screen: id }, '', location.href);
    } else {
      history.pushState({ screen: id }, '', location.href);
    }
  },

  // Called on the very first load to establish the base history entry.
  init() {
    if (!history.state) {
      history.replaceState({ screen: 'screen-home' }, '', location.href);
    }
    this.render(history.state?.screen || 'screen-home');

    window.addEventListener('popstate', (e) => {
      const target = (e.state && e.state.screen) || 'screen-home';
      this.render(target);
    });

    // bfcache restore: re-sync to whatever the history state actually
    // says, so a page frozen mid-transition can never show two
    // screens active at once.
    window.addEventListener('pageshow', (e) => {
      if (e.persisted) {
        this.render(history.state?.screen || 'screen-home');
      }
    });
  },
};

// ---------- Colors ----------
const COLOR = {
  idle:   '#FF073A',   // neon red — normal pieces
  target: '#16a34a',   // green — the ONE goal piece ("Free the Green Arrow")
  hint:   '#f59e0b',   // amber — temporary hint flash on a normal piece
};

// ---------- Game State ----------
const Game = {
  currentLevel : 1,
  levelData    : null,
  presentPieces: null,   // Set<id>
  cellSize     : 0,
  locked       : false,

  async init() {
    await Levels.load();
    this.bindGlobalUI();
    this.refreshHomeStats();
    this.applyMuteIcon();
    this.applyTheme();
    this.registerServiceWorker();
    this.setupInstallPrompt();
    this.setupAds();
    Router.init();
  },

  // Show/initialize the AdSense banner slots based on config. Safe to
  // call even before a real Publisher ID is set -- adsbygoogle.js
  // simply won't have loaded (or will 400 harmlessly) and this no-ops.
  setupAds() {
    if (!GAME_CONFIG.adsEnabled) return;

    const topSlot = document.getElementById('ad-top-banner');
    const bottomSlot = document.getElementById('ad-bottom-banner');

    if (GAME_CONFIG.showTopBanner && topSlot) topSlot.hidden = false;
    if (GAME_CONFIG.showBottomBanner && bottomSlot) bottomSlot.hidden = false;

    // Each <ins class="adsbygoogle"> unit needs one push() call to
    // request an ad. Wrapped in try/catch because this throws if the
    // adsbygoogle script failed to load (e.g. placeholder Publisher
    // ID, ad blocker, or offline) -- none of which should break the
    // rest of the game.
    try {
      (window.adsbygoogle = window.adsbygoogle || []).push({});
      (window.adsbygoogle = window.adsbygoogle || []).push({});
    } catch (e) {
      console.warn('[Ads] adsbygoogle push failed (expected until a real Publisher ID is set):', e);
    }
  },

  bindGlobalUI() {
    document.getElementById('btn-play').addEventListener('click', () => {
      Audio_.click(); this.openLevel(Storage.getCurrentLevel());
    });
    document.getElementById('btn-levels').addEventListener('click', () => {
      Audio_.click(); this.renderLevelGrid(); Router.show('screen-levels');
    });
    document.querySelectorAll('[data-back]').forEach(btn => {
      btn.addEventListener('click', () => {
        Audio_.click();
        Router.show(btn.dataset.back);
        if (btn.dataset.back === 'screen-home')  this.refreshHomeStats();
        if (btn.dataset.back === 'screen-levels') this.renderLevelGrid();
      });
    });
    document.getElementById('btn-mute').addEventListener('click', () => {
      Storage.setMuted(!Storage.getMuted()); this.applyMuteIcon();
    });
    document.getElementById('btn-theme-toggle').addEventListener('click', () => {
      Audio_.click();
      Storage.setTheme(Storage.getTheme() === 'dark' ? 'light' : 'dark');
      this.applyTheme();
    });
    document.getElementById('btn-restart').addEventListener('click', () => {
      Audio_.click(); this.startLevel(this.currentLevel);
    });
    document.getElementById('btn-hint').addEventListener('click', () => this.useHint());
    document.getElementById('btn-next').addEventListener('click', () => {
      Audio_.click(); this.goToNextLevel();
    });
    document.querySelectorAll('[data-action]').forEach(btn => {
      btn.addEventListener('click', () => this.handlePopupAction(btn.dataset.action));
    });
  },

  applyMuteIcon() {
    document.getElementById('btn-mute').textContent = Storage.getMuted() ? '🔇' : '🔊';
  },
  applyTheme() {
    const dark = Storage.getTheme() === 'dark';
    document.getElementById('app').classList.toggle('dark-theme', dark);
    const btn = document.getElementById('btn-theme-toggle');
    if (btn) btn.textContent = dark ? '☀️' : '🌙';
  },
  refreshHomeStats() {
    document.getElementById('home-coins').textContent   = Storage.getCoins();
    document.getElementById('levels-coins').textContent = Storage.getCoins();
  },

  // ---- Level Select ----
  renderLevelGrid() {
    const grid    = document.getElementById('level-grid');
    grid.innerHTML = '';
    const total   = Levels.total() || GAME_CONFIG.totalLevels;
    const current = Storage.getCurrentLevel();
    for (let n = 1; n <= total; n++) {
      const btn       = document.createElement('button');
      btn.className   = 'level-cell';
      const unlocked  = Storage.isUnlocked(n);
      const completed = Storage.isCompleted(n);
      if (completed)      btn.classList.add('completed');
      else if (!unlocked) btn.classList.add('locked');
      if (n === current)  btn.classList.add('current');
      btn.innerHTML = unlocked ? n : '<span class="lock-icon">🔒</span>';
      if (unlocked) btn.addEventListener('click', () => { Audio_.click(); this.openLevel(n); });
      grid.appendChild(btn);
    }
    document.getElementById('levels-coins').textContent = Storage.getCoins();
  },

  openLevel(n) {
    Storage.setCurrentLevel(n);
    Router.show('screen-game');
    this.startLevel(n);
  },

  // ---- Level Lifecycle ----
  startLevel(n) {
    const data = Levels.get(n);
    if (!data) { alert('Level data missing.'); Router.show('screen-levels'); return; }
    this.currentLevel  = n;
    this.levelData     = data;
    this.presentPieces = new Set(data.pieces.map(p => p.id));
    this.locked         = false;
    Storage.restoreLives();
    document.getElementById('game-level-num').textContent = n;
    document.getElementById('game-diff-tag').textContent  = data.difficulty;
    document.getElementById('btn-next').disabled = true;
    this.renderLives();
    this.renderBoard();
    this.hidePopups();
  },

  renderLives() {
    const wrap  = document.getElementById('lives-display');
    const lives = Storage.getLives();
    wrap.innerHTML = '';
    for (let i = 0; i < GAME_CONFIG.maxLives; i++) {
      const s = document.createElement('span');
      s.className   = 'heart';
      s.textContent = i < lives ? '❤️' : '🤍';
      wrap.appendChild(s);
    }
  },

  loseLifeAnimation() {
    const wrap   = document.getElementById('lives-display');
    const hearts = wrap.querySelectorAll('.heart');
    const before = Storage.getLives();
    Storage.loseLife();
    if (hearts[before - 1]) hearts[before - 1].classList.add('lost');
    setTimeout(() => this.renderLives(), 450);
  },

  // ---- Board Rendering ----
  renderBoard() {
    const data  = this.levelData;
    const board = document.getElementById('maze-board');
    board.innerHTML = '';

    const wrap = document.querySelector('.maze-wrap');
    const availW = wrap.clientWidth - 12;
    const availH = wrap.clientHeight - 12;
    // fit the rectangular grid (rows may differ from cols) within the
    // available space by taking the tighter constraint on each axis
    const cellByW = Math.floor(availW / data.cols);
    const cellByH = Math.floor(availH / data.rows);
    const cell = Math.max(6, Math.min(cellByW, cellByH, 48));
    this.cellSize = cell;
    const W = cell * data.cols;
    const H = cell * data.rows;

    board.style.width    = W + 'px';
    board.style.height   = H + 'px';
    board.style.position = 'relative';

    const svgNS = 'http://www.w3.org/2000/svg';
    const svg   = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('width',   W);
    svg.setAttribute('height',  H);
    svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
    svg.style.cssText = 'position:absolute;inset:0;touch-action:none;overflow:visible';
    svg.id = 'maze-svg';

    // thick, rounded stroke — bold neon look
    const stroke = Math.max(3, Math.min(6, cell * 0.17));
    const headR  = Math.max(3.5, cell * 0.19);

    // draw non-target pieces first, target piece last so it renders
    // on top when lines overlap visually near edges
    const nonTarget = data.pieces.filter(p => p.id !== data.targetId);
    const target    = data.pieces.find(p => p.id === data.targetId);

    nonTarget.forEach(piece => {
      if (this.presentPieces.has(piece.id)) {
        const free = this.isPieceFree(piece.id);
        this.drawPiece(svg, piece, cell, stroke, headR, COLOR.idle, false, free);
      }
    });
    if (target && this.presentPieces.has(target.id)) {
      const free = this.isPieceFree(target.id);
      this.drawPiece(svg, target, cell, stroke * 1.3, headR * 1.15, COLOR.target, true, free);
    }

    board.appendChild(svg);
  },

  // Is this piece's exit lane currently completely clear? Used to
  // highlight every extractable piece (not just after a hint), so
  // the player always sees what's available without guessing.
  isPieceFree(pieceId) {
    const data  = this.levelData;
    const piece = data.pieces.find(p => p.id === pieceId);
    if (!piece) return false;
    const [hr, hc] = piece.cells[piece.cells.length - 1];
    const lane = this.exitLaneCells(hr, hc, piece.headDir, data.rows, data.cols);
    const occ  = this.occupiedCells(pieceId);
    return !lane.some(([r, c]) => occ.has(`${r},${c}`));
  },

  drawPiece(svg, piece, cell, stroke, headRadius, color, isTarget, isFree) {
    const svgNS = 'http://www.w3.org/2000/svg';
    const g = document.createElementNS(svgNS, 'g');
    g.dataset.pieceId = piece.id;
    g.classList.add('piece-group');
    if (isTarget) g.classList.add('piece-target');
    if (isFree) g.classList.add('piece-free');
    g.style.cursor = 'pointer';

    const pts  = piece.cells.map(([r,c]) => [c*cell + cell/2, r*cell + cell/2]);
    const pStr = pts.map(p => p.join(',')).join(' ');

    // soft cast shadow beneath the tube, offset slightly, for depth
    const shadowLine = document.createElementNS(svgNS, 'polyline');
    shadowLine.setAttribute('points',        pStr);
    shadowLine.setAttribute('stroke',        'rgba(0,0,0,0.32)');
    shadowLine.setAttribute('stroke-width',  stroke * 1.2);
    shadowLine.setAttribute('stroke-linecap',  'round');
    shadowLine.setAttribute('stroke-linejoin', 'round');
    shadowLine.setAttribute('fill', 'none');
    shadowLine.setAttribute('transform', 'translate(1.4,2)');
    shadowLine.classList.add('piece-shadow');
    g.appendChild(shadowLine);

    const line = document.createElementNS(svgNS, 'polyline');
    line.setAttribute('points',       pStr);
    line.setAttribute('stroke',       color);
    line.setAttribute('stroke-width', stroke);
    line.setAttribute('stroke-linecap',  'round');
    line.setAttribute('stroke-linejoin', 'round');
    line.setAttribute('fill', 'none');
    line.classList.add('piece-line');
    line.dataset.pieceId = piece.id;
    g.appendChild(line);

    // glossy highlight running down the centre of the tube
    const coreLine = document.createElementNS(svgNS, 'polyline');
    coreLine.setAttribute('points',       pStr);
    coreLine.setAttribute('stroke',       'rgba(255,255,255,0.65)');
    coreLine.setAttribute('stroke-width', Math.max(1, stroke * 0.32));
    coreLine.setAttribute('stroke-linecap',  'round');
    coreLine.setAttribute('stroke-linejoin', 'round');
    coreLine.setAttribute('fill', 'none');
    coreLine.classList.add('piece-core');
    g.appendChild(coreLine);

    const [hx, hy] = pts[pts.length - 1];
    const angle    = {N:-90, S:90, E:0, W:180}[piece.headDir];
    const aLen = headRadius * 2.4, aWid = headRadius * 0.9;

    const arrowShadow = document.createElementNS(svgNS, 'polygon');
    arrowShadow.setAttribute('points', `${aLen},0 ${-aLen*0.25},${aWid} ${-aLen*0.25},${-aWid}`);
    arrowShadow.setAttribute('transform', `translate(${hx+1.4},${hy+2}) rotate(${angle})`);
    arrowShadow.setAttribute('fill', 'rgba(0,0,0,0.32)');
    arrowShadow.classList.add('piece-arrow-shadow');
    g.appendChild(arrowShadow);

    const arrowPoly = document.createElementNS(svgNS, 'polygon');
    arrowPoly.setAttribute('points',    `${aLen},0 ${-aLen*0.25},${aWid} ${-aLen*0.25},${-aWid}`);
    arrowPoly.setAttribute('transform', `translate(${hx},${hy}) rotate(${angle})`);
    arrowPoly.setAttribute('fill',      color);
    arrowPoly.classList.add('piece-arrow');
    arrowPoly.dataset.pieceId = piece.id;
    g.appendChild(arrowPoly);

    // small glossy sliver on the arrowhead to match the tube highlight
    const arrowCore = document.createElementNS(svgNS, 'polygon');
    arrowCore.setAttribute('points', `${aLen*0.82},0 ${-aLen*0.05},${aWid*0.32} ${-aLen*0.05},${-aWid*0.32}`);
    arrowCore.setAttribute('transform', `translate(${hx},${hy}) rotate(${angle})`);
    arrowCore.setAttribute('fill', 'rgba(255,255,255,0.55)');
    arrowCore.classList.add('piece-arrow-core');
    g.appendChild(arrowCore);

    // invisible fat hit-area for easy tapping despite thin visible line
    const hitLine = document.createElementNS(svgNS, 'polyline');
    hitLine.setAttribute('points',       pStr);
    hitLine.setAttribute('stroke',       'transparent');
    hitLine.setAttribute('stroke-width', Math.max(Math.min(cell * 0.6, 30), 20));
    hitLine.setAttribute('fill',         'none');
    hitLine.dataset.pieceId = piece.id;
    g.appendChild(hitLine);

    g.addEventListener('pointerdown', (e) => {
      e.stopPropagation();
      this.handlePieceTap(piece.id);
    });

    svg.appendChild(g);
  },

  // ---- Tap = direct extract attempt (any order, no select step) ----
  handlePieceTap(pieceId) {
    if (this.locked) return;
    if (!this.presentPieces.has(pieceId)) return;
    this.attemptExtract(pieceId);
  },

  exitLaneCells(headR, headC, dir, rows, cols) {
    const [dr,dc] = DIR_DELTA[dir];
    const cells   = [];
    let r = headR+dr, c = headC+dc;
    while (r>=0 && c>=0 && r<rows && c<cols) { cells.push([r,c]); r+=dr; c+=dc; }
    return cells;
  },

  occupiedCells(excludeId) {
    const occ = new Set();
    this.levelData.pieces.forEach(p => {
      if (p.id === excludeId || !this.presentPieces.has(p.id)) return;
      p.cells.forEach(([r,c]) => occ.add(`${r},${c}`));
    });
    return occ;
  },

  attemptExtract(pieceId) {
    const data  = this.levelData;
    const piece = data.pieces.find(p => p.id === pieceId);
    if (!piece) return;

    const [headR, headC] = piece.cells[piece.cells.length - 1];
    const lane    = this.exitLaneCells(headR, headC, piece.headDir, data.rows, data.cols);
    const occ     = this.occupiedCells(pieceId);
    const blocked = lane.some(([r,c]) => occ.has(`${r},${c}`));

    if (blocked) {
      this.blockedMove(pieceId);
    } else {
      this.extractPiece(pieceId, piece.id === data.targetId);
    }
  },

  extractPiece(pieceId, isTarget) {
    if (isTarget) Audio_.target(); else Audio_.extract();

    const svg  = document.getElementById('maze-svg');
    const g    = svg && svg.querySelector(`g.piece-group[data-piece-id="${pieceId}"]`);

    this.presentPieces.delete(pieceId);

    if (g) {
      // disable further taps on this piece immediately (it's already
      // committed to leaving, even while the retraction plays out)
      g.style.pointerEvents = 'none';
      this.animateSnakeRetract(g, () => {
        g.remove();
        this.refreshFreeHighlights();
        if (isTarget || this.presentPieces.size === 0) {
          this.locked = true;
          setTimeout(() => this.completeLevel(), 150);
        }
      });
    } else if (isTarget || this.presentPieces.size === 0) {
      this.locked = true;
      setTimeout(() => this.completeLevel(), 150);
    }
  },

  // Re-check every still-present piece's free/blocked status and
  // update its glow class in place (no full re-render, so it doesn't
  // interrupt any other piece's in-flight retraction animation).
  refreshFreeHighlights() {
    const svg = document.getElementById('maze-svg');
    if (!svg) return;
    this.presentPieces.forEach(pid => {
      const g = svg.querySelector(`g.piece-group[data-piece-id="${pid}"]`);
      if (!g) return;
      const free = this.isPieceFree(pid);
      g.classList.toggle('piece-free', free);
    });
  },

  // Snake-style retraction: the piece's line "undraws" itself from the
  // tail toward the head (as if the whole body is being pulled out
  // through its own path), then the arrowhead pops out at the very end.
  // Implemented via stroke-dasharray/dashoffset on the visible polyline.
  animateSnakeRetract(g, onDone) {
    const line   = g.querySelector('.piece-line');
    const core   = g.querySelector('.piece-core');
    const shadow = g.querySelector('.piece-shadow');
    const arrow       = g.querySelector('.piece-arrow');
    const arrowCore   = g.querySelector('.piece-arrow-core');
    const arrowShadow = g.querySelector('.piece-arrow-shadow');
    if (!line) { onDone(); return; }

    const length = line.getTotalLength();
    [line, core, shadow].forEach(el => {
      if (!el) return;
      el.style.strokeDasharray = `${length} ${length}`;
      el.style.strokeDashoffset = '0';
    });

    const DURATION = 260; // ms — snappy but visible retraction
    const start = performance.now();

    function step(now) {
      const t = Math.min(1, (now - start) / DURATION);
      // ease-in: starts slow, accelerates (feels like a quick withdrawal)
      const eased = t * t;
      const offset = `${-length * eased}`;
      line.style.strokeDashoffset = offset;
      if (core)   core.style.strokeDashoffset   = offset;
      if (shadow) shadow.style.strokeDashoffset = offset;
      if (t < 1) {
        requestAnimationFrame(step);
      } else {
        if (arrow)       arrow.style.opacity       = '0';
        if (arrowCore)   arrowCore.style.opacity   = '0';
        if (arrowShadow) arrowShadow.style.opacity = '0';
        onDone();
      }
    }
    requestAnimationFrame(step);

    // arrowhead stays put until the very end, then vanishes with the
    // last bit of the line (feels like the head is the last thing out)
    if (arrow) {
      arrow.style.transition = `opacity ${DURATION * 0.15}ms ease`;
    }
    if (arrowCore)   arrowCore.style.transition   = `opacity ${DURATION * 0.15}ms ease`;
    if (arrowShadow) arrowShadow.style.transition = `opacity ${DURATION * 0.15}ms ease`;
  },

  blockedMove(pieceId) {
    this.locked = true;
    Audio_.blocked();
    if (navigator.vibrate) navigator.vibrate(GAME_CONFIG.vibrateOnWrongMs);

    const svg = document.getElementById('maze-svg');
    const g   = svg && svg.querySelector(`g.piece-group[data-piece-id="${pieceId}"]`);
    if (g) {
      g.classList.add('piece-blocked-shake');
      setTimeout(() => g.classList.remove('piece-blocked-shake'), 500);
    }

    this.showToast('popup-wrong-move', GAME_CONFIG.wrongMoveFlashMs + 200);
    this.loseLifeAnimation();

    setTimeout(() => {
      this.locked = false;
      if (Storage.getLives() <= 0) this.gameOver();
    }, GAME_CONFIG.wrongMoveFlashMs + 250);
  },

  // ---- Hint: flash any ONE currently-extractable non-target piece ----
  useHint() {
    if (this.locked) return;
    if (!Storage.spendCoins(GAME_CONFIG.hintCost)) { this.flashInsufficientCoins(); return; }
    Audio_.click();
    this.refreshHomeStats();

    const data = this.levelData;
    // find any currently-free piece (its exit lane is clear right now)
    let freeId = null;
    for (const piece of data.pieces) {
      if (!this.presentPieces.has(piece.id)) continue;
      const [headR, headC] = piece.cells[piece.cells.length - 1];
      const lane = this.exitLaneCells(headR, headC, piece.headDir, data.rows, data.cols);
      const occ  = this.occupiedCells(piece.id);
      const blocked = lane.some(([r,c]) => occ.has(`${r},${c}`));
      if (!blocked) { freeId = piece.id; break; }
    }
    if (freeId === null) return;

    const svg   = document.getElementById('maze-svg');
    const line  = svg && svg.querySelector(`.piece-line[data-piece-id="${freeId}"]`);
    const arrow = svg && svg.querySelector(`.piece-arrow[data-piece-id="${freeId}"]`);
    const isTgt = freeId === data.targetId;
    if (line)  line.classList.add('hint-pulse');
    if (arrow) arrow.classList.add('hint-pulse');
    if (!isTgt) {
      if (line)  line.setAttribute('stroke', COLOR.hint);
      if (arrow) arrow.setAttribute('fill', COLOR.hint);
    }

    setTimeout(() => {
      if (line)  { line.classList.remove('hint-pulse'); if (!isTgt && this.presentPieces.has(freeId)) line.setAttribute('stroke', COLOR.idle); }
      if (arrow) { arrow.classList.remove('hint-pulse'); if (!isTgt && this.presentPieces.has(freeId)) arrow.setAttribute('fill', COLOR.idle); }
    }, 2500);
  },

  flashInsufficientCoins() {
    const btn = document.getElementById('btn-hint');
    btn.style.background = '#fecaca';
    setTimeout(() => { btn.style.background = ''; }, 300);
  },

  // ---- Win / Lose ----
  completeLevel() {
    Audio_.success();
    const { reward } = getLevelRewardConfig(this.currentLevel);
    Storage.addCoins(reward);
    Storage.completeLevel(this.currentLevel);
    Audio_.coin();
    document.getElementById('coins-earned').textContent = `+${reward}`;
    document.getElementById('btn-next').disabled = (this.currentLevel >= Levels.total());
    this.showPopup('popup-level-complete');
  },

  gameOver() {
    Audio_.fail();
    this.showPopup('popup-game-over');
  },

  goToNextLevel() {
    const next = this.currentLevel + 1;
    if (next > Levels.total()) { Router.show('screen-levels'); this.renderLevelGrid(); return; }
    this.hidePopups();
    this.startLevel(next);
  },

  // ---- Popups ----
  toastTimer: null,

  showPopup(id) {
    if (this.toastTimer) { clearTimeout(this.toastTimer); this.toastTimer = null; }
    document.querySelectorAll('.popup').forEach(p => p.hidden = true);
    document.getElementById('popup-overlay').style.background = '';
    document.getElementById('popup-overlay').hidden = false;
    document.getElementById(id).hidden = false;
  },
  showToast(id, ms) {
    if (this.toastTimer) clearTimeout(this.toastTimer);
    document.querySelectorAll('.popup').forEach(p => p.hidden = true);
    document.getElementById('popup-overlay').hidden = false;
    document.getElementById('popup-overlay').style.background = 'transparent';
    document.getElementById(id).hidden = false;
    this.toastTimer = setTimeout(() => {
      document.getElementById(id).hidden = true;
      document.getElementById('popup-overlay').style.background = '';
      if (this.allPopupsHidden()) document.getElementById('popup-overlay').hidden = true;
      this.toastTimer = null;
    }, ms);
  },
  hidePopups() {
    if (this.toastTimer) { clearTimeout(this.toastTimer); this.toastTimer = null; }
    document.querySelectorAll('.popup').forEach(p => p.hidden = true);
    document.getElementById('popup-overlay').hidden = true;
    document.getElementById('popup-overlay').style.background = '';
  },
  allPopupsHidden() {
    return [...document.querySelectorAll('.popup')].every(p => p.hidden);
  },

  handlePopupAction(action) {
    Audio_.click();
    switch (action) {
      case 'next-level':  this.hidePopups(); this.goToNextLevel(); break;
      case 'to-levels':   this.hidePopups(); Router.show('screen-levels'); this.renderLevelGrid(); break;
      case 'retry':       this.hidePopups(); this.startLevel(this.currentLevel); break;
      case 'watch-ad':
        Storage.setLives(GAME_CONFIG.rewardedAdGrantsLives);
        this.renderLives(); this.hidePopups(); this.locked = false;
        break;
    }
  },

  // ---- PWA ----
  registerServiceWorker() {
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('service-worker.js')
          .catch(err => console.warn('[SW] failed:', err));
      });
    }
  },
  setupInstallPrompt() {
    let deferredPrompt = null;
    const popup = document.getElementById('popup-install');

    window.addEventListener('beforeinstallprompt', (e) => {
      e.preventDefault();
      deferredPrompt = e;
      // small delay so it doesn't appear the instant the page loads
      setTimeout(() => {
        if (deferredPrompt && !Storage.getInstallDismissed()) {
          this.showPopup('popup-install');
        }
      }, 2500);
    });

    document.getElementById('btn-install-accept').addEventListener('click', async () => {
      this.hidePopups();
      if (!deferredPrompt) return;
      deferredPrompt.prompt();
      await deferredPrompt.userChoice;
      deferredPrompt = null;
    });

    document.getElementById('btn-install-dismiss').addEventListener('click', () => {
      this.hidePopups();
      Storage.setInstallDismissed(true);
    });

    window.addEventListener('appinstalled', () => {
      this.hidePopups();
      deferredPrompt = null;
    });
  },
};

window.addEventListener('resize', () => {
  if (document.getElementById('screen-game').classList.contains('active') && Game.levelData) {
    Game.renderBoard();
  }
});

document.addEventListener('DOMContentLoaded', () => Game.init());
