#!/usr/bin/env node
/**
 * Uses TypeScript AST to find <input>/<select>/<textarea> JSX elements
 * missing aria-label/aria-labelledby and adds aria-label.
 * Idempotent and structurally safe.
 */
const fs = require('fs');
const path = require('path');
const ts = require('typescript');

function titleCase(s) {
  return s
    .replace(/[-_]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
    .split(' ')
    .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : ''))
    .join(' ');
}

function getJsxAttr(name, attrs) {
  for (const a of attrs.properties) {
    if (a.kind === ts.SyntaxKind.JsxAttribute && a.name && a.name.text === name) return a;
  }
  return null;
}

function getStringAttrValue(name, attrs) {
  const a = getJsxAttr(name, attrs);
  if (!a || !a.initializer) return undefined;
  if (a.initializer.kind === ts.SyntaxKind.StringLiteral) return a.initializer.text;
  if (a.initializer.kind === ts.SyntaxKind.JsxExpressionContainer && a.initializer.expression) {
    const e = a.initializer.expression;
    if (e.kind === ts.SyntaxKind.StringLiteral) return e.text;
  }
  return undefined;
}

function hasBooleanAttr(name, attrs) {
  return !!getJsxAttr(name, attrs);
}

function getNameText(name) {
  if (!name) return null;
  if (name.kind === ts.SyntaxKind.Identifier) return name.text;
  if (name.kind === ts.SyntaxKind.PropertyAccessExpression) return name.name.text;
  return null;
}

function inferLabelFromSiblingsOrPreceding(sourceFile, node) {
  const scanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ true, ts.LanguageVariant.JSX, sourceFile.text);
  // Walk text before the node to find preceding label or {label(...)}
  const start = node.getStart(sourceFile);
  const before = sourceFile.text.slice(Math.max(0, start - 800), start);
  // Look for label("...") in the most recent expression
  const labelCallMatch = before.match(/label\(\s*['"`]([^'"`]+)['"`]\s*\)/);
  if (labelCallMatch) return labelCallMatch[1];
  // Look for a label JSX element without closing
  const lastLabelOpen = [...before.matchAll(/<label\b[^>]*>(?:(?!<\/label>)[\s\S])*$/gi)];
  if (lastLabelOpen.length) {
    const last = lastLabelOpen[lastLabelOpen.length - 1];
    const afterOpen = before.slice(last.index + last[0].match(/^<label\b[^>]*>/i)[0].length);
    // If there's a </label> after, this label isn't enclosing
    if (!/<\/label>/i.test(afterOpen)) {
      const inner = afterOpen.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
      if (inner) return inner.replace(/[*?]\s*$/, '').trim();
    }
  }
  return null;
}

function isInsideLabel(sourceFile, node) {
  let parent = node.parent;
  while (parent) {
    if (parent.kind === ts.SyntaxKind.JsxElement) {
      const opening = parent.openingElement;
      const tagName = getNameText(opening.tagName);
      if (tagName === 'label') return true;
      // Also check if it's inside a JsxFragment whose parent is a label
    }
    parent = parent.parent;
  }
  // Also check if we're directly inside a label's children
  let cur = node;
  while (cur.parent) {
    if (cur.parent.kind === ts.SyntaxKind.JsxElement && getNameText(cur.parent.openingElement.tagName) === 'label') {
      return true;
    }
    cur = cur.parent;
  }
  return false;
}

function inferLabel(node, sourceFile) {
  const attrs = node.attributes;
  // Type attribute
  const typeAttr = getStringAttrValue('type', attrs);
  const placeholder = getStringAttrValue('placeholder', attrs);
  const nameAttr = getStringAttrValue('name', attrs);
  const idAttr = getStringAttrValue('id', attrs);
  const className = getStringAttrValue('className', attrs);
  const tagName = getNameText(node.tagName);
  if (className && /\bhidden\b/.test(className) && typeAttr === 'file') return 'File upload';
  if (placeholder) {
    const cleaned = placeholder.replace(/\b(e\.g\.?|i\.e\.?|min\.?)\b/gi, '').trim();
    return titleCase(cleaned || placeholder);
  }
  if (nameAttr) return titleCase(nameAttr.replace(/\[.*?\]/g, ''));
  if (idAttr) return titleCase(idAttr);
  const ctx = inferLabelFromSiblingsOrPreceding(sourceFile, node);
  if (ctx) return ctx;
  if (tagName === 'input' && typeAttr) return titleCase(typeAttr);
  return titleCase(tagName);
}

function findTargets(sourceFile) {
  const out = [];
  function visit(node) {
    if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
      const tagName = getNameText(node.tagName);
      if (['input', 'select', 'textarea'].includes(tagName)) {
        const attrs = node.attributes;
        const hasAriaLabel = hasBooleanAttr('aria-label', attrs) || getStringAttrValue('aria-label', attrs) !== undefined;
        const hasAriaLabelledBy = hasBooleanAttr('aria-labelledby', attrs) || getStringAttrValue('aria-labelledby', attrs) !== undefined;
        const typeAttr = getStringAttrValue('type', attrs);
        if (!hasAriaLabel && !hasAriaLabelledBy && typeAttr !== 'hidden') {
          if (!isInsideLabel(sourceFile, node)) {
            out.push(node);
          }
        }
      }
    }
    ts.forEachChild(node, visit);
  }
  visit(sourceFile);
  return out;
}

function escapeForJsx(s) {
  return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
}

function processFile(filePath) {
  const text = fs.readFileSync(filePath, 'utf8');
  const sourceFile = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.LanguageVariant.JSX);
  const targets = findTargets(sourceFile);
  if (targets.length === 0) return 0;

  // Sort by position descending so insertions don't shift earlier offsets
  targets.sort((a, b) => b.getStart(sourceFile) - a.getStart(sourceFile));

  let updated = text;
  let changes = 0;
  for (const node of targets) {
    const inferred = inferLabel(node, sourceFile);
    if (!inferred) continue;
    // Find position: right after the tag name
    const tagNameEnd = node.tagName.getEnd();
    const ariaStr = ` aria-label="${escapeForJsx(inferred)}"`;
    // Reconstruct from the position
    const head = updated.slice(0, tagNameEnd);
    const rest = updated.slice(tagNameEnd);
    updated = head + ariaStr + rest;
    changes++;
  }
  if (changes > 0) {
    fs.writeFileSync(filePath, updated);
  }
  return changes;
}

const ROOT = path.resolve(__dirname, '..');
const TARGET_DIRS = ['src/app', 'src/components', 'src/wholesale', 'src/landing'];
const SKIP_PATTERNS = [
  /node_modules/,
  /\.next\//,
  /dist\//,
  /coverage\//,
  /\.test\.[jt]sx?$/,
  /\.spec\.[jt]sx?$/,
];

function walk(dir, files = []) {
  if (!fs.existsSync(dir)) return files;
  for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
    const p = path.join(dir, ent.name);
    if (SKIP_PATTERNS.some((rx) => rx.test(p))) continue;
    if (ent.isDirectory()) walk(p, files);
    else if (/\.(tsx|jsx)$/.test(ent.name)) files.push(p);
  }
  return files;
}

let totalChanges = 0;
let totalFiles = 0;
const changedFiles = [];
const errors = [];
for (const t of TARGET_DIRS) {
  const dir = path.join(ROOT, t);
  if (!fs.existsSync(dir)) continue;
  const files = walk(dir);
  for (const f of files) {
    try {
      const c = processFile(f);
      if (c > 0) {
        changedFiles.push([c, path.relative(ROOT, f)]);
        totalFiles++;
        totalChanges += c;
      }
    } catch (e) {
      errors.push(`${f}: ${e.message}`);
    }
  }
}
changedFiles.sort((a, b) => b[0] - a[0]);
for (const [c, f] of changedFiles) {
  console.log(`  ${c.toString().padStart(4)}  ${f}`);
}
if (errors.length) {
  console.error('\nErrors:');
  errors.forEach((e) => console.error(' ', e));
}
console.log(`\nTotal: ${totalChanges} aria-labels added across ${totalFiles} files`);