#!/usr/bin/env node

/**
 * tl-impact - Analyze the blast radius of changing a file
 *
 * Shows which files import/depend on the target file, helping you
 * understand the impact of changes before you make them.
 *
 * Usage: tl-impact <file> [--depth N]
 */

// Prompt info for tl-prompt
if (process.argv.includes('--prompt')) {
  console.log(JSON.stringify({
    name: 'tl-impact',
    desc: 'Blast radius - what depends on this file',
    when: 'before-modify',
    example: 'tl-impact src/utils.ts'
  }));
  process.exit(0);
}

import { existsSync, readFileSync } from 'node:fs';
import { basename, dirname, extname, relative, resolve } from 'node:path';
import {
  createOutput,
  parseCommonArgs,
  estimateTokens,
  formatTokens,
  COMMON_OPTIONS_HELP
} from '../src/output.mjs';
import { findProjectRoot, categorizeFile, detectLanguage } from '../src/project.mjs';
import { withCache } from '../src/cache.mjs';
import { listFiles } from '../src/traverse.mjs';
import { formatImportBindings, getJsTsProjectGraph } from '../src/semantic-js-graph.mjs';

const HELP = `
tl-impact - Analyze the blast radius of changing a file

Usage: tl-impact <file> [options]

Options:
  --depth N, -d N       Include transitive importers up to N levels (default: 1)
  --why                 Show which exports each importer uses
${COMMON_OPTIONS_HELP}

Examples:
  tl-impact src/utils/api.ts         # Direct importers only
  tl-impact src/utils/api.ts -d 2    # Include files that import the importers
  tl-impact src/utils/api.ts -j      # JSON output

Output shows:
  - Export usage summary (which exports are used and by how many files)
  - Which files import the target
  - Token cost of each importer
  - Line number of the import
  - Categorized by source/test/story/mock
`;

// ─────────────────────────────────────────────────────────────
// Import Detection — single-pass reverse import map
// ─────────────────────────────────────────────────────────────

const CODE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts']);
const RESOLVE_SUFFIXES = ['', '.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts', '/index.js', '/index.ts', '/index.tsx', '/index.mjs', '/index.mts', '/index.cjs', '/index.cts'];

/**
 * Resolve an import specifier to an absolute path.
 * Returns null if the specifier is a bare module (npm package) or unresolvable.
 */
function resolveImportSpecifier(spec, importerDir) {
  if (!spec.startsWith('.') && !spec.startsWith('/')) return null;

  const base = resolve(importerDir, spec);
  for (const suffix of RESOLVE_SUFFIXES) {
    const candidate = base + suffix;
    if (existsSync(candidate)) return candidate;
  }
  return null;
}

/**
 * Build a reverse import map for the entire project in a single pass.
 * Returns { [targetAbsPath]: [ { importer, line, importType, statement }, ... ] }
 */
function buildReverseImportMap(projectRoot) {
  const files = listFiles(projectRoot);
  if (!files || files.length === 0) return Object.create(null);

  // Filter to code files only
  const codeFiles = [];
  for (const file of files) {
    const relPath = file.relativePath || relative(projectRoot, file.path);
    const ext = extname(relPath).toLowerCase();
    if (CODE_EXTENSIONS.has(ext)) {
      codeFiles.push(file.path);
    }
  }

  const reverseMap = Object.create(null);

  for (const filePath of codeFiles) {
    let content;
    try {
      content = readFileSync(filePath, 'utf-8');
    } catch {
      continue;
    }

    const importerDir = dirname(filePath);
    const lines = content.split('\n');

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

      const importMatches = [
        ...line.matchAll(/from\s+['"]([^'"]+)['"]/g),
        ...line.matchAll(/import\s+['"]([^'"]+)['"]/g),
        ...line.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g),
        ...line.matchAll(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g),
      ];

      for (const match of importMatches) {
        const resolved = resolveImportSpecifier(match[1], importerDir);
        if (!resolved) continue;

        let importType = 'import';
        if (line.includes('require(')) importType = 'require';
        if (line.includes('import(')) importType = 'dynamic import';
        if (line.match(/import\s+type/)) importType = 'type import';

        // Capture full import statement (handles multi-line imports)
        let statement = line.trim();
        if (!statement.includes('import ') && !statement.includes('require')) {
          // This line has 'from' but not 'import' — look backward for the import clause
          let fullImport = '';
          for (let j = Math.max(0, i - 10); j <= i; j++) {
            fullImport += lines[j].trim() + ' ';
            if (lines[j].trim().startsWith('import ')) break;
          }
          // Build it forward from the import keyword
          fullImport = '';
          for (let j = i; j >= Math.max(0, i - 10); j--) {
            if (lines[j].trim().startsWith('import ')) {
              for (let k = j; k <= i; k++) fullImport += lines[k].trim() + ' ';
              break;
            }
          }
          if (fullImport) statement = fullImport.trim();
        }

        if (!reverseMap[resolved]) reverseMap[resolved] = [];
        reverseMap[resolved].push({
          importer: filePath,
          line: i + 1,
          importType,
          statement: statement.substring(0, 200)
        });
      }
    }
  }

  return reverseMap;
}

/**
 * Look up direct importers from the reverse map.
 * Deduplicates by importer path (first match wins), skips self-imports.
 */
function findDirectImporters(filePath, reverseMap) {
  const importers = new Map();
  const entries = reverseMap[filePath];
  if (!entries) return importers;

  for (const entry of entries) {
    if (entry.importer === filePath) continue;
    if (importers.has(entry.importer)) continue;
    importers.set(entry.importer, {
      line: entry.line,
      importType: entry.importType,
      statement: entry.statement
    });
  }

  return importers;
}

/**
 * Breadth-first transitive importer walk. `findDirect(path)` returns the direct
 * importers of a given file as a Map; the lookup source (reverse map vs JS/TS
 * graph) is supplied by the caller so this works for both resolution modes.
 */
function findTransitiveImporters(directImporters, targetPath, findDirect, maxDepth = 2) {
  const allImporters = new Map(directImporters);
  const processed = new Set([targetPath]);
  let currentLevel = [...directImporters.keys()];

  for (let depth = 1; depth < maxDepth && currentLevel.length > 0; depth++) {
    const nextLevel = [];

    for (const filePath of currentLevel) {
      if (processed.has(filePath)) continue;
      processed.add(filePath);

      const importers = findDirect(filePath);

      for (const [path, info] of importers) {
        if (!allImporters.has(path) && !processed.has(path)) {
          allImporters.set(path, { ...info, depth, via: basename(filePath) });
          nextLevel.push(path);
        }
      }
    }

    currentLevel = nextLevel;
  }

  return allImporters;
}

function formatGraphImportType(edge) {
  if (edge.importType === 'dynamic-import') return 'dynamic import';
  if (edge.importType === 'reexport') return 'reexport';
  if (edge.importType === 'require' || edge.importType === 'import-equals') return 'require';
  if (edge.isTypeOnly) return 'type import';
  return 'import';
}

function buildGraphImporterInfo(edge) {
  return {
    line: edge.line,
    importType: formatGraphImportType(edge),
    statement: edge.statement,
    bindings: edge.bindings || [],
    usedBindings: formatImportBindings(edge.bindings || [])
  };
}

function findDirectJsTsImporters(relPath, graph) {
  const importers = new Map();
  const entries = graph.reverseImports[relPath] || [];

  for (const entry of entries) {
    if (entry.importer === relPath) continue;
    if (importers.has(entry.importer)) continue;
    importers.set(entry.importer, buildGraphImporterInfo(entry));
  }

  return importers;
}

function materializeGraphImporters(importers, projectRoot) {
  return new Map(
    [...importers.entries()].map(([relPath, info]) => [resolve(projectRoot, relPath), info])
  );
}


// ─────────────────────────────────────────────────────────────
// Import Statement Parsing (--why)
// ─────────────────────────────────────────────────────────────

function extractImportedNames(statement) {
  // "import { foo, bar } from './x'" -> ['foo', 'bar']
  const namedMatch = statement.match(/\{\s*([^}]+)\s*\}/);
  if (namedMatch) {
    return namedMatch[1].split(',').map(s => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
  }
  // "import Foo from './x'" -> ['default (Foo)']
  const defaultMatch = statement.match(/import\s+(\w+)\s+from/);
  if (defaultMatch) return [`default (${defaultMatch[1]})`];
  // "import * as X from './x'" -> ['* (X)']
  const starMatch = statement.match(/import\s+\*\s+as\s+(\w+)/);
  if (starMatch) return [`* (${starMatch[1]})`];
  // require: "const { foo } = require('./x')"
  const reqNamed = statement.match(/const\s+\{\s*([^}]+)\s*\}\s*=\s*require/);
  if (reqNamed) {
    return reqNamed[1].split(',').map(s => s.trim().split(/\s*:\s*/)[0].trim()).filter(Boolean);
  }
  // require: "const X = require('./x')"
  const reqDefault = statement.match(/const\s+(\w+)\s*=\s*require/);
  if (reqDefault) return [`default (${reqDefault[1]})`];
  // Dynamic import
  if (statement.includes('import(')) return ['dynamic'];
  return [];
}

function getUsedBindings(info) {
  if (Array.isArray(info.usedBindings) && info.usedBindings.length > 0) {
    return info.usedBindings;
  }
  if (typeof info.statement === 'string' && info.statement) {
    return extractImportedNames(info.statement);
  }
  return [];
}

// ─────────────────────────────────────────────────────────────
// Output
// ─────────────────────────────────────────────────────────────

function buildResults(importers, projectRoot) {
  const categories = { source: [], test: [], story: [], mock: [] };

  for (const [path, info] of importers) {
    let category = categorizeFile(path, projectRoot);
    if (!categories[category]) category = 'source';
    const tokens = estimateTokens(readFileSync(path, 'utf-8'));
    categories[category].push({
      path,
      relPath: relative(projectRoot, path),
      tokens,
      ...info
    });
  }

  for (const cat of Object.keys(categories)) {
    categories[cat].sort((a, b) => {
      if ((a.depth || 0) !== (b.depth || 0)) return (a.depth || 0) - (b.depth || 0);
      return a.path.localeCompare(b.path);
    });
  }

  return categories;
}

function buildExportSummary(categories) {
  const counts = {};

  for (const files of Object.values(categories)) {
    for (const file of files) {
      const names = getUsedBindings(file);
      for (const name of names) {
        if (name.startsWith('*') || name === 'dynamic') continue;
        const clean = name.startsWith('default (') ? name.slice(9, -1) : name;
        counts[clean] = (counts[clean] || 0) + 1;
      }
    }
  }

  return Object.entries(counts)
    .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
}

function printCategory(out, title, files, emoji, showWhy) {
  if (files.length === 0) return { totalFiles: 0, totalTokens: 0 };

  out.add(`${emoji ? emoji + ' ' : ''}${title} (${files.length}):`);

  let totalTokens = 0;
  for (const file of files) {
    totalTokens += file.tokens;
    let line = `   ${file.relPath} (~${formatTokens(file.tokens)}) L${file.line}`;
    if (file.depth && file.depth > 0) {
      line += ` [via ${file.via}]`;
    }
    out.add(line);
    if (showWhy) {
      const names = getUsedBindings(file);
      if (names.length > 0) {
        out.add(`      uses: ${names.join(', ')}`);
      }
    }
  }
  out.blank();

  return { totalFiles: files.length, totalTokens };
}

// ─────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────

const args = process.argv.slice(2);
const options = parseCommonArgs(args);

// Parse tool-specific options
let maxDepth = 1;
let showWhy = false;
for (let i = 0; i < options.remaining.length; i++) {
  const arg = options.remaining[i];
  if ((arg === '--depth' || arg === '-d') && options.remaining[i + 1]) {
    maxDepth = parseInt(options.remaining[i + 1], 10);
    i++;
  } else if (arg === '--why') {
    showWhy = true;
  }
}

const filePath = options.remaining.find(a => !a.startsWith('-'));

if (options.help || !filePath) {
  console.log(HELP);
  process.exit(options.help ? 0 : 1);
}

const resolvedPath = resolve(filePath);

if (!existsSync(resolvedPath)) {
  console.error(`File not found: ${filePath}`);
  process.exit(1);
}

const projectRoot = findProjectRoot(dirname(resolvedPath));
const relPath = relative(projectRoot, resolvedPath);
const lang = detectLanguage(resolvedPath);
const useJsTsGraph = lang === 'javascript' || lang === 'typescript';
const targetTokens = estimateTokens(readFileSync(resolvedPath, 'utf-8'));

const out = createOutput(options);

out.header(`\nImpact analysis: ${relPath}`);
out.header(`   Target file: ~${formatTokens(targetTokens)} tokens`);

if (maxDepth > 1) {
  out.header(`   Analyzing ${maxDepth} levels of dependencies...`);
}

let importers;

if (useJsTsGraph) {
  const graph = getJsTsProjectGraph(resolvedPath, { projectRoot });
  const targetRelPath = relative(projectRoot, resolvedPath);
  const directImporters = findDirectJsTsImporters(targetRelPath, graph);
  const graphImporters = maxDepth > 1
    ? findTransitiveImporters(directImporters, targetRelPath, relPath => findDirectJsTsImporters(relPath, graph), maxDepth)
    : directImporters;
  importers = materializeGraphImporters(graphImporters, projectRoot);
} else {
  const reverseMap = withCache(
    { op: 'reverse-import-map' },
    () => buildReverseImportMap(projectRoot),
    { projectRoot, headOnly: true }
  );

  const directImporters = findDirectImporters(resolvedPath, reverseMap);
  importers = maxDepth > 1
    ? findTransitiveImporters(directImporters, resolvedPath, filePath => findDirectImporters(filePath, reverseMap), maxDepth)
    : directImporters;
}

// Set JSON data
out.setData('target', relPath);
out.setData('targetTokens', targetTokens);
out.setData('maxDepth', maxDepth);
out.setData('backend', useJsTsGraph ? 'semantic-graph' : 'regex');

if (importers.size === 0) {
  out.add('No importers found - this file has no dependents!');
  out.blank();
  out.add('This could mean:');
  out.add('  - It\'s an entry point (main, index)');
  out.add('  - It\'s a standalone script');
  out.add('  - It\'s unused and can be safely deleted');
  out.blank();

  out.setData('importers', []);
  out.setData('totalFiles', 0);
  out.setData('totalTokens', 0);

  out.print();
  process.exit(0);
}

const categories = buildResults(importers, projectRoot);

// Export usage summary
const exportSummary = buildExportSummary(categories);
if (exportSummary.length > 0) {
  const MAX_INLINE = 8;
  const parts = exportSummary.slice(0, MAX_INLINE).map(([name, count]) => `${name} (${count})`);
  let line = '   Exports used: ' + parts.join(', ');
  if (exportSummary.length > MAX_INLINE) {
    line += `, ... +${exportSummary.length - MAX_INLINE} more`;
  }
  out.header(line);
}
out.blank();

// Set JSON data
out.setData('exportUsage', Object.fromEntries(exportSummary));
out.setData('importers', categories);

let totalFiles = 0;
let totalTokens = 0;

const s = printCategory(out, 'Source files', categories.source, '', showWhy);
totalFiles += s.totalFiles; totalTokens += s.totalTokens;

const t = printCategory(out, 'Test files', categories.test, '', showWhy);
totalFiles += t.totalFiles; totalTokens += t.totalTokens;

const st = printCategory(out, 'Stories', categories.story, '', showWhy);
totalFiles += st.totalFiles; totalTokens += st.totalTokens;

const m = printCategory(out, 'Mocks/Fixtures', categories.mock, '', showWhy);
totalFiles += m.totalFiles; totalTokens += m.totalTokens;

out.setData('totalFiles', totalFiles);
out.setData('totalTokens', totalTokens);

out.stats('─'.repeat(50));
out.stats(`Total impact: ${totalFiles} files, ~${formatTokens(totalTokens)} tokens`);
out.stats(`   Changing ${basename(resolvedPath)} may affect all listed files.`);
out.blank();

out.print();
