import * as vscode from 'vscode';
import * as fs from 'fs';
import * as http from 'http';
import * as path from 'path';
import * as zlib from 'zlib';
import { getToolWebviewStyles, singletonPanel } from './shared';

const store: { panel: vscode.WebviewPanel | null } = { panel: null };

interface RuntimePayload {
  fileName: string;
  zipSize: number;
  html: string;
  checks: CheckResult[];
}

let runtimePayload: RuntimePayload | null = null;
let runtimeServer: http.Server | null = null;
let runtimePort: number | null = null;

// ─── ZIP Parser (pure Node, no external deps) ─────────────────────────────────

interface ZipEntry {
  name: string;
  compressedSize: number;
  uncompressedSize: number;
  data: Buffer;
}

function parseZip(buf: Buffer): ZipEntry[] {
  const EOCD_SIG = 0x06054b50;
  const CD_SIG   = 0x02014b50;
  const LFH_SIG  = 0x04034b50;

  // Scan backwards for End of Central Directory record
  let eocd = -1;
  for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65558); i--) {
    if (buf.readUInt32LE(i) === EOCD_SIG) { eocd = i; break; }
  }
  if (eocd < 0) throw new Error('Not a valid ZIP file (EOCD not found)');

  const count    = buf.readUInt16LE(eocd + 10);
  const cdOffset = buf.readUInt32LE(eocd + 16);
  const entries: ZipEntry[] = [];
  let pos = cdOffset;

  for (let n = 0; n < count; n++) {
    if (pos + 46 > buf.length || buf.readUInt32LE(pos) !== CD_SIG) break;

    const method     = buf.readUInt16LE(pos + 10);
    const cmpSize    = buf.readUInt32LE(pos + 20);
    const ucmpSize   = buf.readUInt32LE(pos + 24);
    const nameLen    = buf.readUInt16LE(pos + 28);
    const extraLen   = buf.readUInt16LE(pos + 30);
    const commentLen = buf.readUInt16LE(pos + 32);
    const lfhOff     = buf.readUInt32LE(pos + 42);
    const name       = buf.slice(pos + 46, pos + 46 + nameLen).toString('utf8');
    pos += 46 + nameLen + extraLen + commentLen;

    if (name.endsWith('/')) continue;

    if (lfhOff + 30 > buf.length || buf.readUInt32LE(lfhOff) !== LFH_SIG) continue;
    const lfhNameLen  = buf.readUInt16LE(lfhOff + 26);
    const lfhExtraLen = buf.readUInt16LE(lfhOff + 28);
    const dataStart   = lfhOff + 30 + lfhNameLen + lfhExtraLen;
    if (dataStart + cmpSize > buf.length) continue;

    const raw = buf.slice(dataStart, dataStart + cmpSize);
    let data: Buffer;
    try {
      data = method === 0 ? raw : method === 8 ? zlib.inflateRawSync(raw) : Buffer.alloc(0);
    } catch { data = Buffer.alloc(0); }

    entries.push({ name, compressedSize: cmpSize, uncompressedSize: ucmpSize, data });
  }
  return entries;
}

// ─── Static Checks ────────────────────────────────────────────────────────────

interface CheckResult {
  id: string;
  status: 'pass' | 'fail' | 'warn';
  detail: string;
}

const MTG_ZIP_LIMIT  = 5 * 1024 * 1024;
const MTG_HTML_LIMIT = 5 * 1024 * 1024;

function runStaticChecks(html: string, entries: ZipEntry[], zipSize: number): CheckResult[] {
  const scripts = extractScripts(html);
  return [
    checkHtmlRequirements(html),
    checkCtaMethod(scripts),
    checkGameEnd(scripts),
    checkGameReady(html, scripts),
    checkGameStart(scripts),
    checkGameClose(scripts),
    checkFileHandling(html),
    checkFileSpec(entries, zipSize),
    checkStorage(scripts),
  ];
}

function extractScripts(html: string): string {
  const parts: string[] = [];
  const re = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
  let m: RegExpExecArray | null;
  while ((m = re.exec(html)) !== null) parts.push(m[1]);
  return parts.join('\n');
}

// 1. HTML Requirements
function checkHtmlRequirements(html: string): CheckResult {
  const issues: string[] = [];
  if (!/^\s*<!DOCTYPE\s+html\s*>/i.test(html))                              issues.push('Missing <!DOCTYPE html>');
  if (!/<meta\b[^>]*\bcharset\s*=/i.test(html))                             issues.push('Missing charset meta tag');
  if (!/<meta\b[^>]*\bname\s*=\s*["']viewport["']/i.test(html))            issues.push('Missing viewport meta tag');
  if (/<script\b[^>]*\btype\s*=\s*["']module["']/i.test(html))             issues.push('type="module" on script tag');
  if (/<script\b[^>]*\bcrossorigin\b/i.test(html))                          issues.push('crossorigin attribute on script tag');
  if (!/<\/html\s*>/i.test(html))                                           issues.push('Missing </html> closing tag');
  return issues.length === 0
    ? { id: 'html-requirements', status: 'pass',  detail: 'Valid HTML structure' }
    : { id: 'html-requirements', status: 'fail',  detail: issues.join('; ') };
}

// 2. CTA Call Method
function checkCtaMethod(scripts: string): CheckResult {
  if (/\bwindow\.install\b|\btypeof\s+window\.install/.test(scripts)) {
    return { id: 'cta-method', status: 'pass', detail: 'window.install() referenced in scripts' };
  }
  return { id: 'cta-method', status: 'fail', detail: 'window.install() not found — Mintegral CTA requires window.install()' };
}

// 3. Game End Call Method
function checkGameEnd(scripts: string): CheckResult {
  return /\bgameEnd\b/.test(scripts)
    ? { id: 'game-end', status: 'pass', detail: 'gameEnd() found in scripts' }
    : { id: 'game-end', status: 'fail', detail: 'gameEnd() not found' };
}

// 4. Game Ready Call Method (Mintegral-specific: must be in body onload)
function checkGameReady(html: string, scripts: string): CheckResult {
  const inScript = /\bgameReady\b/.test(scripts);
  const inOnload = /\bonload\s*=\s*["'][^"']*\bgameReady\b/i.test(html);
  if (!inScript && !inOnload) {
    return { id: 'game-ready', status: 'fail', detail: 'gameReady() not found' };
  }
  if (!inOnload) {
    return { id: 'game-ready', status: 'warn', detail: 'gameReady() found but not wired to body onload — Mintegral requires onload="gameReady()"' };
  }
  return { id: 'game-ready', status: 'pass', detail: 'gameReady() wired to body onload' };
}

// 5. Game Start Call Method
function checkGameStart(scripts: string): CheckResult {
  return /\bgameStart\b/.test(scripts)
    ? { id: 'game-start', status: 'pass', detail: 'gameStart() found in scripts' }
    : { id: 'game-start', status: 'fail', detail: 'gameStart() not found' };
}

// 6. Game Close Call Method
function checkGameClose(scripts: string): CheckResult {
  return /\bgameClose\b/.test(scripts)
    ? { id: 'game-close', status: 'pass', detail: 'gameClose() found in scripts' }
    : { id: 'game-close', status: 'fail', detail: 'gameClose() not found' };
}

// 7. File Handling Method (no external URLs)
function checkFileHandling(html: string): CheckResult {
  const re = /(?:src|href|action)\s*=\s*["'](https?:\/\/[^"']+)["']|url\s*\(\s*["']?(https?:\/\/[^)"'\s]+)/gi;
  const found: string[] = [];
  let m: RegExpExecArray | null;
  while ((m = re.exec(html)) !== null) found.push(m[1] || m[2]);
  if (found.length === 0) {
    return { id: 'file-handling', status: 'pass', detail: 'All assets are inline — no external URLs detected' };
  }
  const unique = [...new Set(found)];
  return {
    id: 'file-handling',
    status: 'fail',
    detail: `${unique.length} external URL(s): ${unique.slice(0, 2).join(', ')}${unique.length > 2 ? ` (+${unique.length - 2} more)` : ''}`,
  };
}

// 8. File Spec (ZIP structure + size limits)
function checkFileSpec(entries: ZipEntry[], zipSize: number): CheckResult {
  const issues: string[] = [];
  const indexEntry = entries.find(e => e.name === 'index.html');

  if (!indexEntry) {
    const nested = entries.find(e => /\/index\.html$/i.test(e.name));
    issues.push(nested
      ? `index.html found at '${nested.name}' — must be at ZIP root`
      : 'index.html not found in ZIP');
  }
  if (zipSize > MTG_ZIP_LIMIT) {
    issues.push(`ZIP size ${(zipSize / 1024 / 1024).toFixed(2)} MB exceeds 5 MB limit`);
  }
  if (indexEntry && indexEntry.uncompressedSize > MTG_HTML_LIMIT) {
    issues.push(`index.html ${(indexEntry.uncompressedSize / 1024 / 1024).toFixed(2)} MB uncompressed exceeds 5 MB`);
  }

  if (issues.length > 0) {
    return { id: 'file-spec', status: 'fail', detail: issues.join('; ') };
  }

  const extras  = entries.filter(e => e.name !== 'index.html');
  const sizeStr = `index.html ${(indexEntry!.uncompressedSize / 1024).toFixed(0)} KB, ZIP ${(zipSize / 1024).toFixed(0)} KB`;
  return {
    id: 'file-spec',
    status: extras.length > 0 ? 'warn' : 'pass',
    detail: extras.length > 0
      ? `${sizeStr}; ${extras.length} extra file(s) in ZIP (${extras.map(e => e.name).join(', ')})`
      : sizeStr,
  };
}

// 9. Storage Requirements — only flag explicit writes; engine-internal reads (Unity shader cache etc.) are allowed
function checkStorage(scripts: string): CheckResult {
  const issues: string[] = [];
  if (/\blocalStorage\s*\.\s*setItem\s*\(/.test(scripts))   issues.push('localStorage.setItem');
  if (/\bsessionStorage\s*\.\s*setItem\s*\(/.test(scripts)) issues.push('sessionStorage.setItem');
  if (/\bdocument\.cookie\s*=/.test(scripts))               issues.push('document.cookie write');
  return issues.length === 0
    ? { id: 'storage', status: 'pass', detail: 'No persistent storage writes detected' }
    : { id: 'storage', status: 'fail', detail: `Forbidden storage write(s): ${issues.join(', ')}` };
}

// ─── Monitoring Stub Injection ─────────────────────────────────────────────────

function injectMonitoringStubs(html: string): string {
  const stub = '<script src="/preview-util.js"></script>\n<script src="/monitor.js"></script>';
  if (/<head\b[^>]*>/i.test(html)) {
    return html.replace(/<head\b[^>]*>/i, m => m + '\n' + stub);
  }
  return stub + '\n' + html;
}

function getMonitoringScript(): string {
  // Architecture:
  //
  // The real Mintegral SDK (preview-util.js) defines gameReady/gameEnd/install as global
  // function declarations (brace depth 0).  These REPLACE any window.X = fn assignment
  // we make before them, so Object.defineProperty interceptors on window.gameReady etc.
  // are overwritten before the AD ever calls them.
  //
  // Instead we intercept at two points that the SDK cannot overwrite:
  //   1.  window.MW_INIT assignment — SDK does `window.MW_INIT = (()=>{...})()`.
  //       Our Object.defineProperty setter fires, wraps emitCheckData, forces _hasReview()=true.
  //       emitCheckData carries every SDK check result: game_ready, game_end, game_start,
  //       game_close, install, outer_chain, base64, code.
  //   2.  window.postMessage override — SDK fires `window.postMessage({type:"previewer:gameReady"})`
  //       (self-post) inside its gameReady function.  Our override captures it.
  //
  // Fallback mocks (window.gameReady/gameEnd/install) survive for ads that don't bundle the SDK.
  // All events are pushed to window.__hplMtgEvents; the parent polls every 200 ms.
  return `(function(){
window.__hplMtgEvents=window.__hplMtgEvents||[];
function notify(n,extra){
  window.__hplMtgEvents.push({name:n,extra:extra||{}});
  try{if(typeof parent.__hplMtgNotify==='function')parent.__hplMtgNotify(n,extra||{});}catch(e){}
  try{parent.postMessage({type:'lifecycle',name:n,extra:extra||{}},'*');}catch(e){}
}
notify('__ping__');

function fnState(name){
  var t;
  try{t=typeof window[name];}catch(e){t='error';}
  return name+':'+t;
}
function diag(label, extra){
  notify('diagnostic',{label:label,detail:(extra||[
    fnState('gameReady'),fnState('gameStart'),fnState('gameEnd'),fnState('gameClose'),fnState('install')
  ].join(', '))});
}
diag('post-parse');

function stringifyLogArg(v){
  try{
    if(typeof v==='string')return v;
    if(v instanceof Error)return v.message||String(v);
    if(typeof v==='object')return JSON.stringify(v);
    return String(v);
  }catch(e){return String(v);}
}

function applyMutedState(){
  var muted=!!window.__hplMuted;
  try{
    var gains=window.__hplMasterGains||[];
    (gains||[]).forEach(function(g){try{if(g&&g.gain)g.gain.value=muted?0:1;}catch(e){}});
    if(!gains.length){
      (window.__hplAudioContexts||[]).forEach(function(ctx){
        try{
          if(muted&&ctx.state==='running')ctx.suspend();
          if(!muted&&ctx.state==='suspended')ctx.resume();
        }catch(e){}
      });
    }
    (window.__hplMediaElements||[]).forEach(function(el){try{el.muted=muted;el.volume=muted?0:1;}catch(e){}});
  }catch(e){}
}
window.__hplApplyMutedState=applyMutedState;

// ── 0. Audio capture for the simulator mute button ────────────────────────
// Most engines keep AudioContext instances inside closures. Capture them as
// they are constructed so the parent webview can mute even when no global game
// instance is exposed.
try{
  (function(){
    var OrigAC=window.AudioContext||window.webkitAudioContext;
    if(!OrigAC||OrigAC.__hplProxy)return;
    window.__hplAudioContexts=[];
    window.__hplMasterGains=[];
    window.__hplAudioDestinations=[];
    function ACProxy(opts){
      var ctx=arguments.length?new OrigAC(opts):new OrigAC();
      var dest=ctx.destination;
      var gain=null;
      try{
        gain=ctx.createGain();
        gain.gain.value=window.__hplMuted?0:1;
        gain.connect(dest);
        if(window.AudioNode&&window.AudioNode.prototype&&!window.AudioNode.prototype.connect.__hpl){
          var origConnect=window.AudioNode.prototype.connect;
          var connectProxy=function(target){
            var args=Array.prototype.slice.call(arguments);
            try{
              var idx=window.__hplAudioDestinations.indexOf(args[0]);
              if(idx>=0&&window.__hplMasterGains[idx])args[0]=window.__hplMasterGains[idx];
            }catch(e){}
            return origConnect.apply(this,args);
          };
          connectProxy.__hpl=true;
          window.AudioNode.prototype.connect=connectProxy;
        }
      }catch(e){}
      window.__hplAudioCtx=ctx;
      window.__hplMasterGain=gain;
      window.__hplAudioContexts.push(ctx);
      if(gain)window.__hplMasterGains.push(gain);
      if(gain)window.__hplAudioDestinations.push(dest);
      notify('audioContext',{state:ctx.state||'',routed:!!gain});
      return ctx;
    }
    ACProxy.prototype=OrigAC.prototype;
    Object.setPrototypeOf&&Object.setPrototypeOf(ACProxy,OrigAC);
    ACProxy.__hplProxy=true;
    window.AudioContext=ACProxy;
    window.webkitAudioContext=ACProxy;
  })();
}catch(e){}

try{
  window.__hplMediaElements=[];
  var mediaPlay=window.HTMLMediaElement&&window.HTMLMediaElement.prototype&&window.HTMLMediaElement.prototype.play;
  if(mediaPlay&&!mediaPlay.__hpl){
    var playProxy=function(){
      window.__hplMediaElements.push(this);
      applyMutedState();
      notify('audioContext',{state:'media-play',routed:true});
      return mediaPlay.apply(this,arguments);
    };
    playProxy.__hpl=true;
    window.HTMLMediaElement.prototype.play=playProxy;
  }
}catch(e){}

try{
  var _createElement=document.createElement;
  document.createElement=function(tag){
    var el=_createElement.apply(document,arguments);
    var tagName=String(tag).toLowerCase();
    if(tagName==='script'){
      notify('diagnostic',{label:'createElement(script)',detail:''});
    }else if(tagName==='audio'||tagName==='video'){
      try{window.__hplMediaElements.push(el);el.muted=!!window.__hplMuted;el.volume=window.__hplMuted?0:1;}catch(e){}
    }
    return el;
  };
}catch(e){}

['pointerdown','mousedown','touchstart','click','keydown'].forEach(function(evt){
  window.addEventListener(evt,function(){setTimeout(applyMutedState,0);},true);
});

// ── 1. Intercept window.MW_INIT assignment (SDK present) ──────────────────
// SDK does: window.MW_INIT = (()=>{...})()
// Our setter fires, wraps emitCheckData, forces review+preview modes.
// If preview-util.js already created MW_INIT, preserve that object. Replacing
// it with a null-backed accessor breaks the SDK's own gameReady().
function patchMwInit(obj){
  if(!obj||obj.__hplMwPatched)return;
  obj.__hplMwPatched=true;
  obj._hasReview=function(){return true;};
  obj._isPreview=function(){return true;};
  var _oe=obj.emitCheckData;
  if(typeof _oe==='function'){
    obj.emitCheckData=function(d){
      _oe.call(obj,d);
      if(d&&d.type)notify('sdkCheck',{type:d.type,value:d.value,msg:d.msg||''});
    };
  }
}
var _mwInit=window.MW_INIT||null;
patchMwInit(_mwInit);
try{Object.defineProperty(window,'MW_INIT',{configurable:true,enumerable:true,
  get:function(){return _mwInit;},
  set:function(obj){
    _mwInit=obj;
    patchMwInit(obj);
  }
});}catch(e){}

// ── 2. Intercept window.postMessage self-posts ─────────────────────────────
var _owp=window.postMessage;
window.postMessage=function(data,origin,transfer){
  if(data&&typeof data.type==='string'){
    var pm={
      'previewer:gameReady':'gameReady',
      'previewer:gameStart':'gameStart',
      'previewer:gameEnd':'gameEnd',
      'previewer:gameClose':'gameClose',
      'previewer:install':'install',
      'previewer:gameRetry':'gameRetry'
    };
    if(pm[data.type])notify(pm[data.type]);
  }
  return _owp.apply(this,arguments);
};

// ── 3. Accessor interceptors (Vite/IIFE AD path) ──────────────────────────
//
// Problem solved here: the AD's main.ts uses typeof guards:
//   if (typeof window.gameReady === 'undefined') { window.gameReady = bootFn; }
//
// Any early assignment (even DOMContentLoaded wrapping, which registers before
// the AD's listener) makes typeof return 'function', the guard fires, and the
// AD skips its own definition — Phaser never boots, no events ever fire.
//
// Fix: Object.defineProperty with getter returning undefined until the AD assigns.
//   typeof window.gameReady === 'undefined' → TRUE  →  AD's guard passes
//   AD assigns window.gameReady = bootFn    →  our setter wraps it
//   body.onload calls gameReady()           →  getter returns wrapper
//   wrapper: notify('gameReady') + bootFn() →  Phaser boots normally
//
// Note: SDK function declarations (function gameReady(){}) DO overwrite
// configurable accessors; MW_INIT/emitCheckData wrapping handles that path.
function mkAccess(name, onCall) {
  var existing;
  var _v;
  try {
    existing = window[name];
    Object.defineProperty(window, name, {
      configurable: true, enumerable: true,
      get: function() { return _v; },
      set: function(fn) {
        var orig = fn;
        if (typeof orig === 'function') {
          notify('diagnostic',{label:name+' assigned',detail:'wrapped'});
          var w = function() { notify('diagnostic',{label:name+' called',detail:'accessor wrapper'}); onCall(); return orig.apply(this, arguments); };
          w.__hpl = true;
          _v = w;
        } else { _v = orig; }
      }
    });
    if (typeof existing === 'function') window[name] = existing;
  } catch(e) {}
}

var _lh = {
  gameReady: function() {
    notify('gameReady');
    setTimeout(function() {
      notify('sdkCheck', {type:'game_start', value: typeof window.gameStart==='function'?1:0});
      notify('sdkCheck', {type:'game_close', value: typeof window.gameClose==='function'?1:0});
    }, 100);
  },
  gameEnd:   function() { notify('sdkCheck', {type:'game_end',   value:1}); },
  gameStart: function() { notify('sdkCheck', {type:'game_start', value:1}); },
  gameClose: function() { notify('sdkCheck', {type:'game_close', value:1}); },
  install:   function() { notify('sdkCheck', {type:'install',    value:1}); }
};

mkAccess('gameReady', _lh.gameReady);
mkAccess('gameEnd',   _lh.gameEnd);
mkAccess('gameStart', _lh.gameStart);
mkAccess('gameClose', _lh.gameClose);

// install: pre-define for SDK-absent builds; SDK's 'function install(){}' declaration overwrites this.
if (typeof window.install !== 'function') {
  var _installW = function() { _lh.install(); };
  _installW.__hpl = true;
  window.install = _installW;
}

// ── 4. Capture-phase load safety net ──────────────────────────────────────
// Fires BEFORE body.onload (which calls gameReady()).
// Wraps any SDK function declarations that overwrote our mkAccess accessors.
window.addEventListener('load', function() {
  diag('load-capture-before-wrap');
  Object.keys(_lh).forEach(function(name) {
    var fn = window[name];
    if (typeof fn === 'function' && !fn.__hpl) {
      var orig = fn, h = _lh[name];
      notify('diagnostic',{label:name+' load-wrap',detail:'function'});
      var w = function() { notify('diagnostic',{label:name+' called',detail:'load wrapper'}); h(); return orig.apply(this, arguments); };
      w.__hpl = true;
      try { window[name] = w; } catch(e) {}
    } else if (typeof fn !== 'function') {
      var ww = function() { _lh[name](); };
      ww.__hpl = true;
      try { window[name] = ww; } catch(e) {}
    }
  });
  diag('load-capture-after-wrap');
  setTimeout(function(){diag('load-plus-500ms');},500);
}, true);

document.addEventListener('DOMContentLoaded',function(){diag('DOMContentLoaded');},true);

// ── 5. Error monitoring ────────────────────────────────────────────────────
window.onerror=function(msg,src,line){
  notify('exception',{message:String(msg),line:line||0,source:src||''});
  return false;
};
window.addEventListener('unhandledrejection',function(e){
  notify('exception',{message:String(e&&e.reason||'Unhandled promise rejection')});
});
window.addEventListener('error',function(e){
  if(e&&e.target&&e.target!==window){
    var u=String(e.target.src||e.target.href||'');
    if(u&&(u.indexOf('http://')===0||u.indexOf('https://')===0))notify('resourceError',{url:u});
  }
},true);
})();`;
}

// ─── Extension Entry Point ─────────────────────────────────────────────────────

export function openMintegralChecker(_context: vscode.ExtensionContext) {
  const { panel, isNew } = singletonPanel(store, 'hplToolbox.mintegralChecker', 'Mintegral Checker');
  if (!isNew) return;

  panel.webview.html = getHtml();
  panel.webview.onDidReceiveMessage(async (msg) => {
    if (msg.type === 'pickZip') {
      const picked = await vscode.window.showOpenDialog({
        canSelectFiles: true,
        canSelectFolders: false,
        canSelectMany: false,
        filters: { ZIP: ['zip'] },
        openLabel: 'Select Mintegral ZIP',
      });
      if (picked?.[0]) await loadZip(panel, picked[0].fsPath);
    } else if (msg.type === 'reload' && msg.path) {
      await loadZip(panel, msg.path);
    }
  });
}

async function loadZip(panel: vscode.WebviewPanel, zipPath: string): Promise<void> {
  panel.webview.postMessage({ type: 'status', message: `Reading ${path.basename(zipPath)}...` });
  try {
    const buf     = fs.readFileSync(zipPath);
    const entries = parseZip(buf);
    const index   = entries.find(e => e.name === 'index.html');

    if (!index) {
      // Still run file spec check so user sees why it failed
      const checks = runStaticChecks('', entries, buf.length);
      panel.webview.postMessage({ type: 'loaded', fileName: path.basename(zipPath), zipPath, zipSize: buf.length, html: '', checks });
      return;
    }

    const html         = index.data.toString('utf8');
    const instrumented = injectMonitoringStubs(html);
    const checks       = runStaticChecks(html, entries, buf.length);
    const runtimeUrl  = await openEmbeddedRuntime({
      fileName: path.basename(zipPath),
      zipSize: buf.length,
      html: instrumented,
      checks,
    });

    panel.webview.postMessage({
      type: 'loaded',
      fileName: path.basename(zipPath),
      zipPath,
      zipSize: buf.length,
      html: '',
      checks,
      runtimeUrl,
    });
  } catch (e: any) {
    panel.webview.postMessage({ type: 'error', message: e.message || String(e) });
  }
}

async function openEmbeddedRuntime(payload: RuntimePayload): Promise<string> {
  runtimePayload = payload;
  const port = await ensureRuntimeServer();
  return `http://127.0.0.1:${port}/`;
}

function ensureRuntimeServer(): Promise<number> {
  if (runtimeServer && runtimePort) return Promise.resolve(runtimePort);

  runtimeServer = http.createServer((req, res) => {
    const url = new URL(req.url || '/', 'http://127.0.0.1');
    res.setHeader('Cache-Control', 'no-store');

    if (!runtimePayload) {
      res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
      res.end('No Mintegral ZIP loaded.');
      return;
    }

    if (url.pathname === '/ad') {
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      res.end(runtimePayload.html);
      return;
    }

    if (url.pathname === '/monitor.js') {
      res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
      res.end(getMonitoringScript());
      return;
    }

    if (url.pathname === '/preview-util.js') {
      try {
        const sdkPath = path.join(__dirname, '..', '..', 'media', 'mintegral', 'preview-util.js');
        res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
        res.end(fs.readFileSync(sdkPath));
      } catch (e: any) {
        res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
        res.end(e?.message || 'preview-util.js not found');
      }
      return;
    }

    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.end(getExternalRuntimeHtml(runtimePayload));
  });

  return new Promise((resolve, reject) => {
    runtimeServer!.once('error', reject);
    runtimeServer!.listen(0, '127.0.0.1', () => {
      const addr = runtimeServer!.address();
      runtimePort = typeof addr === 'object' && addr ? addr.port : 0;
      resolve(runtimePort);
    });
  });
}

function getExternalRuntimeHtml(payload: RuntimePayload): string {
  const boot = JSON.stringify({
    fileName: payload.fileName,
    zipSize: payload.zipSize,
    checks: payload.checks,
  });

  return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Mintegral Runtime Checker</title>
<style>
:root{color-scheme:dark;--gap-xs:4px;--gap-sm:8px;--gap-md:12px;--gap-lg:16px;--radius:4px;--border:var(--vscode-panel-border,rgba(128,128,128,.35));--panel:var(--vscode-editorWidget-background,var(--vscode-sideBar-background));--panel-soft:var(--vscode-textBlockQuote-background,rgba(128,128,128,.08));--fg:var(--vscode-foreground,#d4d4d4);--muted:var(--vscode-descriptionForeground,#9d9d9d);--ok:var(--vscode-testing-iconPassed,#3fb950);--err:var(--vscode-errorForeground,#f85149);--warn:var(--vscode-editorWarning-foreground,#d29922)}
*{box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden}
body{margin:0;background:var(--vscode-editor-background,#1e1e1e);color:var(--fg);font-family:var(--vscode-font-family,Segoe UI,Arial,sans-serif);font-size:var(--vscode-font-size,13px)}
main{height:100vh;display:grid;grid-template-columns:minmax(360px,1fr) minmax(320px,420px);grid-template-rows:minmax(0,1fr);gap:var(--gap-md);padding:var(--gap-md);align-items:stretch;overflow:hidden}
.panel{border:1px solid var(--border);background:var(--panel-soft);border-radius:var(--radius);overflow:hidden}
.head{display:flex;align-items:center;justify-content:space-between;gap:var(--gap-md);padding:10px 12px;border-bottom:1px solid var(--border);background:var(--panel)}
h1{margin:0;color:var(--fg);font-size:12px;font-weight:700;letter-spacing:.45px;text-transform:uppercase}.sub{color:var(--muted);font-size:12px;overflow-wrap:anywhere}.body{padding:var(--gap-md)}
.head-meta{display:flex;align-items:center;justify-content:flex-end;gap:6px;min-width:0;white-space:nowrap}.head-meta .sub{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.head-meta .sub+.sub::before{content:"|";margin-right:6px;color:var(--border)}.title-actions{display:flex;align-items:center;gap:var(--gap-xs);flex:0 0 auto}
ol{list-style:none;margin:0;padding:0;overflow:auto}.check{display:flex;gap:10px;padding:10px 12px;border-bottom:1px solid var(--border)}.check:last-child{border-bottom:0}
.num{flex:0 0 22px;height:22px;border:1px solid var(--border);border-radius:50%;display:grid;place-items:center;color:var(--muted);background:var(--panel);font-size:10px;font-weight:700}
.label{font-size:12px;font-weight:600}.detail{margin-top:3px;color:var(--muted);font-size:11px;line-height:1.35;overflow-wrap:anywhere}
.badge{display:inline-flex;align-items:center;min-height:18px;margin-left:6px;padding:1px 6px;border:1px solid var(--border);border-radius:999px;font-size:11px;line-height:1.2;color:var(--muted)}
.badge.pass{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:rgba(63,185,80,.12)}.badge.fail{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);background:var(--vscode-inputValidation-errorBackground,rgba(248,81,73,.12))}.badge.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent);background:var(--vscode-inputValidation-warningBackground,rgba(210,153,34,.12))}.badge.waiting{color:var(--muted)}
.runtime-side{display:grid;grid-template-rows:minmax(0,1fr);gap:var(--gap-md);min-height:0}
.playable-panel{min-height:0;display:flex;flex-direction:column}
.playable-body{min-height:0;flex:1;display:flex;flex-direction:column;padding:10px}
.preview-stage{min-height:0;flex:1;display:grid;place-items:center;overflow:hidden;container-type:size}
.device{width:min(100cqw, calc(100cqh * .5625));max-width:100%;aspect-ratio:9/16;margin:auto}
@supports not (width: 100cqw){.device{height:min(100%,calc((100vw - 500px) * 1.777));max-height:calc(100vh - 106px)}}
.screen{position:relative;width:100%;height:100%;background:#000;border-radius:4px;overflow:hidden}
iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:#000}
button{min-height:28px;padding:5px 12px;border:1px solid transparent;border-radius:var(--radius);background:var(--vscode-button-background,#0e639c);color:var(--vscode-button-foreground,#fff);font:inherit;cursor:pointer}button:hover{background:var(--vscode-button-hoverBackground,#1177bb)}
.icon-btn{width:28px;height:28px;min-height:28px;padding:0;display:grid;place-items:center;font-size:15px;line-height:1}.icon-btn[aria-pressed="true"]{color:var(--ok)}
.icon-btn,.tab{background:var(--vscode-button-secondaryBackground,#3a3d41);color:var(--vscode-button-secondaryForeground,var(--fg));border-color:var(--border)}.icon-btn:hover,.tab:hover{background:var(--vscode-button-secondaryHoverBackground,#45494e)}
.tab-panel{min-height:0;display:flex;flex-direction:column}
.tabs{display:flex;gap:var(--gap-xs);padding:8px 8px 0;background:var(--panel);border-bottom:1px solid var(--border)}
.tab{min-height:28px;padding:4px 10px;border-bottom:0;border-radius:var(--radius) var(--radius) 0 0}
.tab.active{background:var(--panel-soft);color:var(--fg);border-bottom-color:var(--panel-soft)}
.tab-content{min-height:0;flex:1;display:none;overflow:auto}.tab-content.active{display:block}
ul.log{list-style:none;margin:0;padding:0;overflow:auto;font-family:var(--vscode-editor-font-family,Consolas,monospace);font-size:12px;min-height:0;height:100%}
ul.log li{display:flex;gap:8px;padding:4px 8px;border-bottom:1px solid var(--border)}.time{color:var(--muted);flex:0 0 auto}.msg{white-space:pre-wrap;overflow-wrap:anywhere;min-width:0}.level{font-weight:700;flex:0 0 auto}.level.pass,.pass{color:var(--ok)}.level.fail,.fail{color:var(--err)}.level.warn,.warn{color:var(--warn)}
@media(max-width:900px){html,body{overflow:auto}main{height:auto;min-height:100vh;grid-template-columns:1fr;overflow:visible}.playable-panel{height:min(72vh,640px);min-height:360px}.tab-panel{min-height:360px}}
@media(max-height:700px) and (max-width:900px){.playable-panel{height:calc(100vh - 24px);min-height:300px}}
</style>
</head>
<body>
<main>
  <section class="runtime-side">
    <section class="panel playable-panel">
      <div class="head"><h1>Playable</h1><div class="title-actions"><button id="muteBtn" class="icon-btn" title="Mute" aria-label="Mute" aria-pressed="false">&#128263;</button><button id="reloadBtn" class="icon-btn" title="Reload" aria-label="Reload">&#8635;</button></div></div>
      <div class="body playable-body">
        <div class="preview-stage"><div class="device"><div class="screen"><iframe id="adFrame" src="/ad?preview=true&loading=0&review=1"></iframe></div></div></div>
      </div>
    </section>
  </section>

  <section class="panel tab-panel">
    <div class="head"><h1>Checks</h1><div class="head-meta"><div class="sub" id="bridge">Starting...</div><div class="sub" id="timer">60s</div></div></div>
    <div class="tabs">
      <button class="tab active" data-tab="checksTab">Checklist</button>
      <button class="tab" data-tab="eventsTab">Events</button>
    </div>
    <ol class="tab-content active" id="checksTab"></ol>
    <ul class="log tab-content" id="eventsTab"></ul>
  </section>
</main>
<script>
const BOOT=${boot};
const CHECKS=[
  {id:'html-requirements',num:1,label:'HTML Requirements',runtimeEvent:null},
  {id:'cta-method',num:2,label:'CTA Call Method',runtimeEvent:'install'},
  {id:'game-end',num:3,label:'Game End Call Method',runtimeEvent:'gameEnd'},
  {id:'game-ready',num:4,label:'Game Ready Call Method',runtimeEvent:'gameReady'},
  {id:'game-start',num:5,label:'Game Start Call Method',runtimeEvent:'gameStart'},
  {id:'game-close',num:6,label:'Game Close Call Method',runtimeEvent:'gameClose'},
  {id:'file-handling',num:7,label:'File Handling Method',runtimeEvent:null},
  {id:'file-spec',num:8,label:'File Spec',runtimeEvent:null},
  {id:'storage',num:9,label:'Storage Requirements',runtimeEvent:null},
  {id:'code-exception',num:10,label:'Code Exception',runtimeEvent:'exception'}
];
const EVENT_TO_CHECK={};CHECKS.forEach(c=>{if(c.runtimeEvent)EVENT_TO_CHECK[c.runtimeEvent]=c.id;});
const SDK_TYPE_TO_CHECK={game_ready:'game-ready',game_end:'game-end',game_start:'game-start',game_close:'game-close',install:'cta-method',outer_chain:'file-handling',base64:'file-handling',code:'code-exception'};
const state={checks:{},seconds:0,muted:false,connected:false,timer:null,timeout:60,codeCleanAfter:5,hasSentNoRetry:false};
CHECKS.forEach(c=>state.checks[c.id]={status:'pending',detail:'',runtimeStatus:c.runtimeEvent?'waiting':null,runtimeDetail:''});
BOOT.checks.forEach(r=>{const ch=state.checks[r.id];if(ch){ch.status=r.status;ch.detail=r.detail;}});
state.checks['code-exception'].status='pass';state.checks['code-exception'].runtimeStatus='monitoring';
['cta-method','game-end','game-ready','game-start','game-close'].forEach(id=>{if(state.checks[id].status==='fail')state.checks[id].runtimeStatus=null;});
const checksEl=document.getElementById('checksTab'),eventLog=document.getElementById('eventsTab'),adFrame=document.getElementById('adFrame'),muteBtn=document.getElementById('muteBtn');
document.querySelectorAll('.tab').forEach(btn=>btn.addEventListener('click',()=>{document.querySelectorAll('.tab').forEach(b=>b.classList.remove('active'));document.querySelectorAll('.tab-content').forEach(p=>p.classList.remove('active'));btn.classList.add('active');document.getElementById(btn.dataset.tab).classList.add('active');}));
function resetChecks(){if(state.timer)clearInterval(state.timer);state.seconds=0;state.connected=false;state.hasSentNoRetry=false;CHECKS.forEach(c=>state.checks[c.id]={status:'pending',detail:'',runtimeStatus:c.runtimeEvent?'waiting':null,runtimeDetail:''});BOOT.checks.forEach(r=>{const ch=state.checks[r.id];if(ch){ch.status=r.status;ch.detail=r.detail;}});state.checks['code-exception'].status='pass';state.checks['code-exception'].runtimeStatus='monitoring';['cta-method','game-end','game-ready','game-start','game-close'].forEach(id=>{if(state.checks[id].status==='fail')state.checks[id].runtimeStatus=null;});eventLog.innerHTML='';document.getElementById('bridge').textContent='Starting...';document.getElementById('timer').textContent=state.timeout+'s';render();state.timer=startRuntimeTimer();}
function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
function addLog(list,name,cls,msg){const d=new Date(),li=document.createElement('li');li.innerHTML='<span class="time">'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0')+'</span><span class="level '+(cls||'')+'">'+esc(name)+'</span><span class="msg">'+esc(msg||'')+'</span>';list.appendChild(li);list.scrollTop=list.scrollHeight;}
function render(){checksEl.innerHTML=CHECKS.map(c=>{const ch=state.checks[c.id];let s=ch.status,d=ch.detail;if(ch.runtimeStatus==='waiting'&&ch.status==='pass'){s='waiting';d=ch.detail+' - awaiting runtime confirmation';}else if(ch.runtimeStatus==='monitoring'){s='waiting';d='Monitoring for exceptions...';}else if(ch.runtimeStatus==='pass'){s='pass';d=(ch.detail||'')+(ch.runtimeDetail?' - '+ch.runtimeDetail:'');}else if(ch.runtimeStatus==='fail'){s='fail';d=ch.runtimeDetail||ch.detail;}else if(ch.runtimeStatus==='timeout'&&ch.status==='pass'){s='warn';d=ch.detail+' - not fired within '+state.timeout+'s';}else if(ch.runtimeStatus==='clean'){s='pass';d='No exceptions detected';}return '<li class="check"><span class="num">'+c.num+'</span><span><div class="label">'+esc(c.label)+' <span class="badge '+s+'">'+s.toUpperCase()+'</span></div><div class="detail">'+esc(d||'')+'</div></span></li>';}).join('');}
function sdkCheck(type,value,msg){const id=SDK_TYPE_TO_CHECK[type],ch=state.checks[id];if(!ch)return;const pass=value===1||value===true;if(type==='code'){ch.status='fail';ch.runtimeStatus='fail';ch.runtimeDetail=msg||'JS error detected';addLog(eventLog,'code error','fail',ch.runtimeDetail);}else if(type==='outer_chain'){ch.runtimeStatus=pass?'pass':'fail';ch.runtimeDetail=pass?'No external resources at runtime':'External resources detected at runtime';addLog(eventLog,type,pass?'pass':'fail',ch.runtimeDetail);}else if(type==='base64'){if(!pass){ch.status='fail';ch.runtimeStatus='fail';ch.runtimeDetail='Non-JS/HTML assets must be base64';addLog(eventLog,type,'warn',ch.runtimeDetail);}}else{if(pass&&(ch.runtimeStatus==='waiting'||ch.runtimeStatus===null)){ch.runtimeStatus='pass';ch.runtimeDetail='Confirmed by SDK at '+state.seconds+'s';addLog(eventLog,type,'pass','');}else if(!pass&&(ch.runtimeStatus==='waiting'||ch.runtimeStatus===null)){ch.runtimeStatus='fail';ch.runtimeDetail=type+' not defined';addLog(eventLog,type,'warn',ch.runtimeDetail);}}render();done();}
function inferAll(text){const s=String(text||''),out=[];if(/(^|[^a-z])gameReady([^a-z]|$)|previewer:gameReady|game_ready|\\bDISPLAYED\\b|\\bLOADED\\b/i.test(s))out.push('gameReady');if(/(^|[^a-z])gameStart([^a-z]|$)|previewer:gameStart|game_start|\\bCHALLENGE_STARTED\\b/i.test(s))out.push('gameStart');if(/(^|[^a-z])gameEnd([^a-z]|$)|previewer:gameEnd|game_end|\\bCHALLENGE_SOLVED\\b|\\bENDCARD_SHOWN\\b/i.test(s))out.push('gameEnd');if(/(^|[^a-z])gameClose([^a-z]|$)|previewer:gameClose|game_close/i.test(s))out.push('gameClose');if(/(^|[^a-z])install([^a-z]|$)|previewer:install|\\bCTA_CLICKED\\b/i.test(s))out.push('install');return out;}
function handle(msg){const name=msg.name,extra=msg.extra||{};if(name==='__ping__'){state.connected=true;document.getElementById('bridge').textContent='Monitor connected';addLog(eventLog,'Monitor connected','pass','');return;}if(name==='sdkCheck'){sdkCheck(extra.type,extra.value,extra.msg||'');return;}if(name==='console'){inferAll(extra.text||'').forEach(ev=>handle({name:ev,extra:{source:'console'}}));return;}if(name==='diagnostic'){addLog(eventLog,extra.label||'diagnostic','warn',extra.detail||'');return;}if(name==='audioContext'){addLog(eventLog,'Audio captured','pass',extra.routed?'master gain ready':'context only');return;}if(name==='exception'){const ch=state.checks['code-exception'];ch.status='fail';ch.runtimeStatus='fail';ch.runtimeDetail=extra.message||'Unhandled exception';addLog(eventLog,'exception','fail',ch.runtimeDetail);render();done();return;}const id=EVENT_TO_CHECK[name];if(id){const ch=state.checks[id];if(ch.runtimeStatus==='waiting'||ch.runtimeStatus===null){ch.runtimeStatus='pass';ch.runtimeDetail=(extra.source==='console'?'Seen in console at ':'Fired at ')+state.seconds+'s';addLog(eventLog,name,'pass',extra.source==='console'?'from console':'');render();done();}}}
window.__hplMtgNotify=(name,extra)=>handle({name,extra:extra||{}});
window.addEventListener('message',e=>{const m=e.data;if(!m)return;if(m.type==='lifecycle')handle(m);else if(typeof m.type==='string'&&m.type.startsWith('previewer:')){const map={'previewer:gameReady':'gameReady','previewer:gameStart':'gameStart','previewer:install':'install','previewer:gameEnd':'gameEnd','previewer:gameClose':'gameClose'};if(map[m.type])handle({name:map[m.type],extra:{}});if(m.type==='previewer:review'&&m.data&&Array.isArray(m.data.reviewResult)){if(!state.hasSentNoRetry){state.hasSentNoRetry=true;try{adFrame.contentWindow.postMessage({type:'previewer:review',data:{reviewResult:{no_game_retry:true}}},'*');}catch(e){}}m.data.reviewResult.forEach(r=>sdkCheck(r.type,r.value,r.msg||''));}}});
function applyMute(){try{const cw=adFrame.contentWindow;if(!cw)return;cw.__hplMuted=state.muted;if(typeof cw.__hplApplyMutedState==='function')cw.__hplApplyMutedState();(cw.__hplMasterGains||[]).forEach(g=>{try{if(g&&g.gain)g.gain.value=state.muted?0:1;}catch(e){}});(cw.__hplMediaElements||[]).forEach(el=>{try{el.muted=state.muted;el.volume=state.muted?0:1;}catch(e){}});}catch(e){}}
function renderMuteButton(){muteBtn.innerHTML=state.muted?'&#128264;':'&#128263;';muteBtn.title=state.muted?'Unmute':'Mute';muteBtn.setAttribute('aria-label',state.muted?'Unmute':'Mute');muteBtn.setAttribute('aria-pressed',state.muted?'true':'false');}
muteBtn.onclick=()=>{state.muted=!state.muted;renderMuteButton();applyMute();addLog(eventLog,state.muted?'Muted':'Unmuted','pass','');};
document.getElementById('reloadBtn').onclick=()=>{resetChecks();document.getElementById('bridge').textContent='Reloading...';adFrame.src='/ad?preview=true&loading=0&review=1&t='+Date.now();};
['pointerdown','click','touchstart','keydown'].forEach(evt=>window.addEventListener(evt,()=>setTimeout(applyMute,0),true));
function markCodeClean(){const ch=state.checks['code-exception'];if(ch&&ch.runtimeStatus==='monitoring'){ch.runtimeStatus='clean';ch.runtimeDetail='No exceptions detected';addLog(eventLog,'code clean','pass',ch.runtimeDetail);render();}}
function done(){const othersDone=CHECKS.every(c=>c.id==='code-exception'||!c.runtimeEvent||state.checks[c.id].runtimeStatus!=='waiting');if(othersDone)markCodeClean();if(CHECKS.every(c=>!c.runtimeEvent||!['waiting','monitoring'].includes(state.checks[c.id].runtimeStatus))){clearInterval(state.timer);document.getElementById('timer').textContent='Runtime complete';}}
function startRuntimeTimer(){return setInterval(()=>{state.seconds++;document.getElementById('timer').textContent=Math.max(0,state.timeout-state.seconds)+'s';if(state.seconds===2&&!state.connected){document.getElementById('bridge').textContent='Monitor not connected';addLog(eventLog,'Monitor not connected','fail','Check browser DevTools console for blocked inline script/CSP errors.');}if(state.seconds>=state.codeCleanAfter)markCodeClean();done();if(state.seconds>=state.timeout){clearInterval(state.timer);CHECKS.forEach(c=>{const ch=state.checks[c.id];if(ch.runtimeStatus==='waiting')ch.runtimeStatus='timeout';if(ch.runtimeStatus==='monitoring'){ch.runtimeStatus='clean';ch.runtimeDetail='No exceptions detected';}});render();document.getElementById('timer').textContent='Runtime complete';}},1000);}
state.timer=startRuntimeTimer();
renderMuteButton();
render();
</script>
</body>
</html>`;
}

// ─── Webview HTML ─────────────────────────────────────────────────────────────

function getHtml(): string {
  return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<style>
${getToolWebviewStyles()}

.checker-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--tool-gap-lg);
  align-items: start;
}

.checklist { list-style: none; margin: 0; padding: 0; }

.check-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--tool-border);
}
.check-item:last-child { border-bottom: none; }

.check-num {
  flex: 0 0 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--tool-panel);
  border: 1px solid var(--tool-border);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: 700;
  color: var(--vscode-descriptionForeground);
  margin-top: 1px;
}

.check-body { flex: 1; min-width: 0; }

.check-label {
  font-size: 12px;
  font-weight: 600;
  margin-bottom: 2px;
  display: flex;
  align-items: center;
  gap: 6px;
}

.check-detail {
  font-size: 11px;
  color: var(--vscode-descriptionForeground);
  overflow-wrap: anywhere;
}

.check-detail.err { color: var(--vscode-errorForeground); }
.check-detail.warn { color: var(--vscode-editorWarning-foreground, #d29922); }

.badge-waiting {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 18px;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid var(--tool-border);
  font-size: 11px;
  line-height: 1.2;
  color: var(--vscode-descriptionForeground);
  white-space: nowrap;
}
.badge-waiting::before {
  content: "";
  width: 8px;
  height: 8px;
  flex: 0 0 auto;
  border: 1.5px solid color-mix(in srgb, var(--vscode-descriptionForeground) 40%, transparent);
  border-top-color: var(--vscode-descriptionForeground);
  border-radius: 50%;
  animation: status-spin 900ms linear infinite;
}

.device-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

.device-frame {
  width: 100%;
  max-width: 300px;
  background: #111;
  border-radius: 28px;
  padding: 14px 10px;
  box-shadow:
    0 0 0 1px rgba(255,255,255,0.08),
    0 0 0 3px #1a1a1a,
    0 0 0 4px rgba(255,255,255,0.05),
    0 8px 32px rgba(0,0,0,0.5);
}

.device-notch {
  width: 72px;
  height: 7px;
  background: #222;
  border-radius: 4px;
  margin: 0 auto 10px;
}

.device-screen {
  width: 100%;
  aspect-ratio: 9 / 16;
  background: #000;
  border-radius: 10px;
  overflow: hidden;
  position: relative;
}

.device-screen-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 8px;
  color: rgba(255,255,255,0.2);
  font-size: 11px;
  text-align: center;
  padding: 16px;
}

#sim-iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

.embedded-runtime {
  display: none;
  margin-top: var(--tool-gap-md);
  height: calc(100vh - 170px);
  min-height: 360px;
}

.embedded-runtime iframe {
  width: 100%;
  height: calc(100% - 39px);
  min-height: 0;
  border: 0;
  background: #171717;
  display: block;
}

.runtime-legend {
  font-size: 10px;
  color: var(--vscode-descriptionForeground);
  text-align: center;
  max-width: 280px;
  line-height: 1.5;
}

.event-log {
  width: 100%;
  max-width: 300px;
}
.event-log-title {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--vscode-descriptionForeground);
  margin-bottom: 4px;
}
.event-log-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--tool-border);
  border-radius: var(--tool-radius);
  background: var(--tool-panel-soft);
  max-height: 120px;
  overflow-y: auto;
  font-size: 11px;
}
.event-log-list li {
  padding: 3px 8px;
  border-bottom: 1px solid var(--tool-border);
  display: flex;
  gap: 6px;
}
.event-log-list li:last-child { border-bottom: none; }
.event-log-empty {
  padding: 6px 8px;
  color: var(--vscode-descriptionForeground);
  font-style: italic;
}
.event-time { color: var(--vscode-descriptionForeground); flex: 0 0 auto; }
.event-name { font-weight: 600; }
.event-name.ok   { color: var(--vscode-testing-iconPassed, #3fb950); }
.event-name.err  { color: var(--vscode-errorForeground); }
.event-name.warn { color: var(--vscode-editorWarning-foreground, #d29922); }
.console-log-list {
  max-height: 180px;
  font-family: var(--vscode-editor-font-family, monospace);
}
.console-log-list li {
  align-items: flex-start;
}
.console-line {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  min-width: 0;
}
.console-level {
  flex: 0 0 auto;
  font-weight: 700;
  color: var(--vscode-descriptionForeground);
}
.console-level.warn { color: var(--vscode-editorWarning-foreground, #d29922); }
.console-level.error { color: var(--vscode-errorForeground); }

.summary-bar {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
}
.summary-count {
  display: inline;
  font-size: 12px;
}
</style>
</head>
<body>
<main class="tool-page">

  <header class="tool-header">
    <h2 class="tool-title">Mintegral Checker</h2>
    <p class="tool-description">
      Validates a Mintegral playable ad ZIP against all PlayTurbo requirements.
      Static checks run immediately; runtime checks update live as you interact with the ad preview.
    </p>
  </header>

  <section class="tool-panel input-panel">
    <div class="panel-header">
      <h3 class="panel-title">Input</h3>
    </div>
    <div class="panel-body">
      <div class="control-label">Playable ZIP</div>
      <div class="field-row">
        <button id="pickBtn" class="secondary">Select File</button>
        <button id="reloadBtn" class="secondary" style="display:none">Reload</button>
      </div>
      <div class="file-name" id="fileName" style="margin-top:8px;">(no file selected)</div>
      <div id="statusBar" class="status-panel"></div>
    </div>
  </section>

  <div id="outputPanel" class="is-hidden">
    <div class="summary-bar" id="summaryBar" style="display:none"></div>
    <section class="tool-panel embedded-runtime" id="embeddedRuntimePanel">
      <div class="panel-header">
        <h3 class="panel-title">Runtime Checker</h3>
      </div>
      <iframe id="runtimeFrame"></iframe>
    </section>

    <div class="checker-layout" id="legacyRuntimePanel">

      <!-- Checklist -->
      <section class="tool-panel" style="margin-bottom:0">
        <div class="panel-header">
          <h3 class="panel-title">Checklist</h3>
          <span id="runtimeTimerLabel" class="file-name" style="font-size:10px;"></span>
        </div>
        <div class="panel-body" style="padding:0;">
          <ol class="checklist" id="checklist"></ol>
        </div>
      </section>

      <!-- Simulator -->
      <section class="tool-panel" style="margin-bottom:0">
        <div class="panel-header">
          <h3 class="panel-title">Simulator</h3>
        </div>
        <div class="panel-body" style="display:flex;flex-direction:column;align-items:center;gap:12px;">

          <div class="device-wrap">
            <div class="device-frame">
              <div class="device-notch"></div>
              <div class="device-screen" id="deviceScreen">
                <div class="device-screen-placeholder" id="screenPlaceholder">
                  <div style="font-size:28px;opacity:0.3">▶</div>
                  <div>Load a ZIP to<br>start the simulator</div>
                </div>
                <iframe id="sim-iframe" style="display:none"></iframe>
              </div>
            </div>
            <div style="display:flex;gap:8px;align-items:center;justify-content:center;">
              <button id="muteBtn" class="secondary" style="display:none;font-size:11px;padding:3px 10px;">Mute</button>
            </div>
            <p class="runtime-legend">
              Interact with the ad to trigger runtime checks.<br>
              Tap CTA to verify <code>window.install()</code>, play through to fire lifecycle events.
            </p>
          </div>

          <div class="event-log" id="eventLogWrap" style="display:none">
            <div class="event-log-title">Runtime Events</div>
            <ul class="event-log-list" id="eventLogList">
              <li class="event-log-empty">No events yet</li>
            </ul>
          </div>

        </div>
      </section>

    </div>
  </div>

</main>
<script>
const vscode = acquireVsCodeApi();

// Exposed on window so the iframe's monitoring stub can call it directly
// without relying on postMessage (more reliable inside VS Code webview).
window.__hplMtgNotify = function(name, extra) {
  handleRuntimeEvent({ type: 'lifecycle', name: name, extra: extra || {} });
};

// ─── Check definitions ─────────────────────────────────────────────────────

const CHECKS = [
  { id: 'html-requirements', num: 1,  label: 'HTML Requirements',      runtimeEvent: null },
  { id: 'cta-method',        num: 2,  label: 'CTA Call Method',        runtimeEvent: 'install' },
  { id: 'game-end',          num: 3,  label: 'Game End Call Method',   runtimeEvent: 'gameEnd' },
  { id: 'game-ready',        num: 4,  label: 'Game Ready Call Method', runtimeEvent: 'gameReady' },
  { id: 'game-start',        num: 5,  label: 'Game Start Call Method', runtimeEvent: 'gameStart' },
  { id: 'game-close',        num: 6,  label: 'Game Close Call Method', runtimeEvent: 'gameClose' },
  { id: 'file-handling',     num: 7,  label: 'File Handling Method',   runtimeEvent: null },
  { id: 'file-spec',         num: 8,  label: 'File Spec',              runtimeEvent: null },
  { id: 'storage',           num: 9,  label: 'Storage Requirements',   runtimeEvent: null },
  { id: 'code-exception',    num: 10, label: 'Code Exception',         runtimeEvent: 'exception' },
];

// Runtime event → check id (for simple pass events from fallback mocks)
const EVENT_TO_CHECK = {};
CHECKS.forEach(c => { if (c.runtimeEvent) EVENT_TO_CHECK[c.runtimeEvent] = c.id; });

// SDK emitCheckData type → check id (used by sdkCheck events)
const SDK_TYPE_TO_CHECK = {
  game_ready:  'game-ready',
  game_end:    'game-end',
  game_start:  'game-start',
  game_close:  'game-close',
  install:     'cta-method',
  outer_chain: 'file-handling',
  base64:      'file-handling',
  code:        'code-exception',
};

// ─── State ─────────────────────────────────────────────────────────────────

const state = {
  checks: {},    // id → { status, detail, runtimeStatus, runtimeDetail }
  zipPath: null,
  iframeLoaded: false,
  timerInterval: null,
  pollInterval: null,
  monitorCheckTimeout: null,
  monitorConnected: false,
  timerSeconds: 0,
  RUNTIME_TIMEOUT: 60,
  muted: false,
};

CHECKS.forEach(c => {
  state.checks[c.id] = {
    status: 'pending',
    detail: '',
    runtimeStatus: c.runtimeEvent ? 'waiting' : null,
    runtimeDetail: '',
  };
});

// ─── DOM refs ───────────────────────────────────────────────────────────────

const pickBtn        = document.getElementById('pickBtn');
const reloadBtn      = document.getElementById('reloadBtn');
const muteBtn        = document.getElementById('muteBtn');
const fileNameEl     = document.getElementById('fileName');
const statusBarEl    = document.getElementById('statusBar');
const outputPanel    = document.getElementById('outputPanel');
const checklistEl    = document.getElementById('checklist');
const iframe         = document.getElementById('sim-iframe');
const runtimeFrame   = document.getElementById('runtimeFrame');
const runtimePanel   = document.getElementById('embeddedRuntimePanel');
const legacyPanel    = document.getElementById('legacyRuntimePanel');
const screenHolder   = document.getElementById('screenPlaceholder');
const summaryBar     = document.getElementById('summaryBar');
const eventLogWrap   = document.getElementById('eventLogWrap');
const eventLogList   = document.getElementById('eventLogList');
const timerLabel     = document.getElementById('runtimeTimerLabel');

// ─── Rendering helpers ──────────────────────────────────────────────────────

function esc(s) {
  return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}

function badgeHtml(status, label) {
  const cls = { pass: 'ok', fail: 'err', warn: 'warn', pending: '', waiting: '' }[status] || '';
  if (status === 'waiting') return '<span class="badge-waiting">Waiting</span>';
  return '<span class="badge ' + cls + '">' + esc(label || status.toUpperCase()) + '</span>';
}

function renderCheck(c, ch) {
  // Determine displayed status
  let dispStatus, dispDetail;

  if (ch.status === 'pending') {
    dispStatus = 'pending';
    dispDetail = '';
  } else {
    // Static result
    dispStatus = ch.status;
    dispDetail = ch.detail;

    // Overlay runtime result if available
    if (ch.runtimeStatus === 'waiting') {
      // Static passed but runtime not yet confirmed
      if (ch.status === 'pass') {
        dispStatus = 'waiting';
        dispDetail = ch.detail ? ch.detail + ' — awaiting runtime confirmation' : 'Awaiting runtime...';
      }
    } else if (ch.runtimeStatus === 'pass') {
      // SDK confirmed — trust runtime over static (handles static-fail overridden by SDK)
      dispStatus = 'pass';
      dispDetail = (ch.detail || '') + (ch.runtimeDetail ? ' — ' + ch.runtimeDetail : '');
    } else if (ch.runtimeStatus === 'fail') {
      dispStatus = 'fail';
      dispDetail = ch.runtimeDetail || ch.detail;
    } else if (ch.runtimeStatus === 'timeout') {
      if (ch.status === 'pass') {
        dispStatus = 'warn';
        dispDetail = ch.detail + ' — not fired within ' + state.RUNTIME_TIMEOUT + 's';
      }
    } else if (ch.runtimeStatus === 'monitoring') {
      dispStatus = 'waiting';
      dispDetail = 'Monitoring for exceptions...';
    } else if (ch.runtimeStatus === 'clean') {
      dispStatus = 'pass';
      dispDetail = 'No exceptions detected';
    }
  }

  const detailClass = dispStatus === 'fail' ? ' err' : dispStatus === 'warn' ? ' warn' : '';

  return '<li class="check-item">' +
    '<span class="check-num">' + c.num + '</span>' +
    '<span class="check-body">' +
      '<span class="check-label">' +
        esc(c.label) + '&nbsp;' +
        (dispStatus === 'pending' ? '' : badgeHtml(dispStatus)) +
      '</span>' +
      (dispDetail ? '<span class="check-detail' + detailClass + '">' + esc(dispDetail) + '</span>' : '') +
    '</span>' +
  '</li>';
}

function renderChecklist() {
  checklistEl.innerHTML = CHECKS.map(c => renderCheck(c, state.checks[c.id])).join('');
}

function renderSummary() {
  summaryBar.textContent = '';
  if (!statusBarEl.classList.contains('is-busy') && !statusBarEl.textContent.startsWith('Error:')) {
    statusBarEl.textContent = '';
    statusBarEl.className = 'status-panel';
  }
}

function logEvent(name, cls, extra) {
  const d = new Date();
  const time = String(d.getMinutes()).padStart(2,'0') + ':' + String(d.getSeconds()).padStart(2,'0');
  const empty = eventLogList.querySelector('.event-log-empty');
  if (empty) empty.remove();

  const li = document.createElement('li');
  li.innerHTML =
    '<span class="event-time">' + esc(time) + '</span>' +
    '<span class="event-name ' + cls + '">' + esc(name) + '</span>' +
    (extra ? '<span class="check-detail">' + esc(extra) + '</span>' : '');
  eventLogList.appendChild(li);
  eventLogList.scrollTop = eventLogList.scrollHeight;
}

function inferLifecycleFromConsole(text) {
  const s = String(text || '');
  if (/(^|[^a-z])gameReady([^a-z]|$)|previewer:gameReady|game_ready/i.test(s)) return 'gameReady';
  if (/(^|[^a-z])gameStart([^a-z]|$)|previewer:gameStart|game_start/i.test(s)) return 'gameStart';
  if (/(^|[^a-z])gameEnd([^a-z]|$)|previewer:gameEnd|game_end/i.test(s)) return 'gameEnd';
  if (/(^|[^a-z])gameClose([^a-z]|$)|previewer:gameClose|game_close/i.test(s)) return 'gameClose';
  if (/(^|[^a-z])install([^a-z]|$)|previewer:install/i.test(s)) return 'install';
  return null;
}

// ─── Timer ──────────────────────────────────────────────────────────────────

function startTimer() {
  stopTimer();
  state.timerSeconds = 0;
  state.timerInterval = setInterval(() => {
    state.timerSeconds++;
    const remaining = state.RUNTIME_TIMEOUT - state.timerSeconds;
    if (remaining > 0) {
      timerLabel.textContent = 'Runtime timeout in ' + remaining + 's';
    }
    if (state.timerSeconds >= state.RUNTIME_TIMEOUT) {
      onRuntimeTimeout();
    }
  }, 1000);
}

function stopTimer() {
  if (state.timerInterval) { clearInterval(state.timerInterval); state.timerInterval = null; }
  stopPolling();
  timerLabel.textContent = '';
}

function onRuntimeTimeout() {
  stopTimer();
  timerLabel.textContent = 'Runtime check complete';
  CHECKS.forEach(c => {
    const ch = state.checks[c.id];
    if (ch.runtimeStatus === 'waiting') {
      ch.runtimeStatus = 'timeout';
      ch.runtimeDetail = '';
    } else if (ch.runtimeStatus === 'monitoring') {
      ch.runtimeStatus = 'clean';
      ch.runtimeDetail = 'No exceptions detected';
    }
  });
  renderChecklist();
  renderSummary();
}

function checkAllRuntimeDone() {
  const allDone = CHECKS.every(c => {
    const ch = state.checks[c.id];
    if (!c.runtimeEvent) return true;
    return ch.runtimeStatus !== 'waiting' && ch.runtimeStatus !== 'monitoring';
  });
  if (allDone) {
    stopTimer();
    timerLabel.textContent = 'All runtime checks complete';
  }
}

// ─── Load ZIP ──────────────────────────────────────────────────────────────

function stopPolling() {
  if (state.pollInterval) { clearInterval(state.pollInterval); state.pollInterval = null; }
  if (state.monitorCheckTimeout) { clearTimeout(state.monitorCheckTimeout); state.monitorCheckTimeout = null; }
}

function startPolling() {
  stopPolling();
  state.pollInterval = setInterval(function() {
    try {
      const cw = iframe.contentWindow;
      if (!cw) return;
      const evts = cw.__hplMtgEvents;
      if (!evts || !evts.length) return;
      const batch = evts.splice(0, evts.length);
      batch.forEach(function(e) {
        handleRuntimeEvent({ type: 'lifecycle', name: e.name, extra: e.extra || {} });
      });
    } catch(e) { /* cross-origin or not yet loaded */ }
  }, 200);
}

function resetState() {
  stopTimer();
  stopPolling();
  CHECKS.forEach(c => {
    state.checks[c.id] = {
      status: 'pending',
      detail: '',
      runtimeStatus: c.runtimeEvent ? 'waiting' : null,
      runtimeDetail: '',
    };
  });
  state.iframeLoaded = false;
  state.monitorConnected = false;
  iframe.style.display = 'none';
  iframe.src = 'about:blank';
  runtimeFrame.src = 'about:blank';
  runtimePanel.style.display = 'none';
  legacyPanel.style.display = '';
  screenHolder.style.display = 'flex';
  eventLogList.innerHTML = '<li class="event-log-empty">No events yet</li>';
  eventLogWrap.style.display = 'none';
  summaryBar.innerHTML = '';
  statusBarEl.textContent = '';
  statusBarEl.className = 'status-panel';
  timerLabel.textContent = '';
  muteBtn.style.display = 'none';
  state.muted = false;
}

function applyStaticResults(checks) {
  checks.forEach(r => {
    const ch = state.checks[r.id];
    if (!ch) return;
    ch.status = r.status;
    ch.detail = r.detail;
    // code-exception is runtime-only; others with a runtimeEvent also need runtime
    if (!ch.runtimeStatus) {
      // static-only checks: done
    }
  });

  // code-exception starts as monitoring
  const excCheck = state.checks['code-exception'];
  excCheck.status = 'pass'; // static: nothing to check
  excCheck.runtimeStatus = 'monitoring';

  // If static check failed for a lifecycle check, override runtimeStatus to show fail
  ['cta-method','game-end','game-ready','game-start','game-close'].forEach(id => {
    const ch = state.checks[id];
    if (ch.status === 'fail') {
      ch.runtimeStatus = null; // don't show waiting — already failed
    }
  });
}

// ─── Extension messages ─────────────────────────────────────────────────────

window.addEventListener('message', (event) => {
  const msg = event.data;
  if (!msg) return;

  // ── iframe→parent: our stub's lifecycle/sdkCheck events (via parent.postMessage) ──
  if (msg.type === 'lifecycle') {
    handleRuntimeEvent(msg);
    return;
  }

  // ── iframe→parent: real SDK native messages (in case parent.postMessage works) ──
  if (typeof msg.type === 'string' && msg.type.startsWith('previewer:')) {
    const sdkLifecycleMap = {
      'previewer:gameReady': 'gameReady',
      'previewer:gameStart': 'gameStart',
      'previewer:install':   'install',
      'previewer:gameEnd':   'gameEnd',
      'previewer:gameClose': 'gameClose',
      'previewer:gameRetry': 'gameRetry',
    };
    if (sdkLifecycleMap[msg.type]) {
      handleRuntimeEvent({ type: 'lifecycle', name: sdkLifecycleMap[msg.type], extra: {} });
      return;
    }
    // SDK emitCheckData result: {type:"previewer:review", data:{reviewResult:[...]}}
    if (msg.type === 'previewer:review' && msg.data && Array.isArray(msg.data.reviewResult)) {
      msg.data.reviewResult.forEach(r => handleSdkCheck(r.type, r.value, r.msg || ''));
      return;
    }
    return;
  }

  // Extension messages
  if (msg.type === 'status') {
    statusBarEl.textContent = msg.message;
    statusBarEl.className = 'status-panel is-busy';
  } else if (msg.type === 'error') {
    statusBarEl.textContent = 'Error: ' + msg.message;
    statusBarEl.className = 'status-panel';
    outputPanel.classList.add('is-hidden');
  } else if (msg.type === 'loaded') {
    onLoaded(msg);
  }
});

function onLoaded(msg) {
  resetState();
  state.zipPath = msg.zipPath;

  fileNameEl.textContent  = msg.fileName + ' (' + (msg.zipSize / 1024).toFixed(0) + ' KB)';
  reloadBtn.style.display = '';
  if (msg.html) { muteBtn.style.display = ''; muteBtn.textContent = 'Mute'; state.muted = false; }
  statusBarEl.textContent = '';
  statusBarEl.className   = 'status-panel';
  outputPanel.classList.remove('is-hidden');
  eventLogWrap.style.display = '';

  applyStaticResults(msg.checks || []);
  renderChecklist();
  renderSummary();

  if (msg.runtimeUrl) {
    legacyPanel.style.display = 'none';
    runtimePanel.style.display = 'block';
    runtimeFrame.src = msg.runtimeUrl;
    stopTimer();
    return;
  }

  if (msg.html) {
    screenHolder.style.display = 'none';
    iframe.style.display       = 'block';
    iframe.srcdoc              = msg.html;
    startPolling();
    state.monitorCheckTimeout = setTimeout(function() {
      if (!state.monitorConnected) {
        logEvent('Monitor blocked', 'err', 'Iframe script bridge did not connect; runtime callbacks, playable console, and mute cannot be verified in this webview.');
      }
    }, 2000);
    startTimer();
  } else {
    // no HTML (index.html missing), static checks show the error
    stopTimer();
    CHECKS.forEach(c => {
      const ch = state.checks[c.id];
      if (ch.runtimeStatus === 'waiting' || ch.runtimeStatus === 'monitoring') {
        ch.runtimeStatus = null;
        ch.runtimeDetail = '';
      }
    });
    renderChecklist();
    renderSummary();
  }
}

// ─── Runtime event handling ─────────────────────────────────────────────────

function handleSdkCheck(sdkType, value, msg) {
  const checkId = SDK_TYPE_TO_CHECK[sdkType];
  if (!checkId) return;
  const ch = state.checks[checkId];
  if (!ch) return;

  const passed = value === 1 || value === true;

  if (sdkType === 'code') {
    // JS error detected by SDK
    const detail = msg || 'JS error detected by SDK';
    ch.status        = 'fail';
    ch.runtimeStatus = 'fail';
    ch.runtimeDetail = detail;
    logEvent('code error', 'err', detail.slice(0, 80));
  } else if (sdkType === 'outer_chain') {
    if (!passed) {
      ch.status        = 'fail';
      ch.runtimeStatus = 'fail';
      ch.runtimeDetail = 'External resources detected at runtime';
      logEvent('outer_chain:fail', 'err', null);
    } else if (ch.runtimeStatus !== 'fail') {
      ch.runtimeStatus = 'pass';
      ch.runtimeDetail = 'No external resources at runtime';
      logEvent('outer_chain:pass', 'ok', null);
    }
  } else if (sdkType === 'base64') {
    if (!passed && ch.status !== 'fail') {
      ch.status        = 'fail';
      ch.runtimeStatus = 'fail';
      ch.runtimeDetail = (ch.runtimeDetail ? ch.runtimeDetail + '; ' : '') + 'Non-JS/HTML assets must be base64';
      logEvent('base64:fail', 'warn', null);
    }
  } else {
    // game_ready, game_end, game_start, game_close, install
    if (passed) {
      if (ch.runtimeStatus === 'waiting' || ch.runtimeStatus === null) {
        ch.runtimeStatus = 'pass';
        ch.runtimeDetail = 'Confirmed by SDK at ' + state.timerSeconds + 's';
        logEvent(sdkType + ':pass', 'ok', null);
      }
    } else {
      // value:0 means the function is not defined (game_start / game_close)
      if (ch.runtimeStatus === 'waiting' || ch.runtimeStatus === null) {
        ch.runtimeStatus = 'fail';
        ch.runtimeDetail = (sdkType === 'game_start' ? 'window.gameStart' : sdkType === 'game_close' ? 'window.gameClose' : sdkType) + ' not defined';
        logEvent(sdkType + ':missing', 'warn', null);
      }
    }
  }

  renderChecklist();
  renderSummary();
  checkAllRuntimeDone();
}

function handleRuntimeEvent(msg) {
  const name  = msg.name;
  const extra = msg.extra || {};

  if (name === '__ping__') {
    state.monitorConnected = true;
    logEvent('Simulator connected', 'ok', null);
    return;
  }

  // SDK emitCheckData result forwarded by stub
  if (name === 'sdkCheck') {
    handleSdkCheck(extra.type, extra.value, extra.msg || '');
    return;
  }

  if (name === 'exception') {
    const ch = state.checks['code-exception'];
    const detail = extra.message
      ? extra.message + (extra.line ? ' (line ' + extra.line + ')' : '')
      : 'Unhandled exception';
    ch.status        = 'fail';
    ch.runtimeStatus = 'fail';
    ch.runtimeDetail = detail;
    logEvent('exception', 'err', detail.slice(0, 80));
    renderChecklist();
    renderSummary();
    checkAllRuntimeDone();
    return;
  }

  if (name === 'resourceError') {
    const ch = state.checks['file-handling'];
    if (ch.status !== 'fail') {
      ch.status = 'fail';
      ch.detail = 'External resource load attempt: ' + (extra.url || '(unknown)');
      renderChecklist();
      renderSummary();
    }
    logEvent('resourceError', 'warn', (extra.url || '').slice(0, 60));
    return;
  }

  if (name === 'diagnostic') {
    logEvent(extra.label || 'diagnostic', 'warn', extra.detail || null);
    return;
  }

  if (name === 'console') {
    const text = String(extra.text || '');
    const inferred = inferLifecycleFromConsole(text);
    if (inferred) {
      handleRuntimeEvent({ type: 'lifecycle', name: inferred, extra: { source: 'console' } });
    }
    return;
  }

  if (name === 'audioContext') {
    logEvent('AudioContext captured', 'ok', extra.routed ? 'master gain ready' : 'context only');
    return;
  }

  // Lifecycle event names (gameReady, gameEnd, install, gameStart, gameClose) from fallback mocks
  // or from window.postMessage intercept — mark corresponding check as passed
  const checkId = EVENT_TO_CHECK[name];
  if (checkId) {
    const ch = state.checks[checkId];
    if (ch.runtimeStatus === 'waiting' || ch.runtimeStatus === null) {
      ch.runtimeStatus = 'pass';
      ch.runtimeDetail = (extra.source === 'console' ? 'Seen in console at ' : 'Fired at ') + state.timerSeconds + 's';
      logEvent(name, 'ok', extra.source === 'console' ? 'from console' : null);
      renderChecklist();
      renderSummary();
      checkAllRuntimeDone();
    }
  }
}

// ─── Button events ──────────────────────────────────────────────────────────

pickBtn.addEventListener('click',   () => vscode.postMessage({ type: 'pickZip' }));
reloadBtn.addEventListener('click', () => {
  if (state.zipPath) vscode.postMessage({ type: 'reload', path: state.zipPath });
});

muteBtn.addEventListener('click', () => {
  state.muted = !state.muted;
  muteBtn.textContent = state.muted ? 'Unmute' : 'Mute';
  try {
    const cw = iframe.contentWindow;
    if (!cw) return;
    cw.__hplMuted = state.muted;

    // Preferred path: AudioContexts captured by the monitoring stub before the ad runs.
    const gains = Array.isArray(cw.__hplMasterGains) ? cw.__hplMasterGains : (cw.__hplMasterGain ? [cw.__hplMasterGain] : []);
    gains.forEach(g => {
      try {
        if (g && g.gain) g.gain.value = state.muted ? 0 : 1;
      } catch(e) {}
    });

    const contexts = Array.isArray(cw.__hplAudioContexts) ? cw.__hplAudioContexts : (cw.__hplAudioCtx ? [cw.__hplAudioCtx] : []);
    if (!gains.length) {
      contexts.forEach(ctx => {
        try {
          if (state.muted && ctx.state === 'running') ctx.suspend();
          if (!state.muted && ctx.state === 'suspended') ctx.resume();
        } catch(e) {}
      });
    }

    if (typeof cw.__hplApplyMutedState === 'function') {
      cw.__hplApplyMutedState();
    }

    // Also support common Phaser/global game shapes when they are exposed.
    if (cw.Phaser && cw.Phaser.game && cw.Phaser.game.sound) {
      cw.Phaser.game.sound.mute = state.muted;
    }
    if (cw.game && cw.game.sound) {
      cw.game.sound.mute = state.muted;
    }

    logEvent(state.muted ? 'Muted' : 'Unmuted', 'ok', gains.length ? 'master gain' : contexts.length ? 'audio context' : 'no audio context yet');
  } catch(e) {}
});
</script>
</body>
</html>`;
}
