/**
 * Module Graph — Build a project-wide dependency graph from import/export statements.
 *
 * Provides forward edges (depends-on) and reverse edges (imported-by) for
 * cross-file analysis. Detects circular imports via DFS.
 */

import type { ScanFile } from '../shared/types'
import {
  extractAllImports,
  extractExports,
  resolveImportPath,
  buildFileIndex,
} from './import-resolver'
import type { ParsedImport, ParsedExport } from './import-resolver'

// ============================================================================
// Types
// ============================================================================

export interface ResolvedImport extends ParsedImport {
  resolvedPath: string | null   // null if unresolved (node_modules, missing)
}

export interface ModuleNode {
  filePath: string
  imports: ResolvedImport[]
  exports: ParsedExport[]
  dependsOn: Set<string>         // forward edges (resolved file paths)
  importedBy: Set<string>        // reverse edges
}

export interface ModuleGraphStats {
  totalFiles: number
  totalEdges: number
  circularImportCount: number
  duration: number
}

export interface ModuleGraph {
  nodes: Map<string, ModuleNode>
  circularImports: Set<string>    // files participating in circular deps
  stats: ModuleGraphStats
}

// ============================================================================
// Graph Construction
// ============================================================================

const CODE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py'])

function isCodeFile(filePath: string): boolean {
  const ext = filePath.substring(filePath.lastIndexOf('.'))
  return CODE_EXTENSIONS.has(ext.toLowerCase())
}

/**
 * Build a project-wide module graph from scanned files.
 *
 * For each code file: extract imports, resolve paths, extract exports.
 * Build forward (dependsOn) and reverse (importedBy) edges.
 * Detect circular imports via DFS.
 */
export function buildModuleGraph(files: ScanFile[]): ModuleGraph {
  const startTime = Date.now()
  const fileIndex = buildFileIndex(files)
  const nodes = new Map<string, ModuleNode>()
  let totalEdges = 0

  // Phase 1: Build nodes with imports and exports
  for (const file of files) {
    if (!isCodeFile(file.path)) continue

    const imports = extractAllImports(file.content, file.path)
    const fileExports = extractExports(file.content, file.path)

    const resolvedImports: ResolvedImport[] = imports
      .filter(imp => imp.isLocal)
      .map(imp => {
        const resolved = resolveImportPath(imp.importPath, file.path, fileIndex)
        return {
          ...imp,
          resolvedPath: resolved?.path ?? null,
        }
      })

    const dependsOn = new Set<string>()
    for (const ri of resolvedImports) {
      if (ri.resolvedPath) {
        dependsOn.add(ri.resolvedPath)
        totalEdges++
      }
    }

    nodes.set(file.path, {
      filePath: file.path,
      imports: resolvedImports,
      exports: fileExports,
      dependsOn,
      importedBy: new Set(),
    })
  }

  // Phase 2: Build reverse edges
  for (const [filePath, node] of nodes) {
    for (const dep of node.dependsOn) {
      const depNode = nodes.get(dep)
      if (depNode) {
        depNode.importedBy.add(filePath)
      }
    }
  }

  // Phase 3: Detect circular imports via DFS
  const circularImports = detectCircularImports(nodes)

  return {
    nodes,
    circularImports,
    stats: {
      totalFiles: nodes.size,
      totalEdges,
      circularImportCount: circularImports.size,
      duration: Date.now() - startTime,
    },
  }
}

// ============================================================================
// Symbol Resolution
// ============================================================================

/**
 * Resolve what file and export an imported symbol refers to.
 * Follows one-hop re-exports through barrel files.
 */
export function resolveImportedName(
  graph: ModuleGraph,
  fromFile: string,
  importedName: string
): { filePath: string; exportName: string } | null {
  const node = graph.nodes.get(fromFile)
  if (!node) return null

  // Find the import that provides this name
  for (const imp of node.imports) {
    if (!imp.resolvedPath) continue

    // Check if this import provides the requested name
    const providesName = imp.importedNames.includes(importedName) ||
      (imp.isNamespace && imp.importedNames[0] === importedName) ||
      (importedName === 'default' && imp.importedNames.includes('default'))

    if (!providesName) continue

    const targetNode = graph.nodes.get(imp.resolvedPath)
    if (!targetNode) return { filePath: imp.resolvedPath, exportName: importedName }

    // Check if target directly exports this name
    const directExport = targetNode.exports.find(e =>
      e.name === importedName || (importedName === 'default' && e.isDefault)
    )

    if (directExport) {
      // If it's a re-export, follow one hop
      if (directExport.kind === 'reexport' && directExport.sourceModule) {
        const reExportTarget = findReExportTarget(graph, imp.resolvedPath, directExport)
        if (reExportTarget) return reExportTarget
      }
      return { filePath: imp.resolvedPath, exportName: directExport.name }
    }

    // Target might be a barrel file — check re-exports
    for (const exp of targetNode.exports) {
      if (exp.kind === 'reexport' && exp.name === importedName && exp.sourceModule) {
        const reExportTarget = findReExportTarget(graph, imp.resolvedPath, exp)
        if (reExportTarget) return reExportTarget
      }
    }

    // Name not found in exports but import resolved — return best guess
    return { filePath: imp.resolvedPath, exportName: importedName }
  }

  return null
}

function findReExportTarget(
  graph: ModuleGraph,
  fromFile: string,
  exp: ParsedExport
): { filePath: string; exportName: string } | null {
  if (!exp.sourceModule) return null

  const fromNode = graph.nodes.get(fromFile)
  if (!fromNode) return null

  // Find the resolved import matching the re-export source
  for (const imp of fromNode.imports) {
    if (imp.importPath === exp.sourceModule && imp.resolvedPath) {
      return { filePath: imp.resolvedPath, exportName: exp.name }
    }
  }

  // If we have a re-export import in the node's import list
  for (const imp of fromNode.imports) {
    if (imp.isReExport && imp.importPath === exp.sourceModule && imp.resolvedPath) {
      return { filePath: imp.resolvedPath, exportName: exp.name }
    }
  }

  return null
}

// ============================================================================
// Circular Import Detection
// ============================================================================

function detectCircularImports(nodes: Map<string, ModuleNode>): Set<string> {
  const circular = new Set<string>()
  const visited = new Set<string>()
  const inStack = new Set<string>()

  function dfs(filePath: string): void {
    if (inStack.has(filePath)) {
      circular.add(filePath)
      return
    }
    if (visited.has(filePath)) return

    visited.add(filePath)
    inStack.add(filePath)

    const node = nodes.get(filePath)
    if (node) {
      for (const dep of node.dependsOn) {
        if (inStack.has(dep)) {
          circular.add(filePath)
          circular.add(dep)
        } else {
          dfs(dep)
        }
      }
    }

    inStack.delete(filePath)
  }

  for (const filePath of nodes.keys()) {
    dfs(filePath)
  }

  return circular
}
