/**
 * Ingestion Orchestrator
 *
 * Coordinates the code parsing and graph building pipeline.
 */

import path from 'path';
import { glob } from 'glob';
import pLimit from 'p-limit';
import type {
  FileEntities,
  CodebaseEntities,
  FileEntity,
  ClassEntity,
  FunctionEntity,
  ModuleEntity,
  ImportStatement,
} from '../graph/types.js';
import { GraphBuilder } from '../graph/builder.js';
import { KuzuConnection } from '../graph/connection.js';
import { ParserRegistry, getSupportedExtensions } from './parsers/registry.js';
import { ChangeDetector } from './change-detector.js';
import type {
  IngestionOptions,
  IngestionStats,
  IngestionProgress,
  ParseResult,
} from './types.js';
import { createLogger, type Logger } from '../utils/logger.js';

/**
 * Default ignore patterns
 */
const DEFAULT_IGNORE_PATTERNS = [
  '**/node_modules/**',
  '**/dist/**',
  '**/build/**',
  '**/.git/**',
  '**/coverage/**',
  '**/__pycache__/**',
  '**/.pytest_cache/**',
  '**/venv/**',
  '**/.venv/**',
  '**/env/**',
  '**/.env/**',
  '**/*.min.js',
  '**/*.bundle.js',
  '**/vendor/**',
];

/**
 * Ingestion Orchestrator
 *
 * Main coordinator for the code parsing pipeline:
 * 1. Scan repository for source files
 * 2. Parse files to extract entities
 * 3. Build graph from entities
 */
export class IngestionOrchestrator {
  private repoRoot: string;
  private options: Required<Omit<IngestionOptions, 'onProgress'>> & {
    onProgress?: (progress: IngestionProgress) => void;
  };
  private logger: Logger;
  private conn?: KuzuConnection;
  private lastIndexedCommit?: string;

  constructor(options: IngestionOptions) {
    this.repoRoot = path.resolve(options.repoRoot);
    this.options = {
      repoRoot: this.repoRoot,
      changedOnly: options.changedOnly ?? false,
      maxWorkers: options.maxWorkers ?? 4,
      ignorePatterns: options.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS,
      extensions: options.extensions ?? getSupportedExtensions(),
      onProgress: options.onProgress,
    };
    this.logger = createLogger({ name: 'ingestion-orchestrator' });
  }

  /**
   * Set the Kuzu connection for graph building
   */
  setConnection(conn: KuzuConnection): void {
    this.conn = conn;
  }

  /**
   * Set the last indexed commit for incremental indexing
   */
  setLastIndexedCommit(commit: string): void {
    this.lastIndexedCommit = commit;
  }

  /**
   * Run the full ingestion pipeline
   */
  async index(): Promise<IngestionStats> {
    const startTime = Date.now();
    this.logger.info({ repoRoot: this.repoRoot }, 'Starting ingestion');

    // Report progress
    this.reportProgress('scanning', 0, 0, 0);

    // Get files to process
    let filesToParse: string[];
    let filesToDelete: string[] = [];

    if (this.options.changedOnly && this.lastIndexedCommit) {
      const changeDetector = new ChangeDetector({
        repoRoot: this.repoRoot,
        extensions: this.options.extensions,
        logger: this.logger,
      });

      const changes = await changeDetector.getChangedFilesSinceLastIndex(
        this.lastIndexedCommit
      );

      if (changes === null) {
        // No previous index, do full scan
        filesToParse = await this.scanForFiles();
      } else if (
        changes.added.length === 0 &&
        changes.modified.length === 0 &&
        changes.deleted.length === 0
      ) {
        // No changes
        this.logger.info('No changes detected, skipping ingestion');
        return this.createStats(0, 0, 0, 0, 0, 0, Date.now() - startTime);
      } else {
        filesToParse = [...changes.added, ...changes.modified, ...changes.renamedTo];
        filesToDelete = [...changes.deleted, ...changes.renamedFrom];
      }
    } else {
      // Full scan
      filesToParse = await this.scanForFiles();
    }

    this.logger.info({ fileCount: filesToParse.length }, 'Files to parse');

    // Parse files
    this.reportProgress('parsing', 0, filesToParse.length, 0);
    const parseResults = await this.parseFiles(filesToParse);

    // Aggregate entities
    const entities = this.aggregateEntities(parseResults);

    // Build graph if connection is available
    if (this.conn) {
      this.reportProgress('building', filesToParse.length, filesToParse.length, 90);

      const builder = new GraphBuilder(this.conn, { logger: this.logger });

      // Delete removed files first
      if (filesToDelete.length > 0) {
        await builder.removeFiles(filesToDelete);
      }

      // Build graph
      if (this.options.changedOnly && filesToDelete.length === 0) {
        // Incremental update
        await builder.updateFiles(
          parseResults
            .filter((r) => r.success && r.entities)
            .map((r) => r.entities!)
        );
      } else {
        // Full rebuild or has deletions
        await builder.build(entities, { clearFirst: !this.options.changedOnly });
      }
    }

    // Calculate stats
    const successCount = parseResults.filter((r) => r.success).length;
    const failedCount = parseResults.filter((r) => !r.success).length;

    const stats = this.createStats(
      parseResults.length,
      successCount,
      failedCount,
      entities.classes.length,
      entities.functions.length,
      entities.imports.length,
      Date.now() - startTime
    );

    this.reportProgress('complete', filesToParse.length, filesToParse.length, 100);
    this.logger.info({ stats }, 'Ingestion complete');

    return stats;
  }

  /**
   * Parse files without building the graph
   */
  async parseOnly(): Promise<{
    results: ParseResult[];
    entities: CodebaseEntities;
  }> {
    const files = await this.scanForFiles();
    const results = await this.parseFiles(files);
    const entities = this.aggregateEntities(results);
    return { results, entities };
  }

  /**
   * Scan repository for source files
   */
  private async scanForFiles(): Promise<string[]> {
    this.logger.debug('Scanning for source files');

    const patterns = this.options.extensions.map((ext) => `**/*${ext}`);

    const files = await glob(patterns, {
      cwd: this.repoRoot,
      ignore: this.options.ignorePatterns,
      absolute: true,
      nodir: true,
    });

    // Filter to only supported files
    const supportedFiles = files.filter((f) => ParserRegistry.isSupported(f));

    this.logger.debug({ count: supportedFiles.length }, 'Found source files');
    return supportedFiles;
  }

  /**
   * Parse files in parallel
   */
  private async parseFiles(files: string[]): Promise<ParseResult[]> {
    const limit = pLimit(this.options.maxWorkers);
    const results: ParseResult[] = [];
    let processed = 0;

    const parseFile = async (filePath: string): Promise<ParseResult> => {
      const startTime = Date.now();

      try {
        const parser = ParserRegistry.getParser(filePath, this.repoRoot);
        if (!parser) {
          return {
            filePath,
            success: false,
            error: 'No parser available for file type',
            durationMs: Date.now() - startTime,
          };
        }

        const entities = await parser.parse();

        return {
          filePath,
          success: true,
          entities,
          durationMs: Date.now() - startTime,
        };
      } catch (error) {
        return {
          filePath,
          success: false,
          error: error instanceof Error ? error.message : 'Unknown error',
          durationMs: Date.now() - startTime,
        };
      }
    };

    const promises = files.map((file) =>
      limit(async () => {
        const result = await parseFile(file);
        results.push(result);
        processed++;

        // Report progress every 10% or every 10 files
        if (processed % Math.max(1, Math.floor(files.length / 10)) === 0 || processed % 10 === 0) {
          const percent = Math.round((processed / files.length) * 80) + 10; // 10-90%
          this.reportProgress('parsing', processed, files.length, percent, file);
        }

        return result;
      })
    );

    await Promise.all(promises);
    return results;
  }

  /**
   * Aggregate parse results into codebase entities
   */
  private aggregateEntities(results: ParseResult[]): CodebaseEntities {
    const files: FileEntity[] = [];
    const classes: ClassEntity[] = [];
    const functions: FunctionEntity[] = [];
    const modules: ModuleEntity[] = [];
    const imports: ImportStatement[] = [];

    const moduleSet = new Set<string>();

    for (const result of results) {
      if (!result.success || !result.entities) continue;

      const e = result.entities;

      if (e.file) {
        files.push(e.file);
      }
      classes.push(...e.classes);
      functions.push(...e.functions);
      imports.push(...e.imports);

      // Deduplicate modules
      for (const mod of e.modules) {
        if (!moduleSet.has(mod.name)) {
          moduleSet.add(mod.name);
          modules.push(mod);
        }
      }
    }

    return { files, classes, functions, modules, imports };
  }

  /**
   * Create ingestion stats
   */
  private createStats(
    filesProcessed: number,
    filesSucceeded: number,
    filesFailed: number,
    classesFound: number,
    functionsFound: number,
    importsFound: number,
    durationMs: number
  ): IngestionStats {
    return {
      filesProcessed,
      filesSucceeded,
      filesFailed,
      classesFound,
      functionsFound,
      importsFound,
      modulesFound: 0, // TODO: track unique modules
      durationMs,
    };
  }

  /**
   * Report progress to callback
   */
  private reportProgress(
    phase: IngestionProgress['phase'],
    filesProcessed: number,
    totalFiles: number,
    percent: number,
    currentFile?: string
  ): void {
    if (this.options.onProgress) {
      this.options.onProgress({
        phase,
        filesProcessed,
        totalFiles,
        percent,
        currentFile,
      });
    }
  }
}

/**
 * Create an ingestion orchestrator
 */
export function createOrchestrator(options: IngestionOptions): IngestionOrchestrator {
  return new IngestionOrchestrator(options);
}

/**
 * Run ingestion on a repository
 */
export async function ingestRepository(
  repoRoot: string,
  conn?: KuzuConnection,
  options?: Partial<IngestionOptions>
): Promise<IngestionStats> {
  const orchestrator = new IngestionOrchestrator({
    repoRoot,
    ...options,
  });

  if (conn) {
    orchestrator.setConnection(conn);
  }

  return orchestrator.index();
}
