#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const vm = require('vm');


function fail(message) {
  console.error(`Error: ${message}`);
  process.exit(1);
}

function read(file) {
  return fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '');
}

function mainScript(html) {
  const a = html.lastIndexOf('<script>');
  const b = html.lastIndexOf('</script>');
  if (a === -1 || b === -1 || b <= a) return null;
  return html.slice(a + '<script>'.length, b);
}

function audioTagById(html, id) {
  const match = html.match(new RegExp(`<audio\\b[^>]*\\bid=["']${id}["'][^>]*>`, 'i'));
  return match ? match[0] : '';
}

function getAttr(tag, name) {
  const m = tag.match(new RegExp(`\\b${name}=["']([^"']*)["']`, 'i'));
  return m ? m[1] : null;
}

function constValue(source, name) {
  const m = source.match(new RegExp(`const\\s+${name}\\s*=\\s*([^;\\n]+)`));
  return m ? m[1].trim() : null;
}

function extractArrayLiteral(source, constName) {
  const marker = new RegExp(`const\\s+${constName}\\s*=\\s*\\[`);
  const match = marker.exec(source);
  if (!match) return null;
  let index = match.index + match[0].lastIndexOf('[');
  let depth = 0;
  let quote = null;
  let escaped = false;
  let inLineComment = false;
  let inBlockComment = false;
  for (let i = index; i < source.length; i++) {
    const ch = source[i];
    const next = source[i + 1];
    if (inLineComment) {
      if (ch === '\n') inLineComment = false;
      continue;
    }
    if (inBlockComment) {
      if (ch === '*' && next === '/') {
        inBlockComment = false;
        i++;
      }
      continue;
    }
    if (quote) {
      if (escaped) escaped = false;
      else if (ch === '\\') escaped = true;
      else if (ch === quote) quote = null;
      continue;
    }
    if (ch === '/' && next === '/') {
      inLineComment = true;
      i++;
      continue;
    }
    if (ch === '/' && next === '*') {
      inBlockComment = true;
      i++;
      continue;
    }
    if (ch === '"' || ch === "'" || ch === '`') {
      quote = ch;
      continue;
    }
    if (ch === '[') depth++;
    if (ch === ']') {
      depth--;
      if (depth === 0) return source.slice(index, i + 1);
    }
  }
  return null;
}

function evalArray(literal, label) {
  try {
    return vm.runInNewContext(`(${literal})`, {}, { timeout: 1000 });
  } catch (error) {
    fail(`Cannot parse ${label}: ${error.message}`);
  }
}

function evalScenes(literal) {
  const normalized = literal.replace(/\bfn\s*:\s*([A-Za-z_$][\w$]*)/g, 'fn:"$1"');
  return evalArray(normalized, 'SCENES');
}

function stripTags(html) {
  return String(html).replace(/<[^>]+>/g, '').replace(/&[a-z#0-9]+;/gi, '_').trim();
}

function main() {
  const file = process.argv[2];
  if (!file) fail('Usage: node validate_html.js path/to/video.html');
  const htmlPath = path.resolve(file);
  const html = read(htmlPath);
  const errors = [];
  const warnings = [];

  ['id="stage"', 'id="shot"', 'id="bg"', 'id="subtitle"', 'id="startBtn"'].forEach((needle) => {
    if (!html.includes(needle) && !html.includes(needle.replace(/"/g, "'"))) errors.push(`Missing ${needle}.`);
  });

  const script = mainScript(html);
  if (!script) {
    errors.push('Could not locate the main <script> block.');
  } else {
    try {
      new vm.Script(script);
    } catch (error) {
      errors.push(`Main <script> has a JavaScript syntax error: ${error.message}`);
    }
  }

  const audio = audioTagById(html, 'audio');
  if (!audio) errors.push('Missing <audio id="audio">.');
  if (/\bcontrols\b/i.test(audio)) errors.push('<audio id="audio"> must not include controls.');
  const audioSrc = getAttr(audio, 'src') || '';
  if (audio && !audioSrc) errors.push('<audio id="audio"> has no src.');
  if (/YOUR_VOICE/i.test(audioSrc)) errors.push('Audio src is still YOUR_VOICE placeholder.');
  if (/^data:audio\//i.test(audioSrc)) warnings.push('Audio is embedded as a data URI; prefer a relative audio file for reusable projects.');
  if (/^https?:\/\//i.test(audioSrc)) warnings.push('Audio src is remote; local relative audio is safer for recording.');

  if (/localStorage|sessionStorage/.test(html)) errors.push('Do not use localStorage/sessionStorage.');
  if (/\bid=["']controls["']|\bid=["']calPanel["']|\bid=["']calBtn["']/.test(html)) errors.push('Finished HTML must not contain visible authoring/debug controls.');
  if (/\bsetTimeout\s*\(|\bsetInterval\s*\(/.test(html)) errors.push('Do not use independent timers; derive timing from audio.currentTime.');
  if (/const\s+PRESET\b|scene-hook|scene-flip|scene-data|scene-wordcloud/.test(html)) errors.push('Old preset/fixed-scene system detected; use the dynamic web video scene functions instead.');

  const hasBgmRaw = constValue(html, 'HAS_BGM');
  const hasBgm = hasBgmRaw === 'true';
  if (hasBgm) {
    const bgm = audioTagById(html, 'bgm');
    if (!bgm) errors.push('HAS_BGM is true but <audio id="bgm"> is missing.');
    if (/YOUR_BGM/i.test(html)) errors.push('HAS_BGM is true but BGM source is still a placeholder.');
    if (/\bcontrols\b/i.test(bgm)) errors.push('<audio id="bgm"> must not include controls.');
  }

  const cueLiteral = extractArrayLiteral(html, 'CUES');
  const sceneLiteral = extractArrayLiteral(html, 'SCENES');
  const chapterLiteral = extractArrayLiteral(html, 'CHAPTER_NAMES');
  if (!cueLiteral) errors.push('Missing const CUES = [...] array.');
  if (!sceneLiteral) errors.push('Missing const SCENES = [...] array.');

  const cues = cueLiteral ? evalArray(cueLiteral, 'CUES') : [];
  const scenes = sceneLiteral ? evalScenes(sceneLiteral) : [];
  const chapters = chapterLiteral ? evalArray(chapterLiteral, 'CHAPTER_NAMES') : [];

  if (cues.length === 0) errors.push('CUES array is empty.');
  cues.forEach((cue, index) => {
    if (typeof cue.s !== 'number' || typeof cue.e !== 'number' || typeof cue.h !== 'string') {
      errors.push(`Cue ${index} must have numeric s/e and string h.`);
      return;
    }
    if (!(cue.s < cue.e)) errors.push(`Cue ${index} has s >= e.`);
    if (index > 0 && cue.s < cues[index - 1].e - 0.001) errors.push(`Cue ${index} overlaps previous cue.`);
    const open = (cue.h.match(/<span\b/gi) || []).length;
    const close = (cue.h.match(/<\/span>/gi) || []).length;
    if (open !== close) errors.push(`Cue ${index} has unbalanced span tags.`);
    const len = stripTags(cue.h).replace(/\s+/g, '').length;
    if (len > 32) warnings.push(`Cue ${index} is ${len} chars (>32); split it for subtitle readability.`);
  });

  if (scenes.length === 0) errors.push('SCENES array is empty.');
  if (scenes.length > 24) warnings.push(`${scenes.length} scenes is a lot for a screen-recorded web video.`);
  scenes.forEach((scene, index) => {
    if (typeof scene.t !== 'number') errors.push(`Scene ${index} is missing numeric t.`);
    if (!scene.fn) errors.push(`Scene ${index} is missing fn.`);
    if (scene.fn && !new RegExp(`function\\s+${scene.fn}\\b`).test(html)) errors.push(`Scene function ${scene.fn} is referenced but not defined.`);
    if (index > 0) {
      const gap = scene.t - scenes[index - 1].t;
      if (gap < 0) errors.push(`Scene ${index} starts before previous scene.`);
      else if (gap < 1.0) errors.push(`Scene ${index} starts only ${gap.toFixed(2)}s after previous scene.`);
      else if (gap < 1.8) warnings.push(`Scene ${index} starts ${gap.toFixed(2)}s after previous scene; very tight cut.`);
    }
  });
  if (chapters.length && scenes.length && chapters.length !== scenes.length) {
    errors.push(`CHAPTER_NAMES length (${chapters.length}) must equal SCENES length (${scenes.length}).`);
  }


  // 八股检测：scene 函数体内禁止写死长中文句（≥8字）
  if (script) {
    const sceneFnBodyPattern = /function\s+scene\w+\s*\([^)]*\)\s*\{([\s\S]*?)(?=\nfunction\s|\n<\/script>|$)/g;
    let fnMatch;
    while ((fnMatch = sceneFnBodyPattern.exec(script)) !== null) {
      const body = fnMatch[1];
      const longChinese = body.match(/['"`][^'"`\n]*[一-龥]{8,}[^'"`\n]*['"`]/g);
      if (longChinese) {
        const fnName = fnMatch[0].match(/function\s+(\w+)/)[1];
        warnings.push(`${fnName}: 函数体内含写死的长中文（≥8字），镜头可能绑死内容——应改为读 cue 字段。`);
      }
    }
  }

  if (/\[CHANGE ME\]|\[项目主标题\]|YOUR_VOICE|YOUR_BGM/.test(html)) {
    errors.push('Found unreplaced template placeholder.');
  }

  warnings.forEach((message) => console.log(`WARN: ${message}`));
  errors.forEach((message) => console.log(`ERROR: ${message}`));
  if (errors.length) process.exit(1);
  console.log(`OK: HTML validated: ${htmlPath}`);
  console.log(`Cues: ${cues.length}; Scenes: ${scenes.length}; BGM: ${hasBgm ? 'on' : 'off'}`);
}

main();
