/**
 * Contamination scanner & autofix.
 *
 * Detects project-specific tokens (hardcoded paths, brand identity, opinionated
 * stack choices) that should NOT enter the upstream BALDART framework when a
 * consumer pushes improvements. Patterns are the same ones used to clean the
 * v3.0.0 refactor.
 *
 * Three severities:
 *
 *   - 'autofixable'      — safe textual substitution (hardcoded paths).
 *                          autofix() applies these; UI confirms once.
 *   - 'requires-decision' — semantic tokens (brand identity, stack opinions).
 *                          UI lists each line, user decides whether to (a) move
 *                          to overlay, (b) keep as-is and force push, or (c) abort.
 *   - 'block'             — secrets / credentials patterns; never auto-cleared.
 *
 * Public API:
 *
 *   scan(content, opts?) → { findings: [{rule, line, lineNum, match, severity, suggestion?}] }
 *   autofix(content)     → { content: <new>, applied: [{rule, count}] }
 *   explain(finding)     → string (one-line human readable)
 *   isClean(findings)    → boolean
 *   summarise(findings)  → { autofixable: N, requiresDecision: N, blocking: N }
 */

// -----------------------------------------------------------------------
// Rules
// -----------------------------------------------------------------------

/**
 * Each rule:
 *   id        — short slug
 *   severity  — 'autofixable' | 'requires-decision' | 'block'
 *   pattern   — RegExp (must be /g for autofix to work)
 *   replace   — function(match) → string  (only meaningful for autofixable)
 *   label     — short human label
 *   advice    — one-line guidance for the UI
 */
const RULES = [
  // ---- Autofixable: hardcoded canonical paths ----
  {
    id: 'path-design-system',
    severity: 'autofixable',
    pattern: /\bdocs\/design-system\//g,
    replace: () => '${paths.design_system}/',
    label: 'docs/design-system/',
    advice: 'replace with `${paths.design_system}/`',
  },
  {
    id: 'path-references-api',
    severity: 'autofixable',
    pattern: /\bdocs\/references\/api\//g,
    replace: () => '${paths.references_dir}/api/',
    label: 'docs/references/api/',
    advice: 'replace with `${paths.references_dir}/api/`',
  },
  {
    id: 'path-references-ui',
    severity: 'autofixable',
    pattern: /\bdocs\/references\/ui\//g,
    replace: () => '${paths.references_dir}/ui/',
    label: 'docs/references/ui/',
    advice: 'replace with `${paths.references_dir}/ui/`',
  },
  {
    id: 'path-references-other',
    severity: 'autofixable',
    // Catches docs/references/<anything> but NOT the api/, ui/ branches
    // already covered, and NOT the bare `docs/references` directory token.
    pattern: /\bdocs\/references\/(?!api\/|ui\/)([a-zA-Z0-9_\-\/.]+)/g,
    replace: (_m, rest) => `\${paths.references_dir}/${rest}`,
    label: 'docs/references/<x>',
    advice: 'replace with `${paths.references_dir}/<x>`',
  },
  {
    id: 'path-prd',
    severity: 'autofixable',
    pattern: /\bdocs\/prd\//g,
    replace: () => '${paths.prd_dir}/',
    label: 'docs/prd/',
    advice: 'replace with `${paths.prd_dir}/`',
  },
  {
    id: 'path-decisions',
    severity: 'autofixable',
    pattern: /\bdocs\/decisions\//g,
    replace: () => '${paths.adrs_dir}/',
    label: 'docs/decisions/',
    advice: 'replace with `${paths.adrs_dir}/`',
  },
  {
    id: 'path-wiki',
    severity: 'autofixable',
    pattern: /\bdocs\/wiki\//g,
    replace: () => '${paths.wiki_dir}/',
    label: 'docs/wiki/',
    advice: 'replace with `${paths.wiki_dir}/`',
  },
  {
    id: 'path-backlog',
    severity: 'autofixable',
    // Match `backlog/` or `/backlog/` when used as a path token. Don't match
    // the word `backlog` inside prose. We require either: it's preceded by a
    // path-like separator (slash, space, quote, paren, ``), AND followed by
    // something path-like (slash, *, alphanumeric, dot).
    pattern: /(^|[\s"'`(\[])\/?backlog\/(?=[a-zA-Z*.\/]|$|\s|["'`)\]])/g,
    replace: (_m, lead) => `${lead}\${paths.backlog_dir}/`,
    label: 'backlog/',
    advice: 'replace with `${paths.backlog_dir}/`',
  },
  {
    id: 'path-components-primitives',
    severity: 'autofixable',
    pattern: /\bsrc\/components\/ui\//g,
    replace: () => '${paths.components_primitives}/',
    label: 'src/components/ui/',
    advice: 'replace with `${paths.components_primitives}/`',
  },
  {
    id: 'path-components-root',
    severity: 'autofixable',
    pattern: /\bsrc\/components\/(?!ui\/)/g,
    replace: () => '${paths.components_root}/',
    label: 'src/components/',
    advice: 'replace with `${paths.components_root}/`',
  },
  {
    id: 'path-global-styles',
    severity: 'autofixable',
    pattern: /\bsrc\/app\/globals\.css\b/g,
    replace: () => '${paths.global_styles}',
    label: 'src/app/globals.css',
    advice: 'replace with `${paths.global_styles}`',
  },

  // ---- Requires decision: identity / brand tokens ----
  {
    id: 'identity-neo-brutalism',
    severity: 'requires-decision',
    pattern: /\bNeo-?Brutalism\b/g,
    label: 'Neo-Brutalism',
    advice: 'move to .baldart/overlays/<skill>.md or reference identity.design_philosophy',
  },
  {
    id: 'identity-italian-first',
    severity: 'requires-decision',
    pattern: /\bItalian-first\b/gi,
    label: 'Italian-first',
    advice: 'replace with reference to identity.language',
  },
  {
    id: 'identity-merchant',
    severity: 'requires-decision',
    pattern: /\b(?:merchant|merchants)\b/gi,
    label: 'merchant',
    advice: 'audience segment — move to overlay or reference identity.audience_segments',
  },
  {
    id: 'identity-customer',
    severity: 'requires-decision',
    pattern: /\b(?:customer|customers)\b/gi,
    label: 'customer',
    advice: 'audience segment — move to overlay or reference identity.audience_segments',
  },
  {
    id: 'identity-fidelity',
    severity: 'requires-decision',
    pattern: /\bfidelity\b/gi,
    label: 'fidelity',
    advice: 'project name leak — generalise or move to overlay',
  },
  {
    id: 'identity-brutal-wrapper',
    severity: 'requires-decision',
    pattern: /\bBrutal(?:Line|Bar|Donut|Heatmap|Area|Pie|Scatter)Chart\b/g,
    label: 'Brutal*Chart',
    advice: 'project chart wrapper — move to overlay or reference stack.charting.wrappers_root',
  },

  // ---- Requires decision: stack opinions ----
  {
    id: 'stack-recharts',
    severity: 'requires-decision',
    pattern: /\brecharts\b/gi,
    label: 'recharts',
    advice: 'opinionated charting choice — should live in stack.charting.canonical or overlay',
  },
  {
    id: 'stack-nivo',
    severity: 'requires-decision',
    pattern: /@nivo\/[a-z-]+/g,
    label: '@nivo/*',
    advice: 'opinionated charting choice — should live in stack.charting.canonical or overlay',
  },
  {
    id: 'stack-forbidden-chart-libs',
    severity: 'requires-decision',
    pattern: /\b(?:chart\.js|echarts|victory|visx|observable-plot|tremor)\b/g,
    label: '<chart-lib>',
    advice: 'forbidden-list entry — should live in stack.charting.forbidden or overlay',
  },
  {
    id: 'stack-framer-motion',
    severity: 'requires-decision',
    pattern: /\bframer-motion\b/g,
    label: 'framer-motion',
    advice: 'opinionated animation choice — should live in stack.animation.canonical',
  },

  // ---- Block: secrets / credentials ----
  // Conservative: we flag clearly-secret-like tokens. False positives are
  // acceptable; false negatives are not.
  {
    id: 'secret-api-key',
    severity: 'block',
    // Quoted or unquoted — covers both `apiKey: "abc..."` (JSON/JS) and
    // `API_KEY=abc...` (.env / shell). Conservative on length to avoid
    // false positives on short config flags.
    pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"]?[A-Za-z0-9_\-]{20,}['"]?/gi,
    label: 'API key literal',
    advice: 'looks like a hardcoded API key — remove before pushing',
  },
  {
    id: 'secret-github-pat',
    severity: 'block',
    pattern: /\bgh[posu]_[A-Za-z0-9]{30,}\b/g,
    label: 'GitHub PAT',
    advice: 'GitHub personal access token pattern — remove before pushing',
  },
  {
    id: 'secret-slack-token',
    severity: 'block',
    pattern: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g,
    label: 'Slack token',
    advice: 'Slack token pattern — remove before pushing',
  },
  {
    id: 'secret-jwt',
    severity: 'block',
    pattern: /\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g,
    label: 'JWT',
    advice: 'JSON Web Token pattern — remove before pushing',
  },
  {
    id: 'secret-bearer',
    severity: 'block',
    pattern: /bearer\s+[A-Za-z0-9._\-]{20,}/gi,
    label: 'Bearer token',
    advice: 'looks like a hardcoded bearer token — remove before pushing',
  },
  {
    id: 'secret-aws',
    severity: 'block',
    pattern: /AKIA[0-9A-Z]{16}/g,
    label: 'AWS access key id',
    advice: 'AWS access key pattern — remove before pushing',
  },
  {
    id: 'secret-private-key',
    severity: 'block',
    pattern: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g,
    label: 'PEM private key header',
    advice: 'private key block — remove before pushing',
  },
];

// -----------------------------------------------------------------------
// Opt-out marker
// -----------------------------------------------------------------------

/**
 * Some files legitimately quote project-default paths or identity tokens as
 * *examples* (e.g. autodetection probe descriptions, migration guides,
 * starter overlays). Autofixing them would destroy their pedagogical value.
 *
 * Such files declare opt-out with either:
 *   - an HTML comment `<!-- contamination-scan: skip -->` anywhere in the
 *     first 20 lines, OR
 *   - a YAML frontmatter key `contamination_scan: skip`.
 *
 * The scan / autofix entry points consult this first and short-circuit.
 */
function isOptedOut(content) {
  if (typeof content !== 'string') return false;
  const head = content.split('\n', 20).join('\n');
  if (/<!--\s*contamination-scan:\s*skip[\s\S]*?-->/i.test(head)) return true;
  if (/^contamination_scan:\s*skip\b/im.test(head)) return true;
  return false;
}

// -----------------------------------------------------------------------
// Public API
// -----------------------------------------------------------------------

/**
 * Scan a single file's text content. Returns findings grouped by rule.
 * `opts.skipRules` is an optional array of rule ids to ignore (used when
 * the user has already confirmed those tokens should pass).
 *
 * If the file declares the opt-out marker (see isOptedOut), returns an
 * empty findings list — the file is explicitly exempt.
 */
function scan(content, opts = {}) {
  if (typeof content === 'string' && isOptedOut(content)) {
    return { findings: [], optedOut: true };
  }
  if (typeof content !== 'string') {
    throw new TypeError('scan(content): expected string');
  }
  const skip = new Set(opts.skipRules || []);
  const lines = content.split('\n');
  const findings = [];

  for (const rule of RULES) {
    if (skip.has(rule.id)) continue;
    // Reset lastIndex defensively — RULES patterns are /g.
    rule.pattern.lastIndex = 0;
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      // Use String.prototype.matchAll to get all hits per line.
      const local = new RegExp(rule.pattern.source, rule.pattern.flags);
      const hits = line.matchAll(local);
      for (const m of hits) {
        findings.push({
          rule: rule.id,
          label: rule.label,
          severity: rule.severity,
          line: line,
          lineNum: i + 1,
          match: m[0],
          advice: rule.advice,
        });
      }
    }
  }
  return { findings };
}

/**
 * Apply autofix substitutions across content. Returns the new content plus a
 * tally of which rules fired and how many times.
 *
 * Only rules with severity === 'autofixable' AND a `replace` function run.
 */
function autofix(content) {
  if (typeof content !== 'string') {
    throw new TypeError('autofix(content): expected string');
  }
  if (isOptedOut(content)) return { content, applied: [], optedOut: true };
  let out = content;
  const applied = [];
  for (const rule of RULES) {
    if (rule.severity !== 'autofixable' || typeof rule.replace !== 'function') continue;
    const re = new RegExp(rule.pattern.source, rule.pattern.flags);
    let count = 0;
    out = out.replace(re, (...args) => {
      count++;
      return rule.replace(...args);
    });
    if (count > 0) applied.push({ rule: rule.id, label: rule.label, count });
  }
  return { content: out, applied };
}

function explain(finding) {
  const sev = finding.severity === 'autofixable'
    ? 'AUTOFIX'
    : finding.severity === 'requires-decision'
      ? 'REVIEW '
      : 'BLOCK  ';
  return `${sev} L${finding.lineNum}  ${finding.label}  —  ${finding.advice}`;
}

function isClean(findings) {
  return !findings || findings.length === 0;
}

function summarise(findings) {
  const out = { autofixable: 0, requiresDecision: 0, blocking: 0, total: 0 };
  if (!findings) return out;
  for (const f of findings) {
    out.total++;
    if (f.severity === 'autofixable') out.autofixable++;
    else if (f.severity === 'requires-decision') out.requiresDecision++;
    else if (f.severity === 'block') out.blocking++;
  }
  return out;
}

module.exports = {
  scan,
  autofix,
  explain,
  isClean,
  isOptedOut,
  summarise,
  // Exposed for tests / introspection
  _rules: RULES,
};
