/**
 * graph.ts — Dependency graph construction and topological sort into parallel waves.
 *
 * Cycle detection (Tarjan SCC) and cycle collapsing are delegated to the
 * shared lib/graph.ts. This file provides the plan-development-specific
 * buildPlan + Kahn's algorithm with WaveModule construction.
 */
import { tarjanSCC, collapseCycles } from '../../../../lib/graph.js';
import type {
  ModuleInfo,
  ModuleKey,
  DevelopmentPlan,
  Wave,
  WaveModule,
  CrossModuleDep,
} from './types.js';
import { moduleKey } from './types.js';

// Re-export for existing test imports
export { tarjanSCC } from '../../../../lib/graph.js';

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

/**
 * Build the development plan from parsed module data.
 *
 * 1. Build a directed dependency graph (module A → module B means A depends on B).
 * 2. Identify external modules (referenced but not in the selected apps).
 * 3. Detect cycles via Tarjan's SCC algorithm.
 * 4. Topological sort into waves via Kahn's algorithm with level tracking.
 * 5. External deps form Wave 0; internal modules fill Wave 1+.
 */
export function buildPlan(
  modules: ModuleInfo[],
  selectedApps: string[],
): DevelopmentPlan {
  const warnings: string[] = [];

  // Index all in-scope modules by key
  const moduleMap = new Map<ModuleKey, ModuleInfo>();
  for (const mod of modules) {
    moduleMap.set(mod.key, mod);
  }

  // --- Build adjacency list and collect external refs ---
  const adjacency = new Map<ModuleKey, Set<ModuleKey>>();
  const externalRefs = new Map<ModuleKey, Set<ModuleKey>>(); // external → set of modules that need it
  const edgeFks = new Map<string, CrossModuleDep[]>(); // "from->to" → FKs
  let totalCrossModuleDeps = 0;

  for (const mod of modules) {
    if (!adjacency.has(mod.key)) adjacency.set(mod.key, new Set());

    for (const dep of mod.crossModuleDeps) {
      totalCrossModuleDeps++;
      const targetKey = moduleKey(dep.targetApp, dep.targetModule);
      const edgeKey = `${mod.key}->${targetKey}`;

      if (!edgeFks.has(edgeKey)) edgeFks.set(edgeKey, []);
      edgeFks.get(edgeKey)!.push(dep);

      if (!moduleMap.has(targetKey)) {
        // External dependency — not in the selected apps
        if (!externalRefs.has(targetKey)) externalRefs.set(targetKey, new Set());
        externalRefs.get(targetKey)!.add(mod.key);
      } else {
        // Internal dependency
        adjacency.get(mod.key)!.add(targetKey);
        if (!adjacency.has(targetKey)) adjacency.set(targetKey, new Set());
      }
    }
  }

  // --- Detect cycles via Tarjan SCC (shared lib) ---
  const internalKeys = [...moduleMap.keys()];
  const cycles = tarjanSCC(internalKeys, adjacency as Map<string, Set<string>>) as ModuleKey[][];

  if (cycles.length > 0) {
    for (const cycle of cycles) {
      warnings.push(
        `Circular dependency detected: ${cycle.join(' ↔ ')}. These modules will be placed in the same wave.`,
      );
    }
  }

  // --- Collapse cycles into single virtual nodes for Kahn's (shared lib) ---
  const { collapsedAdj, nodeToGroup, groupMembers } = collapseCycles(
    internalKeys,
    adjacency as Map<string, Set<string>>,
    cycles,
  );

  // --- Kahn's algorithm with level tracking ---
  const waves = kahnsWithLevels(
    collapsedAdj,
    nodeToGroup,
    groupMembers,
    moduleMap,
    externalRefs,
  );

  return {
    generatedAt: new Date().toISOString().split('T')[0],
    apps: selectedApps,
    totalModules: modules.length,
    totalCrossModuleDeps,
    waves,
    cycles,
    warnings,
  };
}

// ---------------------------------------------------------------------------
// Kahn's algorithm with level tracking → waves
// ---------------------------------------------------------------------------

// Note on key types: nodeToGroup / groupMembers come from the shared
// lib/graph.ts collapseCycles, which types node keys as plain `string` even
// though they really are "app/module" ModuleKey strings — so this function
// accepts the lib's string-keyed shapes (moduleMap is widened accordingly:
// every ModuleKey is a string, and the body only does string-keyed lookups).
function kahnsWithLevels(
  collapsedAdj: Map<string, Set<string>>,
  nodeToGroup: Map<string, string>,
  groupMembers: Map<string, string[]>,
  moduleMap: Map<string, ModuleInfo>,
  externalRefs: Map<ModuleKey, Set<ModuleKey>>,
): Wave[] {
  const waves: Wave[] = [];

  // --- Wave 0: external prerequisites ---
  if (externalRefs.size > 0) {
    const externalModules: WaveModule[] = [];
    for (const [extKey, referencedBy] of externalRefs) {
      const [appCode, moduleCode] = extKey.split('/');
      externalModules.push({
        appCode,
        moduleCode,
        key: extKey as ModuleKey,
        entityCount: 0,
        dependsOn: [],
        isExternal: true,
        referencedBy: [...referencedBy],
        warnings: [],
      });
    }
    // Sort for deterministic output
    externalModules.sort((a, b) => a.key.localeCompare(b.key));
    waves.push({ level: 0, modules: externalModules });
  }

  // --- Compute in-degrees for the collapsed graph ---
  // In-degree = number of DEPENDENCIES (things I depend on), not dependents.
  // collapsedAdj[A] = {B, C} means A depends on B and C → inDegree[A] = 2.
  const inDegree = new Map<string, number>();
  for (const [groupId, deps] of collapsedAdj) {
    inDegree.set(groupId, deps.size);
  }

  // Build reverse index: dependency → list of groups that depend on it.
  // When a dependency is resolved, we decrement in-degree of its dependents.
  const reverseDeps = new Map<string, string[]>();
  for (const [group, deps] of collapsedAdj) {
    for (const dep of deps) {
      if (!reverseDeps.has(dep)) reverseDeps.set(dep, []);
      reverseDeps.get(dep)!.push(group);
    }
  }

  // --- BFS level by level ---
  // Start with groups that have no dependencies (inDegree === 0).
  let queue: string[] = [];
  for (const [groupId, deg] of inDegree) {
    if (deg === 0) queue.push(groupId);
  }

  let level = 0;
  while (queue.length > 0) {
    level++;
    const nextQueue: string[] = [];
    const waveModules: WaveModule[] = [];

    for (const groupId of queue) {
      const members = groupMembers.get(groupId) ?? [];
      for (const memberKey of members) {
        const mod = moduleMap.get(memberKey);
        if (!mod) continue;

        // Collect the module's direct dependencies (internal only)
        const deps: ModuleKey[] = [];
        for (const dep of mod.crossModuleDeps) {
          const depKey = moduleKey(dep.targetApp, dep.targetModule) as ModuleKey;
          if (moduleMap.has(depKey)) {
            deps.push(depKey);
          }
        }

        const modWarnings: string[] = [];
        if (!mod.hasEntiteMd) {
          modWarnings.push('data model not yet generated');
        }

        waveModules.push({
          appCode: mod.appCode,
          moduleCode: mod.moduleCode,
          key: mod.key,
          entityCount: mod.entities.length,
          dependsOn: [...new Set(deps)],
          readiness: mod.readiness,
          isExternal: false,
          warnings: modWarnings,
        });
      }

      // Decrement in-degree of dependents (groups that depend on this group).
      const dependents = reverseDeps.get(groupId) ?? [];
      for (const dependent of dependents) {
        const newDeg = (inDegree.get(dependent) ?? 1) - 1;
        inDegree.set(dependent, newDeg);
        if (newDeg === 0) {
          nextQueue.push(dependent);
        }
      }
    }

    // Sort for deterministic output
    waveModules.sort((a, b) => a.key.localeCompare(b.key));
    if (waveModules.length > 0) {
      waves.push({ level, modules: waveModules });
    }

    queue = nextQueue;
  }

  return waves;
}
