// bom-builder.mjs — CycloneDX 1.6 AI-BOM builder
// Discovers AI components in a Claude Code project and builds a
// CycloneDX-compatible BOM. Zero external dependencies.

import { readFile, readdir, access, stat } from 'node:fs/promises';
import { join, basename, extname } from 'node:path';
import { parseFrontmatter } from './yaml-frontmatter.mjs';

// ---------------------------------------------------------------------------
// File helpers
// ---------------------------------------------------------------------------

async function readJson(filePath) {
  try { return JSON.parse(await readFile(filePath, 'utf-8')); }
  catch { return null; }
}

async function readText(filePath) {
  try { return await readFile(filePath, 'utf-8'); }
  catch { return null; }
}

async function fileExists(filePath) {
  try { await access(filePath); return true; }
  catch { return false; }
}

async function listDir(dirPath) {
  try { return await readdir(dirPath); }
  catch { return []; }
}

// ---------------------------------------------------------------------------
// Component discovery
// ---------------------------------------------------------------------------

/**
 * Extract model references from text (CLAUDE.md, agent frontmatter, settings).
 * @param {string} text
 * @returns {string[]} unique model names
 */
function extractModels(text) {
  if (!text) return [];
  const modelPatterns = [
    /\b(claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5[-\w]*)\b/gi,
    /\b(opus|sonnet|haiku)\b/gi,
    /\b(gpt-4o?[-\w]*|gpt-3\.5[-\w]*)\b/gi,
    /\b(gemini[-\w]*)\b/gi,
  ];
  const found = new Set();
  for (const pat of modelPatterns) {
    for (const match of text.matchAll(pat)) {
      found.add(match[1].toLowerCase());
    }
  }
  return [...found];
}

/**
 * Discover all AI components in a project.
 * @param {string} projectRoot
 * @returns {Promise<object[]>} Array of CycloneDX component objects
 */
export async function discoverComponents(projectRoot) {
  const components = [];
  const dependencies = [];

  // 1. ML Models — from CLAUDE.md, agent frontmatter, settings
  const modelSources = new Set();
  const claudeMd = await readText(join(projectRoot, 'CLAUDE.md'));
  for (const m of extractModels(claudeMd)) modelSources.add(m);

  // Agent files
  const agentDir = join(projectRoot, 'agents');
  const agentFiles = (await listDir(agentDir)).filter(f => f.endsWith('.md'));
  for (const file of agentFiles) {
    const content = await readText(join(agentDir, file));
    if (!content) continue;
    const fm = parseFrontmatter(content);
    if (fm?.model) modelSources.add(fm.model.toLowerCase());
    for (const m of extractModels(content)) modelSources.add(m);
  }

  // Settings files
  for (const settingsPath of [
    join(projectRoot, '.claude', 'settings.json'),
    join(projectRoot, 'settings.json'),
  ]) {
    const settings = await readJson(settingsPath);
    if (settings) {
      for (const m of extractModels(JSON.stringify(settings))) modelSources.add(m);
    }
  }

  for (const model of modelSources) {
    components.push({
      type: 'machine-learning-model',
      name: model,
      'bom-ref': `model-${model}`,
    });
  }

  // 2. MCP Servers — from .mcp.json, .claude/settings.json
  const mcpSources = [
    join(projectRoot, '.mcp.json'),
    join(projectRoot, '.claude', '.mcp.json'),
  ];
  for (const mcpPath of mcpSources) {
    const mcpConfig = await readJson(mcpPath);
    if (!mcpConfig?.mcpServers) continue;
    for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
      components.push({
        type: 'framework',
        name: `mcp-server:${name}`,
        'bom-ref': `mcp-${name}`,
        description: config.command ? `${config.command} ${(config.args || []).join(' ')}`.trim() : undefined,
      });
    }
  }

  // 3. Plugins — from global settings enabledPlugins
  const globalSettings = await readJson(join(projectRoot, '.claude', 'settings.json'));
  if (globalSettings?.enabledPlugins) {
    for (const plugin of globalSettings.enabledPlugins) {
      components.push({
        type: 'library',
        name: `plugin:${basename(plugin)}`,
        'bom-ref': `plugin-${basename(plugin)}`,
      });
    }
  }

  // 4. Knowledge bases — knowledge/*.md files
  const knowledgeDir = join(projectRoot, 'knowledge');
  const knowledgeFiles = (await listDir(knowledgeDir)).filter(f => f.endsWith('.md') || f.endsWith('.json'));
  for (const file of knowledgeFiles) {
    components.push({
      type: 'data',
      name: `knowledge:${file}`,
      'bom-ref': `knowledge-${file}`,
    });
  }

  // 5. Hooks — from hooks/hooks.json
  const hooksJson = await readJson(join(projectRoot, 'hooks', 'hooks.json'));
  if (hooksJson?.hooks) {
    for (const [event, entries] of Object.entries(hooksJson.hooks)) {
      for (const entry of (Array.isArray(entries) ? entries : [entries])) {
        const hooks = entry.hooks || [];
        for (const hook of hooks) {
          if (hook.command) {
            const hookName = basename(hook.command.split('/').pop().replace(/\$\{.*?\}/g, ''));
            components.push({
              type: 'framework',
              name: `hook:${hookName}`,
              'bom-ref': `hook-${hookName}-${event}`,
              description: `${event} hook${entry.matcher ? ` (matcher: ${entry.matcher})` : ''}`,
            });
          }
        }
      }
    }
  }

  return components;
}

/**
 * Build a CycloneDX 1.6 AI-BOM from discovered components.
 * @param {object[]} components - From discoverComponents()
 * @param {object} [projectMeta] - { name, version }
 * @returns {object} CycloneDX 1.6 JSON
 */
export function buildAIBOM(components, projectMeta = {}) {
  return {
    bomFormat: 'CycloneDX',
    specVersion: '1.6',
    version: 1,
    metadata: {
      timestamp: new Date().toISOString(),
      tools: [{
        vendor: 'llm-security',
        name: 'ai-bom-generator',
        version: '6.0.0',
      }],
      component: {
        type: 'application',
        name: projectMeta.name || 'unknown',
        version: projectMeta.version || '0.0.0',
      },
    },
    components,
    dependencies: [],
  };
}
