#!/usr/bin/env node
import { Command } from 'commander';
import { spawn } from 'child_process';
import path2, { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { Severity, generateHTML, UnifiedReportSchema, ToolName, normalizeAnalysisResult, normalizeSeverity } from '@aiready/core';
import fs from 'fs';

var GRAPH_CONSTANTS = {
  DEFAULT_NODE_SIZE: 1,
  DEFAULT_REFERENCE_SIZE: 5,
  DEFAULT_DEPENDENCY_SIZE: 2,
  DEFAULT_CONTEXT_SIZE: 10,
  FUZZY_MATCH_THRESHOLD: 50,
  FUZZY_MATCH_HIGH_THRESHOLD: 80,
  COLORS: {
    CRITICAL: "#ff4d4f",
    MAJOR: "#ff9900",
    MINOR: "#ffd666",
    INFO: "#91d5ff",
    DEFAULT: "#97c2fc"
  },
  SEVERITY_ORDER: {
    [Severity.Critical]: 3,
    [Severity.Major]: 2,
    [Severity.Minor]: 1,
    [Severity.Info]: 0
  }
};
function normalizeLabel(filePath, rootDir) {
  try {
    return path2.relative(rootDir, filePath);
  } catch {
    return filePath;
  }
}
function extractReferencedPaths(message) {
  if (!message || typeof message !== "string") return [];
  const reAbs = /\/(?:[\w\-.]+\/)+[\w\-.]+\.(?:ts|tsx|js|jsx|py|java|go)/g;
  const reRel = /(?:\.\/|\.\.\/)(?:[\w\-.]+\/)+[\w\-.]+\.(?:ts|tsx|js|jsx|py|java|go)/g;
  const abs = message.match(reAbs) ?? [];
  const rel = message.match(reRel) ?? [];
  return abs.concat(rel);
}
function getPackageGroup(fp) {
  if (!fp) return void 0;
  const parts = fp.split(path2.sep);
  const pkgIdx = parts.indexOf("packages");
  if (pkgIdx >= 0 && parts.length > pkgIdx + 1)
    return `packages/${parts[pkgIdx + 1]}`;
  const landingIdx = parts.indexOf("landing");
  if (landingIdx >= 0) return "landing";
  const scriptsIdx = parts.indexOf("scripts");
  if (scriptsIdx >= 0) return "scripts";
  return parts.length > 1 ? parts[1] : parts[0];
}
function rankSeverity(s) {
  return normalizeSeverity(s || void 0);
}
function getColorForSeverity(sev) {
  switch (sev) {
    case Severity.Critical:
      return GRAPH_CONSTANTS.COLORS.CRITICAL;
    case Severity.Major:
      return GRAPH_CONSTANTS.COLORS.MAJOR;
    case Severity.Minor:
      return GRAPH_CONSTANTS.COLORS.MINOR;
    case Severity.Info:
      return GRAPH_CONSTANTS.COLORS.INFO;
    default:
      return GRAPH_CONSTANTS.COLORS.DEFAULT;
  }
}
var GraphProcessors = class {
  static processPatterns(builder, results, rootDir, bumpIssue) {
    const basenameMap = /* @__PURE__ */ new Map();
    results.forEach((p) => {
      const fileName = p.fileName ?? p.file;
      if (fileName) {
        const base = path2.basename(fileName);
        if (!basenameMap.has(base)) basenameMap.set(base, /* @__PURE__ */ new Set());
        basenameMap.get(base).add(fileName);
      }
    });
    results.forEach((entry) => {
      const normalized = normalizeAnalysisResult(entry);
      const file = normalized.fileName;
      if (!file) return;
      builder.addNode(
        file,
        `Issues: ${normalized.issues.length}`,
        normalized.metrics.tokenCost || GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE
      );
      const rawIssues = Array.isArray(entry.issues) ? entry.issues : [];
      if (rawIssues.length > 0) {
        rawIssues.forEach((issue) => {
          bumpIssue(file, rankSeverity(issue.severity));
        });
      } else {
        normalized.issues.forEach((issue) => {
          bumpIssue(file, issue.severity);
        });
      }
      normalized.issues.forEach((issue) => {
        const refs = extractReferencedPaths(issue.message);
        refs.forEach((ref) => {
          const target = path2.isAbsolute(ref) ? ref : path2.resolve(path2.dirname(file), ref);
          builder.addNode(
            target,
            "Referenced file",
            GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE
          );
          builder.addEdge(file, target, "reference");
        });
        const percMatch = (issue.message.match(/(\d+)%/) || [])[1];
        const perc = percMatch ? parseInt(percMatch, 10) : null;
        const wantFuzzy = issue.type === "duplicate-pattern" || /similar/i.test(issue.message) || perc !== null && perc >= GRAPH_CONSTANTS.FUZZY_MATCH_THRESHOLD;
        if (wantFuzzy) {
          const fileGroup = getPackageGroup(file);
          for (const [base, pathsSet] of basenameMap.entries()) {
            if (!issue.message.includes(base) || base === path2.basename(file))
              continue;
            for (const target of pathsSet) {
              const targetGroup = getPackageGroup(target);
              if (fileGroup !== targetGroup && !(perc !== null && perc >= GRAPH_CONSTANTS.FUZZY_MATCH_HIGH_THRESHOLD))
                continue;
              builder.addNode(
                target,
                "Fuzzy match",
                GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE
              );
              builder.addEdge(file, target, "similarity");
            }
          }
        }
      });
    });
  }
  static processDuplicates(builder, report, rootDir, fileIssues) {
    const patternData = report[ToolName.PatternDetect] || report.patternDetect || report.patterns || {};
    const duplicates = (Array.isArray(patternData.duplicates) ? patternData.duplicates : null) || (patternData.summary && Array.isArray(patternData.summary.duplicates) ? patternData.summary.duplicates : null) || (Array.isArray(report.duplicates) ? report.duplicates : []);
    duplicates.forEach((dup) => {
      builder.addNode(
        dup.file1,
        "Similarity target",
        GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE
      );
      builder.addNode(
        dup.file2,
        "Similarity target",
        GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE
      );
      builder.addEdge(dup.file1, dup.file2, "similarity");
      [dup.file1, dup.file2].forEach((file) => {
        const id = path2.resolve(rootDir, file);
        if (!fileIssues.has(id)) {
          fileIssues.set(id, { count: 0, maxSeverity: null, duplicates: 0 });
        }
        fileIssues.get(id).duplicates += 1;
      });
    });
  }
  static processContext(builder, results, rootDir, bumpIssue) {
    results.forEach((ctx) => {
      const normalized = normalizeAnalysisResult(ctx);
      const file = normalized.fileName;
      if (!file) return;
      builder.addNode(
        file,
        `Deps: ${ctx.dependencyCount || 0}`,
        GRAPH_CONSTANTS.DEFAULT_CONTEXT_SIZE
      );
      normalized.issues.forEach((issue) => {
        bumpIssue(file, issue.severity);
      });
      (ctx.relatedFiles ?? []).forEach((rel) => {
        const resolvedRel = path2.isAbsolute(rel) ? rel : path2.resolve(path2.dirname(file), rel);
        const resolvedFile = path2.resolve(builder.getRootDir(), file);
        const resolvedTarget = path2.resolve(builder.getRootDir(), resolvedRel);
        const keyA = `${resolvedFile}->${resolvedTarget}`;
        const keyB = `${resolvedTarget}->${resolvedFile}`;
        if (builder.hasEdge(keyA) || builder.hasEdge(keyB)) return;
        builder.addNode(
          resolvedRel,
          "Related file",
          GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE
        );
        const node = builder.getNode(resolvedTarget);
        if (node) {
          node.size = (node.size || 1) + 2;
        }
        builder.addEdge(file, resolvedRel, "related");
      });
      const fileDir = path2.dirname(path2.resolve(builder.getRootDir(), file));
      (ctx.dependencyList ?? []).forEach((dep) => {
        if (dep.startsWith(".") || dep.startsWith("/")) {
          const possiblePaths = [
            path2.resolve(fileDir, dep),
            path2.resolve(fileDir, dep + ".ts"),
            path2.resolve(fileDir, dep + ".tsx"),
            path2.resolve(fileDir, dep + ".js"),
            path2.resolve(fileDir, dep, "index.ts"),
            path2.resolve(fileDir, dep, "index.tsx")
          ];
          for (const p of possiblePaths) {
            if (fs.existsSync(p)) {
              builder.addNode(
                p,
                "Dependency",
                GRAPH_CONSTANTS.DEFAULT_DEPENDENCY_SIZE
              );
              builder.addEdge(file, p, "dependency");
              break;
            }
          }
        }
      });
    });
  }
  static processToolResults(builder, toolName, legacyKey, report, bumpIssue, title) {
    const camelKey = toolName.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
    const toolData = report[toolName] ?? report[camelKey] ?? report[legacyKey];
    if (!toolData) return;
    const results = Array.isArray(toolData) ? toolData : toolData.results ?? toolData.issues ?? [];
    results.forEach((item) => {
      if (!Array.isArray(item.issues) && (item.severity || item.message)) {
        const file2 = item.fileName ?? item.file ?? item.location?.file;
        if (file2) {
          builder.addNode(file2, title, GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE);
          bumpIssue(file2, rankSeverity(item.severity));
        }
        return;
      }
      const normalized = normalizeAnalysisResult(item);
      const file = normalized.fileName;
      if (file) {
        builder.addNode(file, title, GRAPH_CONSTANTS.DEFAULT_REFERENCE_SIZE);
        normalized.issues.forEach((issue) => {
          bumpIssue(file, issue.severity);
        });
      }
    });
  }
  static finalizeGraph(builder, fileIssues, report) {
    const graph = builder.build();
    let criticalIssues = 0;
    let majorIssues = 0;
    let minorIssues = 0;
    let infoIssues = 0;
    graph.nodes.forEach((node) => {
      const record = fileIssues.get(node.id);
      if (record) {
        node.duplicates = record.duplicates || 0;
        node.color = getColorForSeverity(record.maxSeverity);
        node.group = getPackageGroup(node.id);
        if (record.maxSeverity === Severity.Critical)
          criticalIssues += record.count;
        else if (record.maxSeverity === Severity.Major)
          majorIssues += record.count;
        else if (record.maxSeverity === Severity.Minor)
          minorIssues += record.count;
        else if (record.maxSeverity === Severity.Info)
          infoIssues += record.count;
      } else {
        node.color = getColorForSeverity(null);
        node.group = getPackageGroup(node.id);
        node.duplicates = 0;
      }
    });
    graph.metadata = {
      ...graph.metadata,
      criticalIssues,
      majorIssues,
      minorIssues,
      infoIssues,
      tokenBudget: report.scoring?.tokenBudget
    };
    return graph;
  }
};

// src/graph/builder.ts
var GraphBuilder = class _GraphBuilder {
  constructor(rootDir = process.cwd()) {
    this.rootDir = rootDir;
    this.nodesMap = /* @__PURE__ */ new Map();
    this.edges = [];
    this.edgesSet = /* @__PURE__ */ new Set();
  }
  // --- External accessors for processors ---
  getRootDir() {
    return this.rootDir;
  }
  hasEdge(key) {
    return this.edgesSet.has(key);
  }
  getNode(id) {
    return this.nodesMap.get(id);
  }
  /**
   * Add a new node to the graph or update an existing one.
   */
  addNode(file, title = "", size = GRAPH_CONSTANTS.DEFAULT_NODE_SIZE) {
    if (!file) return;
    const id = path2.resolve(this.rootDir, file);
    const existingNode = this.nodesMap.get(id);
    if (!existingNode) {
      const node = {
        id,
        path: id,
        label: normalizeLabel(id, this.rootDir),
        title,
        size
      };
      this.nodesMap.set(id, node);
    } else {
      if (title && (!existingNode.title || !existingNode.title.includes(title))) {
        existingNode.title = (existingNode.title ? existingNode.title + "\n" : "") + title;
      }
      if (size > (existingNode.size ?? 0)) {
        existingNode.size = size;
      }
    }
  }
  /**
   * Add a directed edge between two nodes in the graph.
   */
  addEdge(from, to, type = "link") {
    if (!from || !to) return;
    const source = path2.resolve(this.rootDir, from);
    const target = path2.resolve(this.rootDir, to);
    if (source === target) return;
    const key = `${source}->${target}`;
    if (!this.edgesSet.has(key)) {
      this.edges.push({
        source,
        target,
        type
      });
      this.edgesSet.add(key);
    }
  }
  /**
   * Build the final GraphData object from collected nodes and edges.
   */
  build() {
    const nodes = Array.from(this.nodesMap.values());
    return {
      nodes,
      edges: [...this.edges],
      clusters: [],
      issues: [],
      metadata: {
        timestamp: (/* @__PURE__ */ new Date()).toISOString(),
        totalFiles: nodes.length,
        totalDependencies: this.edges.length,
        analysisTypes: [],
        criticalIssues: 0,
        majorIssues: 0,
        minorIssues: 0,
        infoIssues: 0
      },
      truncated: {
        nodes: false,
        edges: false
      }
    };
  }
  /**
   * Static helper to build graph from an AIReady report JSON.
   */
  static buildFromReport(report, rootDir = process.cwd()) {
    const validation = UnifiedReportSchema.safeParse(report);
    if (!validation.success) {
      console.warn(
        "Visualizer: Report does not fully match UnifiedReportSchema, proceeding with best-effort parsing."
      );
    }
    const builder = new _GraphBuilder(rootDir);
    const fileIssues = /* @__PURE__ */ new Map();
    const bumpIssue = (file, severity) => {
      if (!file) return;
      const id = path2.resolve(rootDir, file);
      if (!fileIssues.has(id)) {
        fileIssues.set(id, { count: 0, maxSeverity: null, duplicates: 0 });
      }
      const record = fileIssues.get(id);
      record.count += 1;
      if (severity) {
        if (!record.maxSeverity || GRAPH_CONSTANTS.SEVERITY_ORDER[severity] > GRAPH_CONSTANTS.SEVERITY_ORDER[record.maxSeverity]) {
          record.maxSeverity = severity;
        }
      }
    };
    const getResults = (toolKey, legacyKey) => {
      const camelKey = toolKey.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
      const toolData = report[toolKey] ?? report[camelKey] ?? (legacyKey ? report[legacyKey] : void 0);
      if (!toolData) return [];
      if (Array.isArray(toolData)) return toolData;
      return toolData.results ?? toolData.issues ?? [];
    };
    GraphProcessors.processPatterns(
      builder,
      getResults(ToolName.PatternDetect, "patterns"),
      rootDir,
      bumpIssue
    );
    GraphProcessors.processDuplicates(builder, report, rootDir, fileIssues);
    GraphProcessors.processContext(
      builder,
      getResults(ToolName.ContextAnalyzer, "context"),
      rootDir,
      bumpIssue
    );
    GraphProcessors.processToolResults(
      builder,
      ToolName.DocDrift,
      "docDrift",
      report,
      bumpIssue,
      "Doc-Drift Issue"
    );
    GraphProcessors.processToolResults(
      builder,
      ToolName.DependencyHealth,
      "dependencyHealth",
      report,
      bumpIssue,
      "Dependency Issue"
    );
    GraphProcessors.processToolResults(
      builder,
      ToolName.ContractEnforcement,
      "contractEnforcement",
      report,
      bumpIssue,
      "Contract Gap"
    );
    return GraphProcessors.finalizeGraph(builder, fileIssues, report);
  }
};
function createSampleGraph() {
  const builder = new GraphBuilder(process.cwd());
  builder.addNode("src/components/Button.tsx", "Button", 15);
  builder.addNode("src/utils/helpers.ts", "helpers", 12);
  builder.addNode("src/services/api.ts", "api", 18);
  builder.addEdge(
    "src/components/Button.tsx",
    "src/utils/helpers.ts",
    "dependency"
  );
  builder.addEdge("src/utils/helpers.ts", "src/services/api.ts", "dependency");
  return builder.build();
}
var __filename$1 = fileURLToPath(import.meta.url);
var __dirname$1 = dirname(__filename$1);
var WEB_PORT = 8e3;
var program = new Command();
program.name("aiready-visualize").description(
  "Generate interactive visualizations from AIReady analysis results"
).version("0.1.0").option("-d, --dev", "Start interactive web application (default)", true).option("-o, --output <file>", "Output HTML file for static generation").passThroughOptions();
function startDevServer(rootDir) {
  const webDir = resolve(__dirname$1, "../web");
  console.log("\u{1F3AF} AIReady Visualizer");
  console.log("\u{1F680} Starting interactive web application...");
  console.log();
  console.log(`\u{1F4C1} Project root: ${rootDir}`);
  console.log(`\u{1F310} Web server: http://localhost:${WEB_PORT}`);
  console.log();
  console.log("\u{1F4A1} The web app requires report data to visualize.");
  console.log('   Run "pnpm aiready scan ." then copy the report to:');
  console.log(`   web/public/report-data.json`);
  console.log();
  console.log("Press Ctrl+C to stop the server.");
  console.log();
  const vite = spawn("pnpm", ["dev"], {
    cwd: webDir,
    stdio: "inherit",
    shell: true,
    env: { ...process.env, FORCE_COLOR: "1" }
  });
  vite.on("error", (err) => {
    console.error("\u274C Failed to start dev server:", err.message);
    process.exit(1);
  });
}
program.command("sample").description("Generate a sample visualization for testing").option("-o, --output <file>", "Output HTML file", "visualization.html").option("--open", "Open in browser").action(async (options) => {
  const { writeFileSync } = await import('fs');
  const { exec } = await import('child_process');
  console.log("Generating sample visualization...");
  const graph = createSampleGraph();
  console.log(
    `
Sample graph created with ${graph.nodes.length} nodes and ${graph.edges.length} edges`
  );
  const html = generateHTML(graph);
  const outputPath = resolve(options.output);
  writeFileSync(outputPath, html);
  console.log(`\u2705 HTML saved to: ${outputPath}`);
  if (options.open) {
    const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
    exec(`${opener} "${outputPath}"`);
  }
});
program.command("generate").description("Generate visualization from analysis results").argument("<input>", "Input JSON file with analysis results").option("-o, --output <file>", "Output HTML file", "visualization.html").option("--open", "Open in browser").action(async (input, options) => {
  const { readFileSync, writeFileSync } = await import('fs');
  const { exec } = await import('child_process');
  console.log(`Reading analysis results from: ${input}`);
  const report = JSON.parse(readFileSync(input, "utf-8"));
  const rootDir = process.cwd();
  const graph = GraphBuilder.buildFromReport(report, rootDir);
  console.log(
    `Graph built: ${graph.nodes.length} nodes, ${graph.edges.length} edges`
  );
  const html = generateHTML(graph);
  const outputPath = resolve(options.output);
  writeFileSync(outputPath, html);
  console.log(`\u2705 HTML saved to: ${outputPath}`);
  if (options.open) {
    const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
    exec(`${opener} "${outputPath}"`);
  }
});
var args = process.argv.slice(2);
if (args.length === 0 || args.length === 1 && (args[0] === "--dev" || args[0] === "-d")) {
  startDevServer(process.cwd());
} else {
  program.parse();
}
//# sourceMappingURL=cli.js.map
//# sourceMappingURL=cli.js.map