'use strict';

const fs = require('node:fs');
const path = require('node:path');

const FRONTEND_FILE_RE = /\.(html?|css|scss|sass|less|jsx|tsx|vue|svelte|astro)\b/i;
const HTML_FILE_RE = /\.html?\b/i;
const JS_FILE_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx)\b/i;
const ASSET_ATTR_RE = /\b(?:src|href|poster)\s*=\s*["']([^"']+)["']/gi;
const SRCSET_RE = /\bsrcset\s*=\s*["']([^"']+)["']/gi;
const CSS_URL_RE = /url\(\s*(['"]?)([^'")]+)\1\s*\)/gi;
const EVENT_HANDLER_ATTR_RE = /\bon([a-z][a-z0-9_-]*)\s*=\s*(["'])([\s\S]*?)\2/gi;
const SCRIPT_SRC_RE = /<script\b[^>]*\bsrc\s*=\s*(["'])([^"']+)\1[^>]*>/gi;
const INLINE_SCRIPT_RE = /<script\b(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi;
const JS_CALL_RE = /(?:^|[^\w$.\]])(?:window\.|globalThis\.)?([A-Za-z_$][\w$]*)\s*\(/g;
const JS_KEYWORDS = new Set([
  'if',
  'for',
  'while',
  'switch',
  'catch',
  'function',
  'return',
  'typeof',
  'void',
  'new',
  'do',
  'else',
  'class',
  'super',
  'import',
  'export',
]);

function isFrontendFile(filePath = '') {
  return FRONTEND_FILE_RE.test(String(filePath || ''));
}

function isHtmlFile(filePath = '') {
  return HTML_FILE_RE.test(String(filePath || ''));
}

function isScriptFile(filePath = '') {
  return JS_FILE_RE.test(String(filePath || ''));
}

function shouldSkipReference(ref = '') {
  const value = String(ref || '').trim();
  if (!value) return true;
  return value.startsWith('#')
    || /^(?:https?:|data:|mailto:|tel:|javascript:|blob:|about:)/i.test(value);
}

function cleanReference(ref = '') {
  const value = String(ref || '').trim();
  const hashIdx = value.indexOf('#');
  const queryIdx = value.indexOf('?');
  const cut = [hashIdx, queryIdx].filter((n) => n >= 0).sort((a, b) => a - b)[0];
  return cut >= 0 ? value.slice(0, cut) : value;
}

function srcsetReferences(value = '') {
  return String(value || '')
    .split(',')
    .map((entry) => entry.trim().split(/\s+/)[0])
    .filter(Boolean);
}

function extractLocalAssetReferences(content = '') {
  const refs = [];
  let match;
  while ((match = ASSET_ATTR_RE.exec(content))) refs.push(match[1]);
  while ((match = CSS_URL_RE.exec(content))) refs.push(match[2]);
  while ((match = SRCSET_RE.exec(content))) refs.push(...srcsetReferences(match[1]));
  return refs
    .map(cleanReference)
    .filter((ref) => !shouldSkipReference(ref));
}

function resolveReference(cwd, sourceFile, ref) {
  if (/^file:\/\//i.test(ref)) {
    try {
      return new URL(ref).pathname;
    } catch {
      return ref;
    }
  }
  if (ref.startsWith('/')) return path.resolve(cwd, `.${ref}`);
  return path.resolve(path.dirname(sourceFile), ref);
}

function extractInlineHandlerCalls(content = '') {
  const calls = [];
  let match;
  while ((match = EVENT_HANDLER_ATTR_RE.exec(content))) {
    const event = match[1];
    const expression = match[3] || '';
    let callMatch;
    while ((callMatch = JS_CALL_RE.exec(expression))) {
      const name = callMatch[1];
      if (!name || JS_KEYWORDS.has(name)) continue;
      calls.push({ event, expression, name });
    }
  }
  return calls;
}

function extractScriptSources(content = '') {
  const refs = [];
  let match;
  while ((match = SCRIPT_SRC_RE.exec(content))) refs.push(match[2]);
  return refs
    .map(cleanReference)
    .filter((ref) => !shouldSkipReference(ref));
}

function extractInlineScripts(content = '') {
  const scripts = [];
  let match;
  while ((match = INLINE_SCRIPT_RE.exec(content))) scripts.push(match[1] || '');
  return scripts;
}

function extractDeclaredGlobalNames(content = '') {
  const names = new Set();
  const patterns = [
    /\bfunction\s+([A-Za-z_$][\w$]*)\s*\(/g,
    /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:function\b|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/g,
    /\b(?:window|globalThis)\.([A-Za-z_$][\w$]*)\s*=/g,
  ];
  for (const pattern of patterns) {
    let match;
    while ((match = pattern.exec(content))) names.add(match[1]);
  }
  return names;
}

function checkInlineEventHandlers(cwd, files = []) {
  const missing = [];
  const checked = [];
  const htmlFiles = [...new Set(files || [])]
    .filter(isHtmlFile)
    .map((file) => path.isAbsolute(file) ? file : path.join(cwd, file));

  for (const sourceFile of htmlFiles) {
    if (!fs.existsSync(sourceFile) || !fs.statSync(sourceFile).isFile()) continue;
    const html = fs.readFileSync(sourceFile, 'utf8');
    const handlers = extractInlineHandlerCalls(html);
    if (!handlers.length) continue;

    const declared = new Set();
    for (const script of extractInlineScripts(html)) {
      for (const name of extractDeclaredGlobalNames(script)) declared.add(name);
    }
    for (const ref of extractScriptSources(html)) {
      const resolved = resolveReference(cwd, sourceFile, ref);
      if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) continue;
      const js = fs.readFileSync(resolved, 'utf8');
      for (const name of extractDeclaredGlobalNames(js)) declared.add(name);
    }

    for (const handler of handlers) {
      const item = {
        source: path.relative(cwd, sourceFile),
        event: handler.event,
        name: handler.name,
        expression: handler.expression,
      };
      checked.push(item);
      if (!declared.has(handler.name)) missing.push(item);
    }
  }

  return {
    ok: missing.length === 0,
    checked,
    missing,
    htmlFiles: htmlFiles.map((file) => path.relative(cwd, file)),
  };
}

function checkLocalAssetReferences(cwd, files = []) {
  const missing = [];
  const checked = [];
  const frontendFiles = [...new Set(files || [])]
    .filter(isFrontendFile)
    .map((file) => path.isAbsolute(file) ? file : path.join(cwd, file));

  for (const sourceFile of frontendFiles) {
    if (!fs.existsSync(sourceFile) || !fs.statSync(sourceFile).isFile()) continue;
    const content = fs.readFileSync(sourceFile, 'utf8');
    for (const ref of extractLocalAssetReferences(content)) {
      const resolved = resolveReference(cwd, sourceFile, ref);
      checked.push({ source: path.relative(cwd, sourceFile), reference: ref, resolved });
      if (!fs.existsSync(resolved)) {
        missing.push({ source: path.relative(cwd, sourceFile), reference: ref, resolved });
      }
    }
  }

  return {
    ok: missing.length === 0,
    checked,
    missing,
    frontendFiles: frontendFiles.map((file) => path.relative(cwd, file)),
  };
}

function resolveFrontendEntrypoints(cwd, files = [], { limit = 4 } = {}) {
  const entrypoints = [];
  const seen = new Set();
  const add = (file) => {
    if (!file) return;
    const abs = path.isAbsolute(file) ? file : path.join(cwd, file);
    if (!isHtmlFile(abs)) return;
    if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return;
    const real = path.resolve(abs);
    if (seen.has(real)) return;
    seen.add(real);
    entrypoints.push(real);
  };

  for (const file of files || []) add(file);
  for (const file of files || []) {
    const abs = path.isAbsolute(file) ? file : path.join(cwd, file);
    if (!isFrontendFile(abs) && !isScriptFile(abs)) continue;
    add(path.join(path.dirname(abs), 'index.html'));
  }
  add(path.join(cwd, 'index.html'));
  return entrypoints.slice(0, limit);
}

function checkFrontendStaticContracts(cwd, files = []) {
  const assets = checkLocalAssetReferences(cwd, files);
  const handlers = checkInlineEventHandlers(cwd, files);
  return {
    ok: assets.ok && handlers.ok,
    assets,
    handlers,
    concerns: [
      ...assets.missing.map((item) => `[frontend-assets] ${item.source} references missing local asset ${item.reference}`),
      ...handlers.missing.map((item) => `[frontend-js] ${item.source} ${item.event}= handler calls missing ${item.name}()`),
    ],
  };
}

module.exports = {
  isFrontendFile,
  isHtmlFile,
  isScriptFile,
  extractLocalAssetReferences,
  extractInlineHandlerCalls,
  extractDeclaredGlobalNames,
  checkInlineEventHandlers,
  checkLocalAssetReferences,
  checkFrontendStaticContracts,
  resolveFrontendEntrypoints,
};
