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

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

interface ConvertOptions {
  sourcePath: string;
  outputDir: string;
  baseName?: string;
  networks: string[];
}

interface NetworkResult {
  network: string;
  file: string;
  ok: boolean;
  error?: string;
}

export function openPlayworksConverter(_context: vscode.ExtensionContext) {
  const { panel, isNew } = singletonPanel(store, 'hplToolbox.playworksConverter', 'Playworks Converter');
  if (!isNew) return;

  panel.webview.html = getHtml();

  panel.webview.onDidReceiveMessage(async (msg) => {
    switch (msg.type) {
      case 'pickSource': {
        const picked = await vscode.window.showOpenDialog({
          canSelectFiles: true,
          canSelectFolders: false,
          canSelectMany: false,
          filters: { HTML: ['html', 'htm'] },
          openLabel: 'Select Playworks HTML',
        });
        if (!picked?.[0]) return;
        const srcPath = picked[0].fsPath;
        const dir = path.dirname(srcPath);
        panel.webview.postMessage({ type: 'sourcePicked', path: srcPath, dir, baseName: sourceBaseName(srcPath) });
        break;
      }
      case 'pickOutput': {
        const picked = await vscode.window.showOpenDialog({
          canSelectFiles: false,
          canSelectFolders: true,
          canSelectMany: false,
          openLabel: 'Select Output Folder',
        });
        if (!picked?.[0]) return;
        panel.webview.postMessage({ type: 'outputPicked', path: picked[0].fsPath });
        break;
      }
      case 'convert': {
        const opts: ConvertOptions = msg.opts;
        if (!fs.existsSync(opts.sourcePath)) {
          panel.webview.postMessage({ type: 'convertDone', results: [], error: 'Source file not found.' });
          return;
        }
        if (!fs.existsSync(opts.outputDir)) {
          try { fs.mkdirSync(opts.outputDir, { recursive: true }); } catch {
            panel.webview.postMessage({ type: 'convertDone', results: [], error: 'Could not create output folder.' });
            return;
          }
        }

        let html: string;
        try {
          panel.webview.postMessage({ type: 'status', message: 'Reading source HTML...' });
          html = fs.readFileSync(opts.sourcePath, 'utf8');
        } catch (e: any) {
          panel.webview.postMessage({ type: 'convertDone', results: [], error: `Could not read source: ${e.message}` });
          return;
        }

        const results: NetworkResult[] = [];
        try {
          panel.webview.postMessage({ type: 'status', message: 'Converting Unity...' });
          results.push({ network: 'un', file: copyUnityInput(opts), ok: true });
        } catch (e: any) {
          results.push({ network: 'un', file: '', ok: false, error: e.message });
        }
        for (let i = 0; i < opts.networks.length; i++) {
          const network = opts.networks[i];
          panel.webview.postMessage({ type: 'status', message: `Converting ${i + 1}/${opts.networks.length} ${network}` });
          try {
            const filePath = await convertNetwork(html, opts, network);
            results.push({ network, file: filePath, ok: true });
          } catch (e: any) {
            results.push({ network, file: '', ok: false, error: e.message });
          }
        }
        panel.webview.postMessage({ type: 'convertDone', results });
        break;
      }
      case 'openOutput': {
        vscode.env.openExternal(vscode.Uri.file(msg.dir));
        break;
      }
    }
  });
}

const ZIPPED_NETWORKS = new Set(['gg', 'ap', 'vu', 'mtg']);
const SUPPORTED_NETWORKS = new Set(['al', 'is', 'fb', 'gg', 'ap', 'mo', 'vu', 'mtg', 'tt']);
const MRAID_NETWORKS = new Set(['al', 'is']);
const EXITAPI_NETWORKS = new Set(['gg', 'ap']);
const NETWORK_OUTPUT_FOLDERS: Record<string, string> = {
  al: 'Applovin',
  is: 'Ironsource',
  fb: 'Facebook',
  gg: 'GoogleAds',
  ap: 'Appreciate',
  mo: 'Moloco',
  vu: 'Vungle',
  mtg: 'Mintegral',
  tt: 'TikTok',
};

function sourceBaseName(sourcePath: string): string {
  return path.basename(sourcePath, path.extname(sourcePath))
    .replace(/_(?:un|al|is|fb|gg|ap|mo|vu|mtg|tt)$/, '');
}

function outputBaseName(opts: ConvertOptions): string {
  const rawBaseName = opts.baseName?.trim() || sourceBaseName(opts.sourcePath);
  const withoutExtension = rawBaseName.replace(/\.html?$/i, '');
  const safeBaseName = path.basename(withoutExtension).replace(/[<>:"/\\|?*]/g, '_').trim();
  return safeBaseName || sourceBaseName(opts.sourcePath);
}

async function convertNetwork(html: string, opts: ConvertOptions, network: string): Promise<string> {
  if (!SUPPORTED_NETWORKS.has(network)) {
    throw new Error(`Unsupported Playworks target network: ${network}`);
  }
  const transformed = transformHtml(html, network);
  const needsZip = ZIPPED_NETWORKS.has(network);
  const baseName = outputBaseName(opts);
  const htmlFileName = `${baseName}_${network}.html`;
  const outputDir = path.join(opts.outputDir, NETWORK_OUTPUT_FOLDERS[network] ?? network);
  fs.mkdirSync(outputDir, { recursive: true });

  if (needsZip) {
    const tmpPath = path.join(outputDir, `_tmp_${Date.now()}_${network}.html`);
    const zipPath = path.join(outputDir, `${baseName}_${network}.zip`);
    fs.writeFileSync(tmpPath, transformed, 'utf8');
    try {
      await createZip(tmpPath, 'index.html', zipPath);
    } finally {
      try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
    }
    return zipPath;
  }

  const outPath = path.join(outputDir, htmlFileName);
  fs.writeFileSync(outPath, transformed, 'utf8');
  return outPath;
}

function copyUnityInput(opts: ConvertOptions): string {
  const baseName = outputBaseName(opts);
  const outputDir = path.join(opts.outputDir, 'Unity');
  const outPath = path.join(outputDir, `${baseName}_un.html`);
  fs.mkdirSync(outputDir, { recursive: true });
  if (path.resolve(opts.sourcePath) === path.resolve(outPath)) {
    return outPath;
  }
  fs.copyFileSync(opts.sourcePath, outPath);
  return outPath;
}

function transformHtml(html: string, network: string): string {
  let result = sanitizePlayworksHtml(html);

  // Inject into </head>: lifecycle stubs only (must register luna:build listener
  // BEFORE the Playworks bundle so our handler fires first), then any network SDK script.
  // Console restore stays at end-of-body — it must run AFTER the bundle overrides console.
  result = removeNetworkSdkScripts(result);
  let headInject = buildLifecycleScript();
  if (MRAID_NETWORKS.has(network)) {
    headInject += '<script src="mraid.js"></script>' + buildMraidComplianceScript();
  } else if (EXITAPI_NETWORKS.has(network)) {
    headInject += '<script src="exitapi.js"></script>';
  }
  result = injectBeforeHeadClose(result, headInject);

  // Body-level flags
  if (network === 'vu') {
    result = injectAfterBodyOpen(result, '<script>window.__VUNGLE__=true;</script>');
  } else if (network === 'mtg') {
    result = addBodyOnload(result, 'gameReady()');
  } else if (network === 'tt') {
    result = injectAfterBodyOpen(result, '<script>window.__TIKTOK__=true;</script>');
  }

  // Replace the CTA script
  result = replaceCTAScript(result, network);

  return finalizeNetworkHtml(result, network);
}

function sanitizePlayworksHtml(html: string): string {
  let result = cleanupPlayworksHtml(html);
  return removeNetworkSdkScripts(result);
}

function cleanupPlayworksHtml(html: string): string {
  let result = html;
  result = trimAfterFirstHtmlClose(result);
  result = result.replace(/<script\b[^>]*>[\s\S]*?insertYourRemoteDebuggingTokenHere[\s\S]*?<\/script>\s*/gi, '');
  result = result.replace(/https:\/\/mrdoob\.github\.io\/stats\.js\/build\/stats\.min\.js/gi, 'data:text/javascript,');
  result = result.replace(/\s+crossorigin(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?/gi, '');
  result = result.replace(/\s+type\s*=\s*["']module["']/gi, '');
  result = result.replace(/\bconsole\.error\s*\(/g, 'console.warn(');
  return result;
}

function trimAfterFirstHtmlClose(html: string): string {
  const idx = html.search(/<\/html\s*>/i);
  if (idx === -1) return html;
  const close = html.match(/<\/html\s*>/i);
  return close ? html.slice(0, idx + close[0].length) : html;
}

function finalizeNetworkHtml(html: string, network: string): string {
  let result = cleanupPlayworksHtml(html);
  result = dedupeScriptSrc(result, 'mraid.js', MRAID_NETWORKS.has(network));
  result = dedupeScriptSrc(result, 'exitapi.js', EXITAPI_NETWORKS.has(network));
  return result;
}

function removeNetworkSdkScripts(html: string): string {
  let result = html;
  for (const src of ['mraid.js', 'exitapi.js']) {
    const escaped = src.replace('.', '\\.');
    result = result.replace(new RegExp(`<script\\b[^>]*\\bsrc\\s*=\\s*["']${escaped}["'][^>]*>\\s*<\\/script>\\s*`, 'gi'), '');
  }
  return result;
}

function dedupeScriptSrc(html: string, src: string, shouldExist: boolean): string {
  const escaped = src.replace('.', '\\.');
  let seen = false;
  let result = html.replace(new RegExp(`<script\\b[^>]*\\bsrc\\s*=\\s*["']${escaped}["'][^>]*>\\s*<\\/script>\\s*`, 'gi'), (match) => {
    if (shouldExist && !seen) {
      seen = true;
      return `<script src="${src}"></script>`;
    }
    return '';
  });
  if (shouldExist && !seen) {
    result = injectBeforeHeadClose(result, `<script src="${src}"></script>`);
  }
  return result;
}

function injectBeforeHeadClose(html: string, injection: string): string {
  if (/<\/head>/i.test(html)) {
    return html.replace(/<\/head>/i, `${injection}\n</head>`);
  }
  return `${injection}\n${html}`;
}

function injectAfterBodyOpen(html: string, injection: string): string {
  if (/<body\b[^>]*>/i.test(html)) {
    return html.replace(/<body\b[^>]*>/i, match => `${match}\n${injection}`);
  }
  return `${injection}\n${html}`;
}

function addBodyOnload(html: string, handler: string): string {
  if (!/<body\b[^>]*>/i.test(html)) {
    return `<body onload="${handler}">\n${html}`;
  }
  return html.replace(/<body\b([^>]*)>/i, (match, attrs: string) => {
    const onloadMatch = attrs.match(/\bonload\s*=\s*(["'])(.*?)\1/i);
    if (!onloadMatch) return `<body${attrs} onload="${handler}">`;
    if (onloadMatch[2].includes(handler)) return match;
    const updated = `${onloadMatch[2]};${handler}`;
    return match.replace(onloadMatch[0], `onload=${onloadMatch[1]}${updated}${onloadMatch[1]}`);
  });
}

function replaceCTAScript(html: string, network: string): string {
  const marker = 'Luna.Unity.Playable.InstallFullGame=function';
  const markerIdx = html.lastIndexOf(marker);
  if (markerIdx === -1) return html;

  const scriptOpenIdx = html.lastIndexOf('<script>', markerIdx);
  if (scriptOpenIdx === -1) return html;

  const scriptCloseIdx = html.indexOf('</script>', markerIdx);
  if (scriptCloseIdx === -1) return html;

  const before = html.slice(0, scriptOpenIdx);
  const after = html.slice(scriptCloseIdx + '</script>'.length);

  // Console restore runs after the bundle (which overrides console), before CTA
  return before + buildConsoleRestoreScript() + buildCTAScript(network) + after;
}

// Restores native console methods overridden by the Playworks bundle,
// using a temporary iframe to get a fresh un-patched console reference.
function buildConsoleRestoreScript(): string {
  return [
    '<script>(function(){',
    'try{',
    'var f=document.createElement("iframe");',
    'f.style.display="none";',
    'document.documentElement.appendChild(f);',
    'var nc=f.contentWindow.console;',
    'document.documentElement.removeChild(f);',
    'var methods=["log","warn","error","info","debug","dir","table","group","groupEnd","groupCollapsed","time","timeEnd","assert","count","countReset","trace"];',
    'methods.forEach(function(m){try{if(typeof nc[m]==="function")window.console[m]=nc[m].bind(nc);}catch(e){}});',
    '}catch(e){}',
    '})();</script>',
  ].join('');
}

// Exposes gameReady/Start/End/Close stubs on window and wires them to Luna events.
// Registered in <head> so our luna:build listener fires BEFORE the Playworks bundle
// listeners (which synchronously dispatch luna:start inside their luna:build handler).
// One-time guards prevent double-calls:
//   - gameReady: luna:build fires it once; mtg's onload="gameReady()" also fires it
//   - gameStart: luna:start fires twice on non-MRAID networks (two Playworks listeners)
function buildLifecycleScript(): string {
  return [
    '<script>(function(){',
    'if(typeof window.gameReady!=="function")window.gameReady=function(){};',
    'if(typeof window.gameStart!=="function")window.gameStart=function(){};',
    'if(typeof window.gameEnd!=="function")window.gameEnd=function(){};',
    'if(typeof window.gameClose!=="function")window.gameClose=function(){};',
    'var _grOnce=false,_gsOnce=false;',
    'window.addEventListener("luna:build",function(){if(_grOnce)return;_grOnce=true;try{window.gameReady();}catch(e){}});',
    'window.addEventListener("luna:start",function(){if(_gsOnce)return;_gsOnce=true;try{window.gameStart();}catch(e){}});',
    'window.addEventListener("luna:ended",function(){try{window.gameEnd();}catch(e){}});',
    '})();</script>',
  ].join('');
}

function buildMraidComplianceScript(): string {
  return [
    '<script>(function(){',
    'var m=null,scene=null,viewable=true,exposed=true,volume=null;',
    'function get(){return window.mraid||m;}',
    'function emit(name,detail){try{window.dispatchEvent(new CustomEvent(name,{detail:detail}));}catch(e){}}',
    'function apply(){emit("hpl:mraid:visibility",{viewable:viewable,exposed:exposed,hidden:!viewable||!exposed});if(scene&&scene.sound&&typeof scene.sound.setMute==="function")scene.sound.setMute(!viewable||!exposed);}',
    'function onViewable(v){viewable=!!v;apply();}',
    'function onExposure(p){if(typeof p==="number")exposed=p>0;apply();}',
    'function onVolume(v){if(typeof v==="number"){volume=v/100;emit("hpl:mraid:volume",volume);if(scene&&scene.sound&&typeof scene.sound.setVolume==="function")scene.sound.setVolume(volume);}}',
    'function add(name,fn){var x=get();try{if(x&&typeof x.addEventListener==="function")x.addEventListener(name,fn);}catch(e){}}',
    'function unload(){try{if(typeof mraid!=="undefined"&&typeof mraid.unload==="function")mraid.unload();}catch(e){}}',
    'window.__hplMraidUnload=unload;',
    'function setup(){var x=get();if(!x||setup.done)return;setup.done=true;m=x;try{if(typeof mraid!=="undefined"&&typeof mraid.isViewable==="function")viewable=!!mraid.isViewable();else if(typeof x.isViewable==="function")viewable=!!x.isViewable();}catch(e){}try{if(typeof mraid!=="undefined"&&typeof mraid.addEventListener==="function"){mraid.addEventListener("error",function(message,action){console.warn("[MRAID error]",{message:message,action:action});});mraid.addEventListener("stateChange",function(state){console.log("[MRAID stateChange]",state);});mraid.addEventListener("exposureChange",onExposure);mraid.addEventListener("viewableChange",onViewable);mraid.addEventListener("audioVolumeChange",onVolume);apply();return;}}catch(e){}add("error",function(message,action){console.warn("[MRAID error]",{message:message,action:action});});add("stateChange",function(state){console.log("[MRAID stateChange]",state);});add("exposureChange",onExposure);add("viewableChange",onViewable);add("audioVolumeChange",onVolume);apply();}',
    'function ready(){var x=get();if(!x)return false;try{if(typeof mraid!=="undefined"&&typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",setup);return true;}if(typeof x.getState==="function"&&x.getState()==="loading"){add("ready",setup);return true;}}catch(e){}setup();return true;}',
    'window.__hplMraidBindScene=function(s){scene=s;apply();if(typeof volume==="number"&&scene&&scene.sound&&typeof scene.sound.setVolume==="function")scene.sound.setVolume(volume);};',
    'if(!ready()){var end=Date.now()+500;(function poll(){if(ready()||Date.now()>end)return;setTimeout(poll,50);})();}',
    'setTimeout(setup,2000);',
    '})();</script>',
  ].join('');
}

function buildCTAScript(network: string): string {
  const urlSetup = `n=n||window.$environment.packageConfig.iosLink,i=i||window.$environment.packageConfig.androidLink;var o=/iphone|ipad|ipod|macintosh/i.test(window.navigator.userAgent.toLowerCase())?n:i;`;
  // gameClose must fire before every redirect
  const closeCall = `try{window.gameClose();}catch(e){}`;

  let ctaLogic: string;
  switch (network) {
    case 'al':
    case 'is':
      // MRAID — mraid.js injected in <head>
      ctaLogic = `${closeCall}if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){var s=typeof mraid.getState==="function"?mraid.getState():"default";if(s!=="loading"){mraid.open(o);return;}}window.open(o,"_blank");`;
      break;
    case 'fb':
      ctaLogic = `${closeCall}if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");`;
      break;
    case 'gg':
    case 'ap':
      // Google — exitapi.js injected in <head>
      ctaLogic = `${closeCall}if(typeof ExitApi!=="undefined"&&typeof ExitApi.exit==="function"){ExitApi.exit();return;}window.open(o,"_blank");`;
      break;
    case 'mo':
      ctaLogic = `${closeCall}if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`;
      break;
    case 'vu':
      // Vungle — window.__VUNGLE__ flag set in body
      ctaLogic = `${closeCall}try{parent.postMessage("download","*");}catch(e){}`;
      break;
    case 'mtg':
      // Mintegral — onload="gameReady()" set on body
      ctaLogic = `${closeCall}if(typeof window.install==="function"){window.install();return;}window.open(o,"_blank");`;
      break;
    case 'tt':
      // TikTok — window.__TIKTOK__ flag set in body
      ctaLogic = `${closeCall}if(typeof window.openAppStore==="function"){window.openAppStore();return;}window.open(o,"_blank");`;
      break;
    default:
      ctaLogic = `${closeCall}window.open(o,"_blank");`;
  }

  return `<script>window.addEventListener("luna:build",(function(){Bridge.ready((function(){Luna.Unity.Playable.InstallFullGame=function(n,i){window.pi.logCta(),${urlSetup}${ctaLogic}}}))}));</script>`;
}

function createZip(srcFilePath: string, nameInZip: string, destZipPath: string): Promise<void> {
  return new Promise((resolve, reject) => {
    // Remove existing zip if present (Compress-Archive would fail otherwise)
    if (fs.existsSync(destZipPath)) {
      try { fs.unlinkSync(destZipPath); } catch { /* ignore */ }
    }

    const psScript = [
      'Add-Type -AssemblyName System.IO.Compression.FileSystem;',
      `$zip=[System.IO.Compression.ZipFile]::Open('${destZipPath}','Create');`,
      `[void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip,'${srcFilePath}','${nameInZip}');`,
      '$zip.Dispose()',
    ].join(' ');

    const proc = cp.spawn('powershell.exe', ['-NonInteractive', '-Command', psScript], { stdio: ['ignore', 'pipe', 'pipe'] });
    let stderr = '';
    proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString(); });
    proc.on('close', (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`Zip failed (code ${code}): ${stderr.trim()}`));
      }
    });
    proc.on('error', reject);
  });
}

// ─── Webview UI ────────────────────────────────────────────────────────────────

function getHtml(): string {
  const networks = [
    { tag: 'al',  label: 'Applovin',    note: 'HTML + mraid.js in head' },
    { tag: 'is',  label: 'Ironsource',  note: 'HTML + mraid.js in head' },
    { tag: 'fb',  label: 'Facebook',    note: 'HTML' },
    { tag: 'gg',  label: 'Google Ads',  note: 'ZIP + exitapi.js in head' },
    { tag: 'ap',  label: 'Appreciate',   note: 'ZIP + exitapi.js in head' },
    { tag: 'mo',  label: 'Moloco',      note: 'HTML + FbPlayableAd CTA' },
    { tag: 'vu',  label: 'Vungle',      note: 'ZIP + __VUNGLE__ flag' },
    { tag: 'mtg', label: 'Mintegral',   note: 'ZIP + onload="gameReady()"' },
    { tag: 'tt',  label: 'TikTok',      note: 'HTML + __TIKTOK__ flag' },
  ];

  const checkboxRows = networks.map(n => `
    <tr>
      <td style="width:70px;"><input type="checkbox" class="net-cb" data-tag="${n.tag}"${n.tag === 'tt' ? '' : ' checked'} /></td>
      <td style="width:160px;"><span class="net-label">${n.label}</span> <span class="net-tag">${n.tag}</span></td>
      <td><span class="net-note">${n.note}</span></td>
    </tr>`).join('');

  return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<style>
${getToolWebviewStyles()}
  .net-label { min-width: 100px; font-weight: 500; }
  .net-tag { font-size: 10px; opacity: 0.6; font-weight: 400; }
  .net-note { font-size: 11px; opacity: 0.6; }
  .toggle-row { display: flex; gap: 8px; margin-bottom: 6px; }
</style>
</head>
<body>
<main class="tool-page">
<header class="tool-header">
  <h2 class="tool-title">Playworks Converter</h2>
  <p class="tool-description">Convert a Playworks HTML export into network-specific playable outputs.</p>
</header>

<section class="tool-panel input-panel">
  <div class="panel-header">
    <h3 class="panel-title">Inputs</h3>
  </div>
  <div class="panel-body">
    <div class="control-group">
      <div class="control-label">Source HTML (Playworks export)</div>
      <div class="field-row">
        <input id="src" type="hidden" />
        <button id="pickSrc" class="secondary">Select File</button>
        <span id="srcName" class="file-name">(no file)</span>
      </div>
    </div>
    <div class="control-group">
      <div class="control-label">Base Filename</div>
      <div class="field-row">
        <input id="baseName" type="text" placeholder="Base filename..." />
      </div>
    </div>
    <div class="control-group">
      <div class="control-label">Output Folder</div>
      <div class="field-row">
        <input id="outDir" type="text" placeholder="Output folder..." />
        <button id="pickOut" class="secondary">Browse...</button>
      </div>
    </div>
    <div class="control-group">
      <div class="control-label">Networks to Generate</div>
      <div class="toggle-row">
        <button id="selectAll" class="secondary">Select All</button>
        <button id="selectNone" class="secondary">Select None</button>
      </div>
      <div class="results-panel" style="margin-top:0;">
        <table class="data-table">
          <thead><tr><th style="width:70px;">Use</th><th style="width:160px;">Network</th><th>Output behavior</th></tr></thead>
          <tbody>${checkboxRows}</tbody>
        </table>
      </div>
    </div>
    <div class="action-row">
      <button id="convert" disabled>Convert</button>
    </div>
    <div id="status" class="status-panel"></div>
  </div>
</section>

<div id="outputPanel" class="is-hidden">
    <div id="results" class="results-panel"></div>
    <div class="action-row">
      <button id="openOut" class="secondary" style="display:none">Open Output Folder</button>
    </div>
</div>
</main>

<script>
  const vscode = acquireVsCodeApi();
  let outputDir = '';

  const srcEl = document.getElementById('src');
  const srcNameEl = document.getElementById('srcName');
  const outEl = document.getElementById('outDir');
  const baseNameEl = document.getElementById('baseName');
  const convertBtn = document.getElementById('convert');
  const openOutBtn = document.getElementById('openOut');
  const statusEl = document.getElementById('status');
  const resultsEl = document.getElementById('results');
  const outputPanel = document.getElementById('outputPanel');

  function updateConvertBtn() {
    const hasNet = [...document.querySelectorAll('.net-cb')].some(c => c.checked);
    convertBtn.disabled = !srcEl.value || !outEl.value || !baseNameEl.value.trim() || !hasNet;
  }

  function defaultOutputDir(dir) {
    return dir ? dir.replace(/[\\\\/]+$/, '') + '\\\\Output' : '';
  }

  document.getElementById('pickSrc').addEventListener('click', () => vscode.postMessage({ type: 'pickSource' }));
  document.getElementById('pickOut').addEventListener('click', () => vscode.postMessage({ type: 'pickOutput' }));
  document.getElementById('selectAll').addEventListener('click', () => {
    document.querySelectorAll('.net-cb').forEach(c => c.checked = c.dataset.tag !== 'tt');
    updateConvertBtn();
  });
  document.getElementById('selectNone').addEventListener('click', () => {
    document.querySelectorAll('.net-cb').forEach(c => c.checked = false);
    updateConvertBtn();
  });
  document.querySelectorAll('.net-cb').forEach(c => c.addEventListener('change', updateConvertBtn));
  outEl.addEventListener('input', updateConvertBtn);
  baseNameEl.addEventListener('input', updateConvertBtn);

  openOutBtn.addEventListener('click', () => {
    if (outputDir) vscode.postMessage({ type: 'openOutput', dir: outputDir });
  });

  function outputDisplayName(file) {
    const parts = file.split(/[\\\\/]/).filter(Boolean);
    return parts.slice(-2).join('/');
  }

  document.getElementById('convert').addEventListener('click', () => {
    const networks = [...document.querySelectorAll('.net-cb')]
      .filter(c => c.checked).map(c => c.dataset.tag);
    statusEl.textContent = 'Converting...';
    statusEl.classList.add('is-busy');
    resultsEl.innerHTML = '';
    openOutBtn.style.display = 'none';
    outputPanel.classList.add('is-hidden');
    convertBtn.disabled = true;
    vscode.postMessage({
      type: 'convert',
      opts: {
        sourcePath: srcEl.value,
        outputDir: outEl.value,
        baseName: baseNameEl.value,
        networks,
      },
    });
  });

  window.addEventListener('message', (event) => {
    const msg = event.data;
    if (msg.type === 'sourcePicked') {
      srcEl.value = msg.path;
      srcNameEl.textContent = msg.path.split(/[\\\\/]/).filter(Boolean).pop() || msg.path;
      baseNameEl.value = msg.baseName || '';
      if (!outEl.value && msg.dir) {
        outEl.value = defaultOutputDir(msg.dir);
        outputDir = outEl.value;
      }
      updateConvertBtn();
    } else if (msg.type === 'outputPicked') {
      outEl.value = msg.path;
      outputDir = msg.path;
      updateConvertBtn();
    } else if (msg.type === 'status') {
      statusEl.textContent = msg.message;
      statusEl.className = 'status-panel is-busy';
    } else if (msg.type === 'convertDone') {
      convertBtn.disabled = false;
      updateConvertBtn();
      if (msg.error) {
        statusEl.classList.remove('is-busy');
        statusEl.textContent = 'Error: ' + msg.error;
        outputPanel.classList.add('is-hidden');
        return;
      }
      const results = msg.results || [];
      statusEl.classList.remove('is-busy');
      const ok = results.filter(r => r.ok).length;
      statusEl.textContent = ok + ' of ' + results.length + ' network(s) converted.';
      if (!results.length) {
        resultsEl.innerHTML = '';
        outputPanel.classList.add('is-hidden');
        return;
      }
      outputPanel.classList.remove('is-hidden');
      resultsEl.innerHTML =
        '<table class="data-table">' +
          '<thead><tr><th style="width:120px;">Network</th><th style="width:90px;">Status</th><th style="width:90px;">Output</th><th>Path</th><th>Error</th></tr></thead>' +
          '<tbody></tbody>' +
        '</table>';
      const tbody = resultsEl.querySelector('tbody');
      for (const r of results) {
        const row = document.createElement('tr');
        const net = document.createElement('td');
        net.textContent = r.network;
        const status = document.createElement('td');
        status.innerHTML = r.ok ? '<span class="badge ok">Done</span>' : '<span class="badge err">Failed</span>';
        const outputType = document.createElement('td');
        const detail = document.createElement('td');
        detail.className = 'mono wrap';
        const error = document.createElement('td');
        error.className = 'err wrap';
        if (r.ok) {
          outputType.textContent = /\\.zip$/i.test(r.file) ? 'ZIP' : 'HTML';
          detail.textContent = outputDisplayName(r.file);
        } else {
          outputType.textContent = '-';
          detail.innerHTML = '<span class="muted">-</span>';
          error.textContent = r.error || 'Unknown error';
        }
        if (r.ok) error.innerHTML = '<span class="muted">-</span>';
        row.append(net, status, outputType, detail, error);
        tbody.appendChild(row);
      }
      if (ok > 0) {
        outputDir = outEl.value;
        openOutBtn.style.display = '';
      }
    }
  });
</script>
</body>
</html>`;
}
