#!/usr/bin/env node
/*
  Generates an auditable map of state that can grow without bound.

  Focus (best-effort heuristics):
  - Dynamic arrays in state variables: `Type[] ...;`
  - Mappings to dynamic arrays: `mapping(...) => Type[] ...;`

  Excludes contracts/deprecated.

  Writes: docs/security/STORAGE_GROWTH_SURFACE_AUTOGEN.md

  Note: This is documentation tooling, not a formal Solidity parser.
*/

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

const ROOT = process.cwd();
const CONTRACTS_DIR = path.join(ROOT, 'contracts');
const OUT_PATH = path.join(ROOT, 'docs', 'security', 'STORAGE_GROWTH_SURFACE_AUTOGEN.md');

function walk(dir, out) {
  const entries = fs.readdirSync(dir, { withFileTypes: true });
  for (const entry of entries) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      if (full.includes(`${path.sep}contracts${path.sep}deprecated${path.sep}`)) continue;
      walk(full, out);
    } else if (entry.isFile() && entry.name.endsWith('.sol')) {
      if (full.includes(`${path.sep}contracts${path.sep}deprecated${path.sep}`)) continue;
      out.push(full);
    }
  }
}

function stripLineComments(line) {
  const idx = line.indexOf('//');
  return idx >= 0 ? line.slice(0, idx) : line;
}

function isLikelyStateVarLine(line) {
  // Best-effort: skip function bodies (indented a lot) and require a semicolon.
  const trimmed = line.trim();
  if (!trimmed.endsWith(';')) return false;
  if (trimmed.startsWith('function ')) return false;
  if (trimmed.startsWith('event ')) return false;
  if (trimmed.startsWith('error ')) return false;
  if (trimmed.startsWith('modifier ')) return false;
  if (trimmed.startsWith('using ')) return false;
  if (trimmed.startsWith('import ')) return false;
  if (trimmed.startsWith('pragma ')) return false;
  if (trimmed.startsWith('struct ') || trimmed.startsWith('enum ')) return false;
  // Roughly avoid local declarations by requiring it not to begin with `return`/`require`/etc.
  if (/^(return|require|revert|emit)\b/.test(trimmed)) return false;
  return true;
}

function extract(filePath) {
  const content = fs.readFileSync(filePath, 'utf8');
  const lines = content.split(/\r?\n/);

  let contractName = null;
  const hits = []; // { contractName, fileRel, line, kind, decl }

  for (let i = 0; i < lines.length; i++) {
    const raw = lines[i];
    const noComment = stripLineComments(raw);

    const contractMatch = noComment.match(/^\s*contract\s+([A-Za-z0-9_]+)\b/);
    if (contractMatch) contractName = contractMatch[1];

    if (!isLikelyStateVarLine(noComment)) continue;

    const trimmed = noComment.trim();

    // mapping(...) => Type[] name;
    const mapToArray = trimmed.match(/^mapping\s*\([^)]*\)\s*(public\s+)?([A-Za-z0-9_\[\] ]*=>\s*)?([A-Za-z0-9_]+)\s*\[\]\s+([A-Za-z0-9_]+)\s*;/);
    if (trimmed.includes('mapping') && /=>\s*.*\[\]\s+/.test(trimmed)) {
      hits.push({
        contractName: contractName || '(unknown-contract)',
        fileRel: path.relative(ROOT, filePath).split(path.sep).join('/'),
        line: i + 1,
        kind: 'mapping→array',
        decl: raw.trim(),
      });
      continue;
    }

    // Type[] name;
    if (/\b[A-Za-z0-9_]+\s*\[\]\s+[A-Za-z0-9_]+\s*;/.test(trimmed) && !trimmed.startsWith('mapping')) {
      hits.push({
        contractName: contractName || '(unknown-contract)',
        fileRel: path.relative(ROOT, filePath).split(path.sep).join('/'),
        line: i + 1,
        kind: 'array',
        decl: raw.trim(),
      });
    }
  }

  return hits;
}

function render(byContract) {
  const parts = [];
  parts.push('# Storage Growth Surface (Autogenerated)');
  parts.push('');
  parts.push('This file is autogenerated from Solidity sources. Do not hand-edit.');
  parts.push('');
  parts.push('- Generated by: `node tools/generate_storage_growth_surface.js`');
  parts.push('');
  parts.push('Vaultfire principle: unbounded state growth should be **visible**, and UIs/indexers should have safe pagination patterns.');
  parts.push('');

  const contracts = Object.keys(byContract).sort((a, b) => a.localeCompare(b));
  if (!contracts.length) {
    parts.push('_No dynamic state arrays detected in non-deprecated contracts._');
    parts.push('');
    return parts.join('\n');
  }

  for (const name of contracts) {
    parts.push(`## ${name}`);
    parts.push('');
    for (const h of byContract[name]) {
      parts.push(`- ${h.fileRel}:${h.line} [${h.kind}] ${h.decl}`);
    }
    parts.push('');
  }

  parts.push('---');
  parts.push('');
  parts.push('Review notes:');
  parts.push('- Unbounded arrays are not automatically a bug, but they are a scaling risk for UIs/indexers.');
  parts.push('- Prefer pagination getters for any arrays expected to grow over time.');
  parts.push('');

  return parts.join('\n');
}

(function main() {
  const files = [];
  walk(CONTRACTS_DIR, files);

  const byContract = {};
  for (const file of files) {
    const hits = extract(file);
    for (const h of hits) {
      byContract[h.contractName] ||= [];
      byContract[h.contractName].push(h);
    }
  }

  for (const k of Object.keys(byContract)) {
    byContract[k] = byContract[k].sort((a, b) => {
      const fc = a.fileRel.localeCompare(b.fileRel);
      if (fc !== 0) return fc;
      return a.line - b.line;
    });
  }

  fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true });
  fs.writeFileSync(OUT_PATH, render(byContract), 'utf8');
})();
