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

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

interface ConvertOptions {
  sourcePath: string;
  outputDir: 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 });
        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 {
          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[] = [];
        for (const network of opts.networks) {
          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', 'vu', 'mtg']);

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

async function convertNetwork(html: string, opts: ConvertOptions, network: string): Promise<string> {
  const transformed = transformHtml(html, network);
  const needsZip = ZIPPED_NETWORKS.has(network);
  const baseName = sourceBaseName(opts.sourcePath);
  const htmlFileName = `${baseName}_${network}.html`;

  if (needsZip) {
    const tmpPath = path.join(opts.outputDir, `_tmp_${Date.now()}_${network}.html`);
    const zipPath = path.join(opts.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(opts.outputDir, htmlFileName);
  fs.writeFileSync(outPath, transformed, 'utf8');
  return outPath;
}

function transformHtml(html: string, network: string): string {
  let result = 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.
  let headInject = buildLifecycleScript();
  if (network === 'al' || network === 'is') {
    headInject += '<script src="mraid.js"></script>';
  } else if (network === 'gg') {
    headInject += '<script src="exitapi.js"></script>';
  }
  result = result.replace('</head>', headInject + '\n</head>');

  // Body-level flags
  if (network === 'vu') {
    result = result.replace('<body>', '<body>\n<script>window.__VUNGLE__=true;</script>');
  } else if (network === 'mtg') {
    result = result.replace('<body>', '<body onload="gameReady()">');
  } else if (network === 'tt') {
    result = result.replace('<body>', '<body>\n<script>window.__TIKTOK__=true;</script>');
  }

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

  // Unity: rewrite window.top → window.self (Luna static scan requirement)
  if (network === 'un') {
    result = result.split('window.top').join('window.self');
  }

  return result;
}

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 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"){if(typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",function(){mraid.open(o)});}else{mraid.open(o);}}else{window.open(o,"_blank");}`;
      break;
    case 'un':
      // Unity Ads — MRAID-based; window.top already rewritten separately
      ctaLogic = `${closeCall}if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){mraid.open(o);}else{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':
      // 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 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: 'un',  label: 'Unity',       note: 'HTML (window.top → self)' },
    { tag: 'fb',  label: 'Facebook',    note: 'HTML' },
    { tag: 'gg',  label: 'Google Ads',  note: 'ZIP + exitapi.js in head' },
    { tag: 'mo',  label: 'Moloco',      note: 'HTML' },
    { 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 => `
    <label class="net-row">
      <input type="checkbox" class="net-cb" data-tag="${n.tag}" checked />
      <span class="net-label">${n.label} <span class="net-tag">${n.tag}</span></span>
      <span class="net-note">${n.note}</span>
    </label>`).join('');

  return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<style>
  body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 12px 10px; font-size: 13px; }
  h2 { font-size: 14px; margin: 0 0 14px 0; font-weight: 600; }
  .section { margin-bottom: 14px; }
  .section-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.65; margin-bottom: 5px; }
  .row { display: flex; gap: 6px; align-items: center; }
  input[type=text] {
    flex: 1; padding: 4px 7px;
    background: var(--vscode-input-background);
    color: var(--vscode-input-foreground);
    border: 1px solid var(--vscode-input-border, #555);
    border-radius: 2px; font-size: 12px;
  }
  button {
    padding: 4px 10px; cursor: pointer; font-size: 12px; border-radius: 2px;
    background: var(--vscode-button-secondaryBackground);
    color: var(--vscode-button-secondaryForeground);
    border: 1px solid var(--vscode-panel-border, #444);
  }
  button:hover { background: var(--vscode-button-secondaryHoverBackground); }
  button.primary {
    background: var(--vscode-button-background);
    color: var(--vscode-button-foreground);
    border: none; padding: 6px 18px; font-size: 13px;
  }
  button.primary:hover { background: var(--vscode-button-hoverBackground); }
  button:disabled { opacity: 0.45; cursor: default; }
  .net-grid { display: flex; flex-direction: column; gap: 4px; }
  .net-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; user-select: none; }
  .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; }
  .toggle-row button { font-size: 11px; padding: 2px 8px; }
  .actions { display: flex; gap: 10px; align-items: center; margin-top: 16px; }
  #status { margin-top: 12px; font-size: 12px; opacity: 0.75; min-height: 16px; }
  .results { margin-top: 10px; display: flex; flex-direction: column; gap: 3px; }
  .res-row { display: flex; align-items: center; gap: 8px; font-size: 12px; font-family: var(--vscode-editor-font-family); }
  .mark { font-weight: bold; width: 14px; flex-shrink: 0; }
  .ok { color: #3fb950; }
  .bad { color: #f85149; }
  .res-net { min-width: 70px; font-weight: 500; }
  .res-file { opacity: 0.65; word-break: break-all; }
  .res-err { color: #f85149; opacity: 0.9; }
  hr { border: none; border-top: 1px solid var(--vscode-panel-border, #444); margin: 14px 0; }
</style>
</head>
<body>
<h2>Playworks Converter</h2>

<div class="section">
  <div class="section-label">Source HTML (Playworks export)</div>
  <div class="row">
    <input id="src" type="text" placeholder="Path to Playworks HTML..." readonly />
    <button id="pickSrc">Browse...</button>
  </div>
</div>

<div class="section">
  <div class="section-label">Output Folder</div>
  <div class="row">
    <input id="outDir" type="text" placeholder="Output folder..." />
    <button id="pickOut">Browse...</button>
  </div>
</div>

<hr />

<div class="section">
  <div class="section-label">Networks to generate</div>
  <div class="toggle-row">
    <button id="selectAll">All</button>
    <button id="selectNone">None</button>
  </div>
  <div class="net-grid">${checkboxRows}</div>
</div>

<div class="actions">
  <button class="primary" id="convert" disabled>Convert</button>
  <button id="openOut" style="display:none">Open Output Folder</button>
</div>

<div id="status"></div>
<div class="results" id="results"></div>

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

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

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

  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 = true);
    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);

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

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

  window.addEventListener('message', (event) => {
    const msg = event.data;
    if (msg.type === 'sourcePicked') {
      srcEl.value = msg.path;
      if (!outEl.value && msg.dir) {
        outEl.value = msg.dir;
        outputDir = msg.dir;
      }
      updateConvertBtn();
    } else if (msg.type === 'outputPicked') {
      outEl.value = msg.path;
      outputDir = msg.path;
      updateConvertBtn();
    } else if (msg.type === 'convertDone') {
      convertBtn.disabled = false;
      updateConvertBtn();
      if (msg.error) {
        statusEl.textContent = 'Error: ' + msg.error;
        return;
      }
      const results = msg.results || [];
      const ok = results.filter(r => r.ok).length;
      statusEl.textContent = ok + ' of ' + results.length + ' network(s) converted.';
      resultsEl.innerHTML = '';
      for (const r of results) {
        const row = document.createElement('div');
        row.className = 'res-row';
        const mark = document.createElement('span');
        mark.className = 'mark ' + (r.ok ? 'ok' : 'bad');
        mark.textContent = r.ok ? '✓' : '✗';
        const net = document.createElement('span');
        net.className = 'res-net';
        net.textContent = r.network;
        const detail = document.createElement('span');
        if (r.ok) {
          detail.className = 'res-file';
          detail.textContent = r.file.split(/[\\\\/]/).pop();
        } else {
          detail.className = 'res-err';
          detail.textContent = r.error || 'Unknown error';
        }
        row.append(mark, net, detail);
        resultsEl.appendChild(row);
      }
      if (ok > 0) {
        outputDir = outEl.value;
        openOutBtn.style.display = '';
      }
    }
  });
</script>
</body>
</html>`;
}
