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

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

type Severity = 'requirement' | 'best-practice';

interface MraidIssue {
  id: string;
  severity: Severity;
  title: string;
  detail: string;
  line?: number;
  reference: string;
}

interface MraidResult {
  file: string;
  ok: boolean;
  issues: MraidIssue[];
}

interface MraidRule {
  id: string;
  severity: Severity;
  title: string;
  detail: string;
  reference: string;
  test: (ctx: ScanContext) => MraidIssue | null;
}

interface ScanContext {
  filePath: string;
  html: string;
  scriptText: string;
  lowerHtml: string;
  lowerScript: string;
  mraidCallLines: Map<string, number>;
}

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

  panel.webview.html = getHtml();
  panel.webview.onDidReceiveMessage(async (msg) => {
    if (msg.type === 'ready') {
      const ws = vscode.workspace.workspaceFolders?.[0];
      if (ws) {
        const distPath = path.join(ws.uri.fsPath, 'dist');
        if (fs.existsSync(distPath) && fs.statSync(distPath).isDirectory()) {
          const files = (await collectHtmlFiles(distPath)).filter(isInSupportedMraidFolderPath);
          panel.webview.postMessage({ type: 'filesPicked', paths: files });
        }
      }
    } else if (msg.type === 'pickFolder') {
      const picked = await vscode.window.showOpenDialog({
        canSelectFolders: true,
        canSelectFiles: false,
        canSelectMany: false,
        openLabel: 'Select Folder',
      });
      if (picked && picked[0]) {
        const files = (await collectHtmlFiles(picked[0].fsPath)).filter(isInSupportedMraidFolderPath);
        panel.webview.postMessage({ type: 'filesPicked', paths: files });
      }
    } else if (msg.type === 'pickFiles') {
      const picked = await vscode.window.showOpenDialog({
        canSelectFolders: false,
        canSelectFiles: true,
        canSelectMany: true,
        openLabel: 'Select Files',
        filters: { 'HTML': ['html', 'htm'] },
      });
      if (picked && picked.length) {
        panel.webview.postMessage({ type: 'filesPicked', paths: picked.map(u => u.fsPath) });
      }
    } else if (msg.type === 'scan') {
      const folder: string | undefined = msg.folder;
      const files: string[] | undefined = msg.files;
      let targets: string[] = [];
      let baseDir = '';
      let skipped = 0;
      let skippedReason = '';
      let noTargetsError = 'No HTML files were found.';
      if (files && files.length) {
        const existing = files.filter(f => fs.existsSync(f));
        targets = existing;
        skipped = files.length - existing.length;
        skippedReason = 'missing file(s).';
        noTargetsError = 'No selected HTML files were found.';
        baseDir = targets.length ? commonBaseDir(targets) : '';
      } else if (folder && fs.existsSync(folder)) {
        const allHtmlFiles = await collectHtmlFiles(folder);
        targets = allHtmlFiles.filter(isInSupportedMraidFolderPath);
        skipped = allHtmlFiles.length - targets.length;
        skippedReason = 'HTML file(s) outside AppLovin/ironSource/Unity folders.';
        noTargetsError = 'No AppLovin, ironSource, or Unity folder HTML files were found.';
        baseDir = folder;
      } else {
        panel.webview.postMessage({ type: 'results', results: [], error: 'Pick a folder or files first' });
        return;
      }

      if (targets.length === 0) {
        panel.webview.postMessage({
          type: 'results',
          results: [],
          skipped,
          skippedReason,
          error: noTargetsError,
        });
        return;
      }

      const results: MraidResult[] = [];
      for (let i = 0; i < targets.length; i++) {
        const file = targets[i];
        panel.webview.postMessage({ type: 'status', message: `Scanning ${i + 1}/${targets.length} ${path.basename(file)}` });
        try {
          const content = fs.readFileSync(file, 'utf8');
          const issues = checkMraid(content, file);
          const display = baseDir ? path.relative(baseDir, file) || path.basename(file) : file;
          results.push({ file: display, ok: issues.length === 0, issues });
        } catch {
          // Skip unreadable files, matching the scanner pattern used elsewhere.
        }
      }
      panel.webview.postMessage({ type: 'results', results, skipped, skippedReason });
    }
  });
}

function commonBaseDir(files: string[]): string {
  if (files.length === 0) return '';
  if (files.length === 1) return path.dirname(files[0]);
  const split = files.map(f => path.dirname(f).split(path.sep));
  const first = split[0];
  let i = 0;
  while (i < first.length && split.every(parts => parts[i] === first[i])) i++;
  return first.slice(0, i).join(path.sep);
}

async function collectHtmlFiles(root: string): Promise<string[]> {
  const out: string[] = [];
  const stack: string[] = [root];
  while (stack.length) {
    const dir = stack.pop()!;
    let entries: fs.Dirent[];
    try {
      entries = fs.readdirSync(dir, { withFileTypes: true });
    } catch {
      continue;
    }
    for (const e of entries) {
      const full = path.join(dir, e.name);
      if (e.isDirectory()) {
        if (e.name === 'node_modules' || e.name === '.git') continue;
        stack.push(full);
      } else if (e.isFile() && /\.html?$/i.test(e.name)) {
        out.push(full);
      }
    }
  }
  return out;
}

function checkMraid(html: string, filePath = ''): MraidIssue[] {
  const scriptText = extractScriptText(html);
  const ctx: ScanContext = {
    filePath,
    html,
    scriptText,
    lowerHtml: html.toLowerCase(),
    lowerScript: scriptText.toLowerCase(),
    mraidCallLines: collectMraidCallLines(html),
  };

  return MRAID_RULES
    .map(rule => rule.test(ctx))
    .filter((issue): issue is MraidIssue => issue !== null);
}

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

function collectMraidCallLines(html: string): Map<string, number> {
  const calls = new Map<string, number>();
  const lines = html.split(/\r?\n/);
  const callRegex = /\bmraid\s*\.\s*([a-zA-Z_$][\w$]*)\s*\(/g;
  for (let i = 0; i < lines.length; i++) {
    let m: RegExpExecArray | null;
    while ((m = callRegex.exec(lines[i])) !== null) {
      if (!calls.has(m[1])) calls.set(m[1], i + 1);
    }
  }
  return calls;
}

function isInSupportedMraidFolderPath(filePath: string): boolean {
  const folderParts = path.dirname(filePath)
    .split(/[\\/]+/)
    .map(part => part.toLowerCase());
  return folderParts.some(part => [
    'al',
    'applovin',
    'app-lovin',
    'is',
    'ironsource',
    'iron-source',
    'iron_source',
    'un',
    'unity',
    'unityads',
    'unity-ads',
    'unity_ads',
  ].includes(part));
}

function hasMraidReference(ctx: ScanContext): boolean {
  return /\bmraid\b/i.test(ctx.html);
}

function countMraidJsReferences(ctx: ScanContext): number {
  return (ctx.html.match(/\bmraid\.js\b/gi) || []).length;
}

function hasViewportMeta(ctx: ScanContext): boolean {
  return /<meta\b[^>]*\bname\s*=\s*['"]viewport['"][^>]*>/i.test(ctx.html);
}

function hasMraidCall(ctx: ScanContext, method: string): boolean {
  return ctx.mraidCallLines.has(method);
}

function mraidLine(ctx: ScanContext, method: string): number | undefined {
  return ctx.mraidCallLines.get(method);
}

function issue(rule: MraidRule, line?: number): MraidIssue {
  return {
    id: rule.id,
    severity: rule.severity,
    title: rule.title,
    detail: rule.detail,
    line,
    reference: rule.reference,
  };
}

function hasReadyGate(ctx: ScanContext): boolean {
  return (
    /mraid\s*\.\s*addEventListener\s*\(\s*['"]ready['"]/i.test(ctx.scriptText) ||
    /mraid\s*\.\s*getState\s*\(\s*\)\s*={0,2}\s*['"]loading['"]/i.test(ctx.scriptText) ||
    /['"]loading['"]\s*={0,2}\s*mraid\s*\.\s*getState\s*\(\s*\)/i.test(ctx.scriptText)
  );
}

function hasDomReadyGate(ctx: ScanContext): boolean {
  return (
    /document\s*\.\s*readyState/i.test(ctx.scriptText) ||
    /DOMContentLoaded/i.test(ctx.scriptText) ||
    /\bwindow\s*\.\s*(addEventListener\s*\(\s*['"]load['"]|onload\b)/i.test(ctx.scriptText)
  );
}

function hasReadyListenerAndStateFallback(ctx: ScanContext): boolean {
  return hasEventListener(ctx, 'ready') && hasMraidCall(ctx, 'getState');
}

function hasMraidTypeofGuard(ctx: ScanContext): boolean {
  return /typeof\s+(?:window\s*\.\s*)?mraid\s*!={1,2}\s*['"]undefined['"]/i.test(ctx.scriptText);
}

function hasMraidFunctionGuard(ctx: ScanContext, method: string): boolean {
  const escaped = method.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  return new RegExp(`typeof\\s+(?:window\\s*\\.\\s*)?mraid\\s*\\.\\s*${escaped}\\s*={2,3}\\s*['"]function['"]|typeof\\s+(?:window\\s*\\.\\s*)?mraid\\s*\\.\\s*${escaped}\\s*!={1,2}\\s*['"]undefined['"]`, 'i').test(ctx.scriptText);
}

function hasMraidReadyTimeout(ctx: ScanContext): boolean {
  return /setTimeout\s*\(/i.test(ctx.scriptText) && /\bmraid\b/i.test(ctx.scriptText);
}

function hasLateMraidDetection(ctx: ScanContext): boolean {
  return /setInterval\s*\(|setTimeout\s*\(|requestAnimationFrame\s*\(/i.test(ctx.scriptText) && /window\s*\.\s*mraid|\bmraid\b/i.test(ctx.scriptText);
}

function hasMraidOpenGuard(ctx: ScanContext): boolean {
  return (
    /typeof\s+(?:window\s*\.\s*)?mraid\s*\.\s*open\s*={2,3}\s*['"]function['"]/i.test(ctx.scriptText) ||
    /typeof\s+(?:window\s*\.\s*)?mraid\s*\.\s*open\s*!={1,2}\s*['"]undefined['"]/i.test(ctx.scriptText)
  );
}

function hasMraidOpenLoadingGuard(ctx: ScanContext): boolean {
  return (
    /mraid\s*\.\s*getState\s*\(\s*\)\s*!={1,2}\s*['"]loading['"]/i.test(ctx.scriptText) ||
    /['"]loading['"]\s*!={1,2}\s*mraid\s*\.\s*getState\s*\(\s*\)/i.test(ctx.scriptText) ||
    /typeof\s+(?:window\s*\.\s*)?mraid\s*\.\s*getState\s*!={1,2}\s*['"]function['"]/i.test(ctx.scriptText) ||
    (hasMraidCall(ctx, 'getState') && /\b\w+\s*!={1,2}\s*['"]loading['"]|['"]loading['"]\s*!={1,2}\s*\w+\b/i.test(ctx.scriptText)) ||
    (hasMraidCall(ctx, 'getState') && /\b\w+\s*={2,3}\s*['"]loading['"][\s\S]{0,500}\bwindow\s*\.\s*open\s*\(/i.test(ctx.scriptText))
  );
}

function hasAudioVolumeNullSafeHandling(ctx: ScanContext): boolean {
  return hasEventListener(ctx, 'audioVolumeChange') && (/typeof\s+\w+\s*={2,3}\s*['"]number['"]/i.test(ctx.scriptText) || /!=\s*null|!==\s*null/i.test(ctx.scriptText));
}

function hasLifecycleSignal(ctx: ScanContext, name: string): boolean {
  return new RegExp(`\\b${name}\\b`, 'i').test(ctx.html);
}

function hasUnsupportedHyperlink(ctx: ScanContext): boolean {
  return /<a\b[^>]*\bhref\s*=/i.test(ctx.html);
}

function hasTwoPartExpand(ctx: ScanContext): boolean {
  return /mraid\s*\.\s*expand\s*\(\s*[^)\s]/i.test(ctx.scriptText);
}

function opensVideoUrl(ctx: ScanContext): boolean {
  return /mraid\s*\.\s*open\s*\(\s*[^)]*\.(mp4|m4v|mov|webm|avi|m3u8)(?:['"`?#)]|$)/i.test(ctx.scriptText);
}

function isInMraidNetworkPath(ctx: ScanContext): boolean {
  if (!ctx.filePath) return true;
  return isInSupportedMraidFolderPath(ctx.filePath) || /_(al|is|un)\.html?$/i.test(path.basename(ctx.filePath));
}

function hasEventListener(ctx: ScanContext, eventName: string): boolean {
  const pattern = new RegExp(`mraid\\s*\\.\\s*addEventListener\\s*\\(\\s*['"]${eventName}['"]`, 'i');
  return pattern.test(ctx.scriptText);
}

function hasSupportCheck(ctx: ScanContext, feature: string): boolean {
  const pattern = new RegExp(`mraid\\s*\\.\\s*supports\\s*\\(\\s*['"]${feature}['"]`, 'i');
  return pattern.test(ctx.scriptText);
}

function usesDirectBrowserNavigation(ctx: ScanContext): boolean {
  return /\bwindow\s*\.\s*open\s*\(/i.test(ctx.scriptText);
}

function usesLocationNavigation(ctx: ScanContext): boolean {
  return /\blocation\s*\.\s*(href|assign|replace)\b/i.test(ctx.scriptText);
}

const MRAID_REFERENCE = 'GUIDE.md, MRAID 3.0 specification, and MRAID 3.0 Best Practices Guide.';

const MRAID_RULES: MraidRule[] = [
  {
    id: 'mraid-js-missing',
    severity: 'requirement',
    title: 'mraid.js reference not found',
    detail: 'MRAID creatives for AppLovin, ironSource, and Unity must request mraid.js early in the generated HTML.',
    reference: MRAID_REFERENCE,
    test: (ctx) => isInMraidNetworkPath(ctx) && countMraidJsReferences(ctx) === 0 ? issue(MRAID_RULES_BY_ID['mraid-js-missing']) : null,
  },
  {
    id: 'mraid-js-duplicate',
    severity: 'requirement',
    title: 'Multiple mraid.js references found',
    detail: 'Request mraid.js exactly once; duplicate references can inject multiple MRAID libraries and slow or destabilize the ad.',
    reference: MRAID_REFERENCE,
    test: (ctx) => countMraidJsReferences(ctx) > 1 ? issue(MRAID_RULES_BY_ID['mraid-js-duplicate']) : null,
  },
  {
    id: 'mraid-reference',
    severity: 'requirement',
    title: 'MRAID API reference not found',
    detail: 'Playable HTML should use the injected mraid object when it depends on an MRAID container.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidReference(ctx) ? null : issue(MRAID_RULES_BY_ID['mraid-reference']),
  },
  {
    id: 'viewport-meta',
    severity: 'best-practice',
    title: 'Viewport meta tag not found',
    detail: 'MRAID creatives should include a mobile viewport meta tag so the ad scales predictably on mobile WebViews.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasViewportMeta(ctx) ? null : issue(MRAID_RULES_BY_ID['viewport-meta']),
  },
  {
    id: 'mraid-typeof-guard',
    severity: 'requirement',
    title: 'MRAID object is not guarded',
    detail: 'Guard MRAID access with typeof mraid !== "undefined" or an equivalent window.mraid guard before making MRAID calls.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidReference(ctx) && !hasMraidTypeofGuard(ctx) ? issue(MRAID_RULES_BY_ID['mraid-typeof-guard']) : null,
  },
  {
    id: 'dom-ready-gate',
    severity: 'requirement',
    title: 'DOM readiness is not guarded',
    detail: 'MRAID startup should wait for DOM readiness as well as MRAID readiness before building or binding rich media content.',
    reference: MRAID_REFERENCE,
    test: (ctx) => !hasMraidReference(ctx) || hasDomReadyGate(ctx) ? null : issue(MRAID_RULES_BY_ID['dom-ready-gate']),
  },
  {
    id: 'ready-gate',
    severity: 'requirement',
    title: 'MRAID ready state is not guarded',
    detail: 'Gate ad startup behind mraid.ready, or call startup immediately only when mraid.getState() is not loading.',
    reference: MRAID_REFERENCE,
    test: (ctx) => !hasMraidReference(ctx) || hasReadyGate(ctx) ? null : issue(MRAID_RULES_BY_ID['ready-gate']),
  },
  {
    id: 'ready-state-fallback',
    severity: 'requirement',
    title: 'MRAID ready listener/state fallback pair not found',
    detail: 'Use both mraid.addEventListener("ready", ...) and mraid.getState() so startup works when ready fires before the listener attaches.',
    reference: MRAID_REFERENCE,
    test: (ctx) => !hasMraidReference(ctx) || hasReadyListenerAndStateFallback(ctx) ? null : issue(MRAID_RULES_BY_ID['ready-state-fallback']),
  },
  {
    id: 'ready-timeout-fallback',
    severity: 'best-practice',
    title: 'MRAID ready timeout fallback not found',
    detail: 'A timeout fallback prevents startup hangs when a container never fires ready.',
    reference: MRAID_REFERENCE,
    test: (ctx) => !hasMraidReference(ctx) || hasMraidReadyTimeout(ctx) ? null : issue(MRAID_RULES_BY_ID['ready-timeout-fallback']),
  },
  {
    id: 'late-mraid-detection',
    severity: 'best-practice',
    title: 'Late MRAID injection detection not found',
    detail: 'Poll or retry briefly for late window.mraid injection before deciding the creative is running without MRAID.',
    reference: MRAID_REFERENCE,
    test: (ctx) => !hasMraidReference(ctx) || hasLateMraidDetection(ctx) ? null : issue(MRAID_RULES_BY_ID['late-mraid-detection']),
  },
  {
    id: 'error-listener',
    severity: 'best-practice',
    title: 'No MRAID error listener found',
    detail: 'Listen for mraid error events so unsupported or failed MRAID calls can be logged or handled gracefully.',
    reference: MRAID_REFERENCE,
    test: (ctx) => !hasMraidReference(ctx) || hasEventListener(ctx, 'error') ? null : issue(MRAID_RULES_BY_ID['error-listener']),
  },
  {
    id: 'viewability',
    severity: 'best-practice',
    title: 'MRAID viewability fallback chain not found',
    detail: 'Use exposureChange for MRAID 3.0 and keep viewableChange or isViewable() as MRAID 2.0 fallback before starting animation, audio, video, or timers.',
    reference: MRAID_REFERENCE,
    test: (ctx) => {
      const hasVisibilitySignal = hasEventListener(ctx, 'exposureChange') && (hasMraidCall(ctx, 'isViewable') || hasEventListener(ctx, 'viewableChange'));
      return !hasMraidReference(ctx) || hasVisibilitySignal ? null : issue(MRAID_RULES_BY_ID['viewability']);
    },
  },
  {
    id: 'audio-volume-change',
    severity: 'best-practice',
    title: 'audioVolumeChange handling not found',
    detail: 'Listen for audioVolumeChange and ignore null values; only apply volume math when the value is numeric.',
    reference: MRAID_REFERENCE,
    test: (ctx) => !hasMraidReference(ctx) || hasAudioVolumeNullSafeHandling(ctx) ? null : issue(MRAID_RULES_BY_ID['audio-volume-change']),
  },
  {
    id: 'open-api',
    severity: 'requirement',
    title: 'Browser navigation used instead of mraid.open',
    detail: 'Use mraid.open(url) for click-through navigation inside an MRAID ad container; avoid hyperlinks, location redirects, and unguarded browser navigation.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasUnsupportedHyperlink(ctx) || usesLocationNavigation(ctx) || (usesDirectBrowserNavigation(ctx) && !hasMraidCall(ctx, 'open')) ? issue(MRAID_RULES_BY_ID['open-api']) : null,
  },
  {
    id: 'open-guarded-fallback',
    severity: 'requirement',
    title: 'mraid.open guarded fallback not found',
    detail: 'Guard mraid.open with function/state checks and provide a window.open fallback when MRAID is unavailable or still loading.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidCall(ctx, 'open') && (!hasMraidOpenGuard(ctx) || !hasMraidOpenLoadingGuard(ctx) || !usesDirectBrowserNavigation(ctx)) ? issue(MRAID_RULES_BY_ID['open-guarded-fallback'], mraidLine(ctx, 'open')) : null,
  },
  {
    id: 'open-video-url',
    severity: 'best-practice',
    title: 'mraid.open appears to target video media',
    detail: 'Use mraid.playVideo(url) for native video playback cases instead of mraid.open(videoUrl).',
    reference: MRAID_REFERENCE,
    test: (ctx) => opensVideoUrl(ctx) && !hasMraidCall(ctx, 'playVideo') ? issue(MRAID_RULES_BY_ID['open-video-url'], mraidLine(ctx, 'open')) : null,
  },
  {
    id: 'close-api',
    severity: 'best-practice',
    title: 'No mraid.close call found',
    detail: 'If the creative renders its own close control, wire that control to mraid.close().',
    reference: MRAID_REFERENCE,
    test: (ctx) => {
      const appearsToHaveCloseUi = /\b(close|dismiss|skip)\b/i.test(ctx.html);
      return appearsToHaveCloseUi && !hasMraidCall(ctx, 'close') ? issue(MRAID_RULES_BY_ID['close-api']) : null;
    },
  },
  {
    id: 'unload-fallback',
    severity: 'best-practice',
    title: 'No mraid.unload graceful failure path found',
    detail: 'Use mraid.unload() when the ad cannot run or refuses to show, instead of leaving a broken or blank creative.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidReference(ctx) && !hasMraidCall(ctx, 'unload') ? issue(MRAID_RULES_BY_ID['unload-fallback']) : null,
  },
  {
    id: 'resize-properties',
    severity: 'requirement',
    title: 'mraid.resize used without setResizeProperties',
    detail: 'Call mraid.setResizeProperties(...) before mraid.resize(); otherwise the container should emit an error.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidCall(ctx, 'resize') && !hasMraidCall(ctx, 'setResizeProperties') ? issue(MRAID_RULES_BY_ID['resize-properties'], mraidLine(ctx, 'resize')) : null,
  },
  {
    id: 'use-custom-close',
    severity: 'best-practice',
    title: 'Deprecated useCustomClose found',
    detail: 'MRAID 3.0 ignores useCustomClose; the host provides the mandatory close control.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidCall(ctx, 'useCustomClose') || /useCustomClose/i.test(ctx.scriptText) ? issue(MRAID_RULES_BY_ID['use-custom-close'], mraidLine(ctx, 'useCustomClose')) : null,
  },
  {
    id: 'two-part-expand',
    severity: 'best-practice',
    title: 'Two-part expand appears to be used',
    detail: 'Two-part expandable ads are deprecated in MRAID 3.0; use self-contained one-part expandables.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasTwoPartExpand(ctx) ? issue(MRAID_RULES_BY_ID['two-part-expand'], mraidLine(ctx, 'expand')) : null,
  },
  {
    id: 'expand-support',
    severity: 'requirement',
    title: 'mraid.expand used without function guard',
    detail: 'Guard mraid.expand with a function check before using expand behavior.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidCall(ctx, 'expand') && !hasMraidFunctionGuard(ctx, 'expand') ? issue(MRAID_RULES_BY_ID['expand-support'], mraidLine(ctx, 'expand')) : null,
  },
  {
    id: 'resize-support',
    severity: 'requirement',
    title: 'mraid.resize used without function guard',
    detail: 'Guard mraid.resize with a function check before using resize behavior.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidCall(ctx, 'resize') && !hasMraidFunctionGuard(ctx, 'resize') ? issue(MRAID_RULES_BY_ID['resize-support'], mraidLine(ctx, 'resize')) : null,
  },
  {
    id: 'store-picture-support',
    severity: 'requirement',
    title: 'storePicture used without supports check',
    detail: 'Call mraid.supports("storePicture") before mraid.storePicture(...).',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidCall(ctx, 'storePicture') && !hasSupportCheck(ctx, 'storePicture') ? issue(MRAID_RULES_BY_ID['store-picture-support'], mraidLine(ctx, 'storePicture')) : null,
  },
  {
    id: 'calendar-support',
    severity: 'requirement',
    title: 'createCalendarEvent used without supports check',
    detail: 'Call mraid.supports("calendar") before mraid.createCalendarEvent(...).',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidCall(ctx, 'createCalendarEvent') && !hasSupportCheck(ctx, 'calendar') ? issue(MRAID_RULES_BY_ID['calendar-support'], mraidLine(ctx, 'createCalendarEvent')) : null,
  },
  {
    id: 'location-support',
    severity: 'requirement',
    title: 'getLocation used without supports check',
    detail: 'Call mraid.supports("location") before mraid.getLocation(). Do not use HTML5 geolocation as a substitute in MRAID.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidCall(ctx, 'getLocation') && !hasSupportCheck(ctx, 'location') ? issue(MRAID_RULES_BY_ID['location-support'], mraidLine(ctx, 'getLocation')) : null,
  },
  {
    id: 'vpaid-support',
    severity: 'requirement',
    title: 'VPAID integration used without supports check',
    detail: 'Call mraid.supports("vpaid") before mraid.initVpaid(...).',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidCall(ctx, 'initVpaid') && !hasSupportCheck(ctx, 'vpaid') ? issue(MRAID_RULES_BY_ID['vpaid-support'], mraidLine(ctx, 'initVpaid')) : null,
  },
  {
    id: 'orientation-support',
    severity: 'best-practice',
    title: 'Orientation properties set without function guard',
    detail: 'Guard mraid.setOrientationProperties with a function check before requiring orientation behavior.',
    reference: MRAID_REFERENCE,
    test: (ctx) => hasMraidCall(ctx, 'setOrientationProperties') && !hasMraidFunctionGuard(ctx, 'setOrientationProperties') ? issue(MRAID_RULES_BY_ID['orientation-support'], mraidLine(ctx, 'setOrientationProperties')) : null,
  },
  {
    id: 'size-change',
    severity: 'best-practice',
    title: 'No sizeChange listener found',
    detail: 'Listen for sizeChange when layout depends on container size, resize, expand, or orientation behavior.',
    reference: MRAID_REFERENCE,
    test: (ctx) => {
      const dependsOnSize = hasMraidCall(ctx, 'getMaxSize') || hasMraidCall(ctx, 'getScreenSize') || hasMraidCall(ctx, 'resize') || hasMraidCall(ctx, 'expand') || hasMraidCall(ctx, 'setOrientationProperties');
      return dependsOnSize && !hasEventListener(ctx, 'sizeChange') ? issue(MRAID_RULES_BY_ID['size-change']) : null;
    },
  },
  {
    id: 'state-change',
    severity: 'best-practice',
    title: 'No stateChange listener found',
    detail: 'Listen for stateChange when using expand, resize, close, or other state-changing container behavior.',
    reference: MRAID_REFERENCE,
    test: (ctx) => {
      const changesState = hasMraidCall(ctx, 'expand') || hasMraidCall(ctx, 'resize') || hasMraidCall(ctx, 'close');
      return changesState && !hasEventListener(ctx, 'stateChange') ? issue(MRAID_RULES_BY_ID['state-change']) : null;
    },
  },
  {
    id: 'output-script-hygiene',
    severity: 'requirement',
    title: 'Rejected script attributes found',
    detail: 'Playable output should not contain type="module" or crossorigin on script tags; ad networks can reject ES modules/CORS attributes.',
    reference: MRAID_REFERENCE,
    test: (ctx) => /<script\b[^>]*(\btype\s*=\s*['"]module['"]|\bcrossorigin\b)/i.test(ctx.html) ? issue(MRAID_RULES_BY_ID['output-script-hygiene']) : null,
  },
  {
    id: 'console-error',
    severity: 'best-practice',
    title: 'console.error found',
    detail: 'The ship checklist expects no console.error calls in output; use guarded logging or remove debug error logs before submission.',
    reference: MRAID_REFERENCE,
    test: (ctx) => /\bconsole\s*\.\s*error\s*\(/i.test(ctx.scriptText) ? issue(MRAID_RULES_BY_ID['console-error']) : null,
  },
  {
    id: 'lifecycle-stubs',
    severity: 'requirement',
    title: 'Playable lifecycle signals not found',
    detail: 'Expose or call gameReady, gameStart, gameEnd, and gameClose so network preview tools can detect the playable lifecycle.',
    reference: MRAID_REFERENCE,
    test: (ctx) => {
      const missing = ['gameReady', 'gameStart', 'gameEnd', 'gameClose'].filter(name => !hasLifecycleSignal(ctx, name));
      if (!missing.length) return null;
      const rule = MRAID_RULES_BY_ID['lifecycle-stubs'];
      return { ...issue(rule), detail: `${rule.detail} Missing: ${missing.join(', ')}.` };
    },
  },
];

const MRAID_RULES_BY_ID = MRAID_RULES.reduce<Record<string, MraidRule>>((acc, rule) => {
  acc[rule.id] = rule;
  return acc;
}, {});

function getHtml(): string {
  return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<style>
${getToolWebviewStyles()}
  .issues { margin: 0; padding: 0; }
  .issue { list-style: none; margin: 0 0 8px; }
  .issue:last-child { margin-bottom: 0; }
  .severity-badge {
    display: inline-flex;
    min-width: 82px;
    justify-content: center;
    margin-right: 6px;
    padding: 1px 6px;
    border-radius: 999px;
    border: 1px solid var(--tool-border);
    font-size: 11px;
  }
  .requirement { color: var(--vscode-errorForeground); background: var(--vscode-inputValidation-errorBackground, rgba(248,81,73,0.14)); }
  .best-practice { color: var(--vscode-editorWarning-foreground, #d29922); background: var(--vscode-inputValidation-warningBackground, rgba(210,153,34,0.14)); }
  .issue-title { font-weight: 600; }
  .issue-detail { color: var(--vscode-descriptionForeground); margin-top: 2px; }
  .selected-list { margin-top: 8px; }
  .pager { margin-top: 8px; display: flex; align-items: center; gap: 8px; justify-content: flex-end; }
</style>
</head>
<body>
<main class="tool-page">
  <header class="tool-header">
    <h2 class="tool-title">MRAID Checker</h2>
    <p class="tool-description">Static checks for AppLovin, ironSource, and Unity HTML builds based on GUIDE.md plus the MRAID 3.0 specification and Best Practices Guide.</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="field-row">
          <input id="folder" type="hidden" />
          <button id="pick" class="secondary">Select Folder</button>
          <button id="pickFiles" class="secondary">Select File(s)</button>
          <button id="clear" class="secondary">Clear</button>
        </div>
        <div id="selection" class="selected-list"></div>
      </div>
      <div class="action-row">
        <button id="scan">Scan</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>
</main>
<script>
  const vscode = acquireVsCodeApi();
  vscode.postMessage({ type: 'ready' });
  const folderEl = document.getElementById('folder');
  const statusEl = document.getElementById('status');
  const resultsEl = document.getElementById('results');
  const outputPanel = document.getElementById('outputPanel');
  const selectionEl = document.getElementById('selection');
  const PAGE_SIZE = 5;
  let pickedFolder = '';
  let pickedFiles = [];
  let selectedPage = 0;

  function basename(p) {
    const i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\\\'));
    return i >= 0 ? p.slice(i + 1) : p;
  }

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

  function renderSelection() {
    const oldPager = document.getElementById('selectedPager');
    if (oldPager) oldPager.remove();
    const items = pickedFiles.length
      ? pickedFiles.map((p, i) => ({ label: basename(p), index: i, type: 'file' }))
      : (pickedFolder ? [{ label: pickedFolder, index: 0, type: 'folder' }] : []);
    if (!items.length) {
      selectionEl.innerHTML = '<span class="file-name">(no files selected)</span>';
      return;
    }
    const maxPage = Math.max(0, Math.ceil(items.length / PAGE_SIZE) - 1);
    selectedPage = Math.min(selectedPage, maxPage);
    const visibleItems = items.slice(selectedPage * PAGE_SIZE, selectedPage * PAGE_SIZE + PAGE_SIZE);
    selectionEl.innerHTML =
      '<div class="results-panel" style="margin-top:0;">' +
        '<table class="data-table">' +
          '<thead><tr><th>Filename</th><th style="width:44px;"></th></tr></thead>' +
          '<tbody>' +
            visibleItems.map(item =>
              '<tr><td class="mono wrap">' + escapeText(item.label) + '</td>' +
              '<td><button class="remove-selected danger" data-index="' + item.index + '" data-type="' + item.type + '" title="Remove">×</button></td></tr>'
            ).join('') +
          '</tbody>' +
        '</table>' +
      '</div>';
    selectionEl.querySelectorAll('.remove-selected').forEach(btn => {
      btn.addEventListener('click', () => {
        if (btn.dataset.type === 'folder') {
          pickedFolder = '';
          folderEl.value = '';
        } else {
          pickedFiles.splice(Number(btn.dataset.index), 1);
        }
        renderSelection();
      });
    });
    if (items.length > PAGE_SIZE) {
      const pager = document.createElement('div');
      pager.id = 'selectedPager';
      pager.className = 'pager';
      pager.innerHTML =
        '<button class="secondary" id="prevPage"' + (selectedPage === 0 ? ' disabled' : '') + '>Previous</button>' +
        '<span class="file-name">Page ' + (selectedPage + 1) + ' of ' + (maxPage + 1) + '</span>' +
        '<button class="secondary" id="nextPage"' + (selectedPage === maxPage ? ' disabled' : '') + '>Next</button>';
      selectionEl.appendChild(pager);
      pager.querySelector('#prevPage').addEventListener('click', () => { selectedPage--; renderSelection(); });
      pager.querySelector('#nextPage').addEventListener('click', () => { selectedPage++; renderSelection(); });
    }
  }

  renderSelection();

  document.getElementById('pick').addEventListener('click', () => {
    vscode.postMessage({ type: 'pickFolder' });
  });
  document.getElementById('pickFiles').addEventListener('click', () => {
    vscode.postMessage({ type: 'pickFiles' });
  });
  document.getElementById('clear').addEventListener('click', () => {
    folderEl.value = '';
    pickedFolder = '';
    pickedFiles = [];
    selectedPage = 0;
    renderSelection();
    statusEl.textContent = '';
    statusEl.classList.remove('is-busy');
    resultsEl.innerHTML = '';
    outputPanel.classList.add('is-hidden');
  });
  document.getElementById('scan').addEventListener('click', () => {
    statusEl.textContent = 'Scanning...';
    statusEl.classList.add('is-busy');
    resultsEl.innerHTML = '';
    outputPanel.classList.add('is-hidden');
    if (pickedFiles.length) {
      vscode.postMessage({ type: 'scan', files: pickedFiles });
    } else {
      const folder = folderEl.value.trim();
      if (!folder) { statusEl.textContent = 'Pick a folder or files first.'; return; }
      vscode.postMessage({ type: 'scan', folder });
    }
  });

  window.addEventListener('message', (event) => {
    const msg = event.data;
    if (msg.type === 'folderPicked') {
      folderEl.value = msg.path;
      pickedFolder = msg.path;
      pickedFiles = [];
      selectedPage = 0;
      renderSelection();
    } else if (msg.type === 'filesPicked') {
      const existing = new Set(pickedFiles);
      pickedFiles = pickedFiles.concat((msg.paths || []).filter(p => !existing.has(p)));
      folderEl.value = '';
      pickedFolder = '';
      selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
      renderSelection();
    } else if (msg.type === 'status') {
      statusEl.textContent = msg.message;
      statusEl.className = 'status-panel is-busy';
    } else if (msg.type === 'results') {
      if (msg.error) {
        statusEl.classList.remove('is-busy');
        statusEl.textContent = 'Error: ' + msg.error;
        outputPanel.classList.add('is-hidden');
        return;
      }
      const total = msg.results.length;
      statusEl.classList.remove('is-busy');
      const withIssues = msg.results.filter(r => !r.ok).length;
      const skipped = Number(msg.skipped || 0);
      const skippedReason = msg.skippedReason || 'file(s).';
      const skippedText = skipped ? ' Skipped ' + skipped + ' ' + skippedReason : '';
      statusEl.textContent = 'Scanned ' + total + ' HTML file(s). ' + withIssues + ' file(s) need review.' + skippedText;
      if (!total) {
        resultsEl.innerHTML = '';
        outputPanel.classList.add('is-hidden');
        return;
      }
      outputPanel.classList.remove('is-hidden');
      resultsEl.innerHTML =
        '<table class="data-table">' +
          '<thead><tr><th>File</th><th>Results</th></tr></thead>' +
          '<tbody></tbody>' +
        '</table>';
      const tbody = resultsEl.querySelector('tbody');
      for (const r of msg.results) {
        const row = document.createElement('tr');
        const file = document.createElement('td');
        file.className = 'mono wrap';
        file.textContent = r.file;
        const issues = document.createElement('td');
        issues.className = 'wrap';
        if (!r.ok) {
          const list = document.createElement('ul');
          list.className = 'issues';
          for (const i of r.issues) {
            const item = document.createElement('li');
            item.className = 'issue';
            const line = i.line ? ' line ' + i.line + ':' : '';
            item.innerHTML =
              '<span class="severity-badge ' + i.severity + '">' + escapeText(i.severity) + '</span>' +
              '<span class="issue-title">' + escapeText(line + ' ' + i.title) + '</span>' +
              '<div class="issue-detail">' + escapeText(i.detail) + '</div>';
            list.appendChild(item);
          }
          issues.appendChild(list);
        } else {
          issues.innerHTML = '<span class="badge ok">OK</span>';
        }
        row.append(file, issues);
        tbody.appendChild(row);
      }
    }
  });
</script>
</body>
</html>`;
}
