#!/usr/bin/env node
/**
 * human-flow scanner v2 (AST-Powered)
 *
 * Sophisticated analysis pass for mouse + keyboard workflow friction.
 * Uses @babel/parser for deep JSX/TSX understanding.
 *
 * Usage:
 *   node scripts/scan.mjs --path src
 *   node scripts/scan.mjs --path src --fix
 */

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parse } from '@babel/parser';
import _traverse from '@babel/traverse';
import _generate from '@babel/generator';
import * as t from '@babel/types';
const traverse = _traverse.default;
const generate = _generate.default;

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const args = process.argv.slice(2);
let targetPath = 'src';
let format = 'text';
let mode = 'friction'; // 'friction' | 'fancy'
let applyFix = false;

for (let i = 0; i < args.length; i++) {
  if (args[i] === '--path' || args[i] === '-p') targetPath = args[++i];
  if (args[i] === '--format' || args[i] === '-f') format = args[++i];
  if (args[i] === '--fancy' || args[i] === '--mode=fancy') mode = 'fancy';
  if (args[i] === '--fix') applyFix = true;
}

const absTarget = path.resolve(process.cwd(), targetPath);

if (!fs.existsSync(absTarget)) {
  console.error(`Target not found: ${absTarget}`);
  process.exit(1);
}

// `--fix` auto-apply is DISABLED for now: @babel/generator reprints the whole
// AST, producing noisy diffs that touch untouched code. Until it does surgical
// edits, run advisory only — agents apply fixes surgically from the report.
if (applyFix) {
  console.error('[INFO] --fix (auto-apply) is disabled; running an advisory scan instead. Apply fixes surgically from the report.');
  applyFix = false;
}

const findings = [];
let fixesApplied = 0;

// Friction Index Rubric Weights
const WEIGHTS = {
  MOTOR: 3.0,
  COGNITIVE: 2.5,
  KEYBOARD: 2.5,
  FEEDBACK: 2.0
};

const SEVERITY_POINTS = {
  high: 1.0,
  medium: 0.5,
  low: 0.2
};

function walk(dir) {
  if (!fs.existsSync(dir)) return;
  const entries = fs.readdirSync(dir, { withFileTypes: true });
  for (const entry of entries) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      if (['node_modules', 'dist', 'build', '.git'].includes(entry.name)) continue;
      walk(full);
    } else if (/\.(tsx|jsx|ts|js)$/.test(entry.name)) {
      analyzeFile(full);
    }
  }
}

function analyzeFile(file) {
  const content = fs.readFileSync(file, 'utf8');
  const rel = path.relative(process.cwd(), file).replace(/\\/g, '/');
  let modified = false;
  
  try {
    const ast = parse(content, {
      sourceType: 'module',
      plugins: ['jsx', 'typescript', 'decorators-legacy', 'classProperties'],
      errorRecovery: true
    });

    if (mode === 'fancy') {
      // Fancy / beauty & elegance pass
      const hasMotion = /transition:|animate-|@keyframes|framer-motion|ViewTransition/i.test(content);
      if (hasMotion) {
        addFinding({
          file: rel,
          line: 1,
          category: 'FEEDBACK',
          severity: 'low',
          pattern: 'existing-motion',
          message: 'Existing motion detected. Review for quality, easing, and restraint.',
          humanImpact: 'Motion can feel premium or cheap depending on execution.',
          suggestion: 'Check if easings match the Restraint-o-Meter Level 3-4 (150-250ms).'
        });
      }
      
      // Missing View Transitions in SPA contexts
      if (/(useNavigate|navigate\(|<Link|router\.push)/i.test(content) && !/document\.startViewTransition/i.test(content)) {
        addFinding({
          file: rel,
          line: 1,
          category: 'FEEDBACK',
          severity: 'low',
          pattern: 'missing-view-transitions',
          message: 'Navigation detected without View Transitions API.',
          humanImpact: 'View changes feel abrupt. Transitions feel significantly more premium.',
          suggestion: 'Wrap navigation in document.startViewTransition() where appropriate.'
        });
      }
      return;
    }

    traverse(ast, {
      JSXOpeningElement(path) {
        const node = path.node;
        const name = getComponentName(node);

        // 1. Unlabeled Icon Button (with Fixer)
        if (isButtonLike(node) && !hasAriaLabel(node)) {
          const parent = path.parentPath.node;
          if (parent.children && hasOnlyIconChild(parent.children)) {
            if (applyFix) {
              const iconNode = parent.children.find(c => c.type === 'JSXElement');
              const iconName = getComponentName(iconNode.openingElement);
              const label = iconName.replace(/Icon$/, '');
              node.attributes.push(t.jsxAttribute(t.jsxIdentifier('aria-label'), t.stringLiteral(label)));
              modified = true;
              fixesApplied++;
            } else {
              addFinding({
                file: rel,
                line: node.loc.start.line,
                category: 'COGNITIVE',
                severity: 'high',
                pattern: 'unlabeled-icon-button',
                message: `Button "${name}" contains only an icon but has no aria-label or title.`,
                humanImpact: 'Keyboard and screen reader users have no way to know what this button does.',
                suggestion: 'Add an aria-label or title prop describing the action.'
              });
            }
          }
        }

        // 2. Tiny Target Calculator
        if (isInteractive(node)) {
          const size = getTargetSize(node);
          if (size < 32) {
            addFinding({
              file: rel,
              line: node.loc.start.line,
              category: 'MOTOR',
              severity: 'high',
              pattern: 'tiny-target',
              message: `Interactive element "${name}" has a detected size of ~${size}px.`,
              humanImpact: 'Small targets require high precision, leading to slower workflows and mis-clicks.',
              suggestion: 'Increase height/width to at least 32px (ideally 44px) or add generous padding.'
            });
          }
        }

        // 3. Interaction Feedback Missing
        if (name === 'Button' || name === 'ActionButton') {
          if (!hasFeedbackProps(node)) {
            addFinding({
              file: rel,
              line: node.loc.start.line,
              category: 'FEEDBACK',
              severity: 'medium',
              pattern: 'missing-feedback-props',
              message: `Button "${name}" lacks loading or active state props.`,
              humanImpact: 'Users may be unsure if their click was registered during long operations.',
              suggestion: 'Add isLoading or active props to provide immediate visual feedback.'
            });
          }
        }

        // 4. Keyboard Parity: onClick without key handler
        if (hasProp(node, 'onClick') && !isNativeButton(node) && !hasKeyboardProps(node)) {
          addFinding({
            file: rel,
            line: node.loc.start.line,
            category: 'KEYBOARD',
            severity: 'high',
            pattern: 'click-without-keyboard',
            message: `Custom element "${name}" has onClick but no keyboard handlers (onKeyDown) or tabIndex.`,
            humanImpact: 'Keyboard users cannot trigger this action, creating a complete blocker for some workflows.',
            suggestion: 'Add tabIndex={0} and an onKeyDown handler for Enter/Space.'
          });
        }
      }
    });

    if (modified && applyFix) {
      const output = generate(ast, { retainLines: true }, content);
      fs.writeFileSync(file, output.code);
    }
  } catch (e) {
    // Graceful degradation: Fallback to regex for critical failures
    runLegacyRegexScan(content, rel);
  }
}

function addFinding(f) {
  findings.push(f);
}

// Helpers
function getComponentName(node) {
  if (node.name.type === 'JSXIdentifier') return node.name.name;
  if (node.name.type === 'JSXMemberExpression') return node.name.property.name;
  return 'unknown';
}

function isButtonLike(node) {
  const name = getComponentName(node);
  return ['button', 'Button', 'IconButton', 'ActionButton'].includes(name) || hasProp(node, 'role', 'button');
}

function isNativeButton(node) {
  return getComponentName(node) === 'button';
}

function isInteractive(node) {
  const name = getComponentName(node);
  return isButtonLike(node) || ['a', 'input', 'select', 'textarea'].includes(name) || hasProp(node, 'onClick');
}

function hasProp(node, propName, value) {
  return node.attributes.some(attr => {
    if (attr.type !== 'JSXAttribute') return false;
    if (attr.name.name !== propName) return false;
    if (value === undefined) return true;
    if (attr.value && attr.value.type === 'StringLiteral') return attr.value.value === value;
    return false;
  });
}

function hasAriaLabel(node) {
  return hasProp(node, 'aria-label') || hasProp(node, 'title') || hasProp(node, 'label');
}

function hasOnlyIconChild(children) {
  const visibleChildren = children.filter(c => c.type !== 'JSXText' || c.value.trim() !== '');
  if (visibleChildren.length !== 1) return false;
  const child = visibleChildren[0];
  if (child.type !== 'JSXElement') return false;
  const name = getComponentName(child.openingElement);
  return name.endsWith('Icon') || name === 'Icon';
}

function getTargetSize(node) {
  let size = 44; // Default
  node.attributes.forEach(attr => {
    if (attr.type === 'JSXAttribute' && attr.name.name === 'size') {
      if (attr.value.value === 'sm' || attr.value.value === 'xs') size = 28;
    }
    if (attr.type === 'JSXAttribute' && attr.name.name === 'className') {
      const val = attr.value.value || '';
      if (val.includes('btn--sm') || val.includes('h-6') || val.includes('h-4')) size = 24;
    }
  });
  return size;
}

function hasFeedbackProps(node) {
  return hasProp(node, 'loading') || hasProp(node, 'isLoading') || hasProp(node, 'active');
}

function hasKeyboardProps(node) {
  return hasProp(node, 'onKeyDown') || hasProp(node, 'onKeyPress') || hasProp(node, 'tabIndex');
}

function runLegacyRegexScan(content, rel) {
  // Simple fallback for files that fail AST parsing
  if (/onClick=\{[^}]+}\s*(?!.*(onKeyDown|tabIndex|role=))/g.test(content)) {
    addFinding({
      file: rel,
      line: 1,
      category: 'KEYBOARD',
      severity: 'high',
      pattern: 'regex-click-without-keyboard',
      message: 'Detected onClick without keyboard support via fallback scanner.',
      humanImpact: 'Potential keyboard blocker.',
      suggestion: 'Manually review for keyboard parity.'
    });
  }
}

// Start Scan
walk(absTarget);

if (applyFix) {
  console.log(`\nFixed ${fixesApplied} mechanical issues across the target.`);
} else {
  // Calculate Score
  const scoreDeductions = findings.reduce((acc, f) => {
    const dim = f.category;
    const points = SEVERITY_POINTS[f.severity] * WEIGHTS[dim];
    acc[dim] = (acc[dim] || 0) + points;
    return acc;
  }, {});

  const totalDeduction = Object.values(scoreDeductions).reduce((a, b) => a + b, 0);
  const finalScore = Math.max(0, Math.min(10, 10 - totalDeduction)).toFixed(1);

  if (format === 'json') {
    console.log(JSON.stringify({ target: absTarget, score: finalScore, findings }, null, 2));
  } else {
    console.log(`## Human-Flow Scan: ${targetPath}`);
    console.log(`**Overall Human Workflow Score: ${finalScore}/10**\n`);

    if (findings.length === 0) {
      console.log('[OK] No friction detected. Workflow is clean.');
    } else {
      findings.forEach((f, i) => {
        console.log(`${i + 1}. [${f.severity.toUpperCase()}] ${f.category} — ${f.pattern}`);
        console.log(`   File: ${f.file}:${f.line}`);
        console.log(`   ${f.message}`);
        console.log(`   Human impact: ${f.humanImpact}`);
        console.log(`   Suggestion: ${f.suggestion}\n`);
      });
    }
  }
}
