/**
 * Change Detector
 *
 * Detects file changes between commits for incremental indexing.
 */

import path from 'path';
import { simpleGit, SimpleGit } from 'simple-git';
import type { ChangedFiles } from './types.js';
import { createLogger, type Logger } from '../utils/logger.js';

/**
 * Options for change detection
 */
export interface ChangeDetectorOptions {
  /** Repository root directory */
  repoRoot: string;
  /** File extensions to include (e.g., ['.ts', '.js']) */
  extensions?: string[];
  /** Logger instance */
  logger?: Logger;
}

/**
 * Change Detector
 *
 * Uses git diff to detect file changes between commits.
 */
export class ChangeDetector {
  private git: SimpleGit;
  private repoRoot: string;
  private extensions: string[];
  private logger: Logger;

  constructor(options: ChangeDetectorOptions) {
    this.repoRoot = options.repoRoot;
    this.git = simpleGit(this.repoRoot);
    this.extensions = options.extensions ?? [];
    this.logger = options.logger ?? createLogger({ name: 'change-detector' });
  }

  /**
   * Get changed files between two commits
   */
  async getChangedFiles(
    fromCommit: string,
    toCommit: string = 'HEAD'
  ): Promise<ChangedFiles> {
    this.logger.debug({ fromCommit, toCommit }, 'Detecting changes');

    const changes: ChangedFiles = {
      added: [],
      modified: [],
      deleted: [],
      renamedFrom: [],
      renamedTo: [],
    };

    try {
      // Get diff summary between commits
      const diff = await this.git.diffSummary([fromCommit, toCommit]);

      for (const file of diff.files) {
        const filePath = path.join(this.repoRoot, file.file);

        // Filter by extension if specified
        if (!this.shouldInclude(filePath)) {
          continue;
        }

        // Categorize by change type
        if (file.binary) {
          // Skip binary files
          continue;
        }

        // simple-git uses different property names
        const diffFile = file as any;

        if (diffFile.status === 'A' || diffFile.insertions > 0 && diffFile.deletions === 0 && !await this.fileExistsInCommit(file.file, fromCommit)) {
          changes.added.push(filePath);
        } else if (diffFile.status === 'D' || (diffFile.deletions > 0 && diffFile.insertions === 0 && !await this.fileExistsInCommit(file.file, toCommit))) {
          changes.deleted.push(filePath);
        } else if (diffFile.status === 'R') {
          // Renamed
          changes.renamedFrom.push(path.join(this.repoRoot, diffFile.from || file.file));
          changes.renamedTo.push(filePath);
        } else {
          // Modified
          changes.modified.push(filePath);
        }
      }

      this.logger.info(
        {
          added: changes.added.length,
          modified: changes.modified.length,
          deleted: changes.deleted.length,
          renamed: changes.renamedTo.length,
        },
        'Changes detected'
      );

      return changes;
    } catch (error) {
      this.logger.error({ error }, 'Failed to detect changes');
      throw error;
    }
  }

  /**
   * Get changed files since last indexed commit
   * Returns null if no previous index exists
   */
  async getChangedFilesSinceLastIndex(
    lastIndexedCommit?: string
  ): Promise<ChangedFiles | null> {
    if (!lastIndexedCommit) {
      this.logger.debug('No last indexed commit, full index required');
      return null;
    }

    // Verify the commit exists
    try {
      await this.git.revparse([lastIndexedCommit]);
    } catch {
      this.logger.warn({ commit: lastIndexedCommit }, 'Last indexed commit not found');
      return null;
    }

    // Check if we're already at the same commit
    const currentHead = await this.git.revparse(['HEAD']);
    if (currentHead.trim() === lastIndexedCommit) {
      this.logger.debug('Already at last indexed commit, no changes');
      return {
        added: [],
        modified: [],
        deleted: [],
        renamedFrom: [],
        renamedTo: [],
      };
    }

    return this.getChangedFiles(lastIndexedCommit, 'HEAD');
  }

  /**
   * Get all tracked files in the repository
   */
  async getAllTrackedFiles(): Promise<string[]> {
    const result = await this.git.raw(['ls-files']);
    const files = result
      .split('\n')
      .filter((f) => f.trim())
      .map((f) => path.join(this.repoRoot, f))
      .filter((f) => this.shouldInclude(f));

    return files;
  }

  /**
   * Get current HEAD commit SHA
   */
  async getCurrentCommit(): Promise<string> {
    const result = await this.git.revparse(['HEAD']);
    return result.trim();
  }

  /**
   * Check if a file should be included based on extensions
   */
  private shouldInclude(filePath: string): boolean {
    if (this.extensions.length === 0) {
      return true;
    }

    const ext = path.extname(filePath).toLowerCase();
    return this.extensions.some((e) => e.toLowerCase() === ext);
  }

  /**
   * Check if a file exists in a specific commit
   */
  private async fileExistsInCommit(filePath: string, commit: string): Promise<boolean> {
    try {
      await this.git.raw(['cat-file', '-e', `${commit}:${filePath}`]);
      return true;
    } catch {
      return false;
    }
  }
}

/**
 * Create a change detector for a repository
 */
export function createChangeDetector(options: ChangeDetectorOptions): ChangeDetector {
  return new ChangeDetector(options);
}
