// services/diff-processing.service.js
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const database = require('../config/database');
const GitRepoService = require('./git-repo.service');

class DiffProcessingService {
  constructor() {
    this.gitService = new GitRepoService();
    this.baseDir = process.env.ATTACHED_REPOS_DIR || '/tmp/attached-repos';
    // Allow overriding diff storage root via env; fallback to <ATTACHED_REPOS_DIR>/diffs
    const envDiffDir = process.env.DIFF_STORAGE_DIR && process.env.DIFF_STORAGE_DIR.trim().length > 0
      ? process.env.DIFF_STORAGE_DIR
      : null;
    this.diffStorageDir = envDiffDir || path.join(this.baseDir, 'diffs');
    
    // Size threshold: <= 50KB store in DB; > 50KB store on disk
    this.SIZE_THRESHOLDS = {
      SMALL: 50 * 1024
    };
    
    this.ensureDiffStorageDir();
  }

  ensureDiffStorageDir() {
    if (!fs.existsSync(this.diffStorageDir)) {
      fs.mkdirSync(this.diffStorageDir, { recursive: true });
    }
  }

  // Main method to process diffs for a commit
  async processCommitDiffs(commitId, repositoryId, repoPath, fromSha, toSha) {
    console.log(`🔄 Processing diffs for commit ${commitId}`);
    console.log(`📁 Repository path: ${repoPath}`);
    console.log(`📝 From SHA: ${fromSha}`);
    console.log(`📝 To SHA: ${toSha}`);
    
    try {
      // Verify repository path exists
      const fs = require('fs');
      if (!fs.existsSync(repoPath)) {
        console.error(`❌ Repository path does not exist: ${repoPath}`);
        return { success: false, error: 'Repository path does not exist' };
      }
      
      // Verify .git directory exists
      const gitPath = require('path').join(repoPath, '.git');
      if (!fs.existsSync(gitPath)) {
        console.error(`❌ Git directory does not exist: ${gitPath}`);
        return { success: false, error: 'Git directory does not exist' };
      }
      
      console.log(`📝 Getting diff content from git...`);
      
      // Get the diff content
      const diffContent = await this.gitService.getDiff(repoPath, fromSha, toSha);
      
      console.log(`📝 Diff content length: ${diffContent ? diffContent.length : 0} characters`);
      
      if (!diffContent || diffContent.trim().length === 0) {
        console.log(`⚠️  No diff content found for commit ${commitId}`);
        return { success: false, reason: 'No diff content' };
      }

      // Parse the diff to extract individual file changes
      const fileDiffs = this.parseDiffContent(diffContent);
      
      console.log(`📄 Found ${fileDiffs.length} file changes in diff`);

      if (fileDiffs.length === 0) {
        console.log(`⚠️  No file changes found in diff`);
        return { success: false, reason: 'No file changes found' };
      }

      // Process each file diff
      const results = [];
      for (const fileDiff of fileDiffs) {
        try {
          console.log(`📄 Processing diff for file: ${fileDiff.filePath}`);
          const result = await this.processFileDiff(commitId, fileDiff);
          results.push(result);
        } catch (error) {
          console.error(`❌ Failed to process diff for file ${fileDiff.filePath}:`, error.message);
          console.error(`❌ Error stack:`, error.stack);
          results.push({
            success: false,
            filePath: fileDiff.filePath,
            error: error.message
          });
        }
      }

      // Update processing statistics
      await this.updateDiffStatistics(repositoryId, results);

      return {
        success: true,
        processedFiles: results.length,
        results: results
      };

    } catch (error) {
      console.error(`❌ Failed to process commit diffs:`, error.message);
      console.error(`❌ Error stack:`, error.stack);
      return { success: false, error: error.message };
    }
  }

  // Parse diff content into individual file diffs
  parseDiffContent(diffContent) {
    const fileDiffs = [];
    const lines = diffContent.split('\n');
    
    let currentFileDiff = null;
    let currentDiffBody = [];
    
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      
      // Check for file header (diff --git)
      if (line.startsWith('diff --git')) {
        // Save previous file diff if exists
        if (currentFileDiff) {
          currentFileDiff.diffBody = currentDiffBody.join('\n');
          // Infer accurate change type and canonical paths from diff body
          this.inferChangeTypeAndPaths(currentFileDiff);
          fileDiffs.push(currentFileDiff);
        }
        
        // Start new file diff
        currentFileDiff = this.parseFileHeader(line);
        currentDiffBody = [line];
      } else if (currentFileDiff) {
        currentDiffBody.push(line);
      }
    }
    
    // Don't forget the last file diff
    if (currentFileDiff) {
      currentFileDiff.diffBody = currentDiffBody.join('\n');
      this.inferChangeTypeAndPaths(currentFileDiff);
      fileDiffs.push(currentFileDiff);
    }
    
    return fileDiffs;
  }

  // Parse file header from diff line
  parseFileHeader(diffLine) {
    // Example: diff --git a/src/app.js b/src/app.js
    const match = diffLine.match(/diff --git a\/(.+) b\/(.+)/);
    if (!match) {
      throw new Error(`Invalid diff header: ${diffLine}`);
    }
    
    const oldPath = match[1];
    const newPath = match[2];
    
    // Determine change type
    let changeType = 'modified';
    if (oldPath === '/dev/null') {
      changeType = 'added';
    } else if (newPath === '/dev/null') {
      changeType = 'deleted';
    } else if (oldPath !== newPath) {
      changeType = 'renamed';
    }
    
    return {
      filePath: newPath === '/dev/null' ? oldPath : newPath,
      oldPath: oldPath === '/dev/null' ? null : oldPath,
      newPath: newPath === '/dev/null' ? null : newPath,
      changeType: changeType,
      diffHeader: diffLine
    };
  }

  // Inspect the diff body to accurately infer change type and canonical paths
  inferChangeTypeAndPaths(fileDiff) {
    if (!fileDiff || !fileDiff.diffBody) return;

    const lines = fileDiff.diffBody.split('\n');
    let headerOld = null;
    let headerNew = null;
    let sawNewFile = false;
    let sawDeletedFile = false;
    let sawRenameFrom = null;
    let sawRenameTo = null;

    for (const l of lines) {
      if (l.startsWith('new file mode')) sawNewFile = true;
      if (l.startsWith('deleted file mode')) sawDeletedFile = true;
      if (l.startsWith('rename from ')) sawRenameFrom = l.replace('rename from ', '').trim();
      if (l.startsWith('rename to ')) sawRenameTo = l.replace('rename to ', '').trim();
      if (l.startsWith('--- ')) headerOld = l.substring(4).trim(); // e.g., 'a/path' or '/dev/null'
      if (l.startsWith('+++ ')) headerNew = l.substring(4).trim(); // e.g., 'b/path' or '/dev/null'
      // Stop early after we've collected the header markers
      if (headerOld && headerNew && (sawNewFile || sawDeletedFile || sawRenameFrom || sawRenameTo)) break;
    }

    // Normalize paths like 'a/path' or 'b/path'
    const normalize = (p) => {
      if (!p) return null;
      if (p === '/dev/null') return '/dev/null';
      // strip leading a/ or b/
      return p.replace(/^a\//, '').replace(/^b\//, '');
    };

    const oldPath = normalize(headerOld);
    const newPath = normalize(headerNew);

    // Decide change type priority: rename > added > deleted > modified
    if (sawRenameFrom || sawRenameTo) {
      fileDiff.changeType = 'renamed';
      fileDiff.oldPath = sawRenameFrom || oldPath || fileDiff.oldPath || null;
      fileDiff.newPath = sawRenameTo || newPath || fileDiff.newPath || null;
      fileDiff.filePath = fileDiff.newPath || fileDiff.filePath;
      return;
    }

    if (sawNewFile || oldPath === '/dev/null') {
      fileDiff.changeType = 'added';
      fileDiff.oldPath = null;
      fileDiff.newPath = newPath || fileDiff.newPath || fileDiff.filePath;
      fileDiff.filePath = fileDiff.newPath;
      return;
    }

    if (sawDeletedFile || newPath === '/dev/null') {
      fileDiff.changeType = 'deleted';
      fileDiff.newPath = null;
      fileDiff.oldPath = oldPath || fileDiff.oldPath || fileDiff.filePath;
      fileDiff.filePath = fileDiff.oldPath;
      return;
    }

    // Default to modified; refine filePath if headers present
    fileDiff.changeType = 'modified';
    if (newPath && newPath !== '/dev/null') {
      fileDiff.filePath = newPath;
    }
    if (!fileDiff.oldPath && oldPath && oldPath !== '/dev/null') fileDiff.oldPath = oldPath;
    if (!fileDiff.newPath && newPath && newPath !== '/dev/null') fileDiff.newPath = newPath;
  }

  // Process individual file diff
  async processFileDiff(commitId, fileDiff) {
    const diffSize = Buffer.byteLength(fileDiff.diffBody, 'utf8');
    const storageType = 'external';
    
    console.log(`📊 File ${fileDiff.filePath}: ${diffSize} bytes, storage: ${storageType}`);
    
    // Get file change record
    const fileChangeQuery = `
      SELECT rcf.id 
      FROM repository_commit_files rcf
      JOIN repository_commit_details rcd ON rcf.commit_id = rcd.id
      WHERE rcd.id = $1 AND rcf.file_path = $2
    `;
    
    const fileChangeResult = await database.query(fileChangeQuery, [commitId, fileDiff.filePath]);
    
    if (fileChangeResult.rows.length === 0) {
      throw new Error(`File change record not found for ${fileDiff.filePath}`);
    }
    
    const fileChangeId = fileChangeResult.rows[0].id;
    
    // Store diff based on size
    const diffContentId = await this.storeExternally(commitId, fileChangeId, fileDiff, diffSize);
    
    return {
      success: true,
      filePath: fileDiff.filePath,
      changeType: fileDiff.changeType,
      diffSize: diffSize,
      storageType: storageType,
      diffContentId: diffContentId
    };
  }

  // Determine storage type (always external)
  determineStorageType(size) { return 'external'; }

  // Removed DB storage path (all diffs stored externally)

  // Remove compression path; no longer used

  // Store large diff externally
  async storeExternally(commitId, fileChangeId, fileDiff, diffSize) {
    // Create directory structure: diffs/repo_id/commit_sha/
    const commitDir = path.join(this.diffStorageDir, commitId);
    if (!fs.existsSync(commitDir)) {
      fs.mkdirSync(commitDir, { recursive: true });
    }
    
    // Create safe filename
    const safeFileName = fileDiff.filePath.replace(/[^a-zA-Z0-9._-]/g, '_') + '.diff';
    const filePath = path.join(commitDir, safeFileName);
    
    // Write diff to file
    fs.writeFileSync(filePath, fileDiff.diffBody, 'utf8');
    
    const query = `
      INSERT INTO diff_contents (
        commit_id, file_change_id, diff_header, diff_size_bytes,
        storage_type, external_storage_path, external_storage_provider,
        file_path, change_type, processing_status
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
      RETURNING id
    `;
    
    const result = await database.query(query, [
      commitId, fileChangeId, fileDiff.diffHeader, diffSize,
      'external', filePath, 'local',
      fileDiff.filePath, fileDiff.changeType, 'processed'
    ]);
    
    return result.rows[0].id;
  }

  // Update diff processing statistics
  async updateDiffStatistics(repositoryId, results) {
    const successful = results.filter(r => r.success);
    const failed = results.filter(r => !r.success);
    
    const totalSize = successful.reduce((sum, r) => sum + (r.diffSize || 0), 0);
    const avgSize = successful.length > 0 ? totalSize / successful.length : 0;
    const externalCount = successful.filter(r => r.storageType === 'external').length;
    
    const query = `
      INSERT INTO diff_statistics (
        repository_id, period_start, period_end,
        total_commits, total_files_changed, total_diffs_processed,
        total_diff_size_bytes, avg_diff_size_bytes, max_diff_size_bytes,
        diffs_stored_external, failed_processing_count
      ) VALUES ($1, NOW() - INTERVAL '1 hour', NOW(), 
        1, $2, $3, $4, $5, $6, $7, $8)
    `;
    
    await database.query(query, [
      repositoryId, successful.length, successful.length,
      totalSize, avgSize, Math.max(...successful.map(r => r.diffSize || 0), 0),
      externalCount, failed.length
    ]);
  }

  // Queue diff processing for background processing
  async queueDiffProcessing(commitId, repositoryId, repoPath, fromSha, toSha, priority = 0) {
    const query = `
      INSERT INTO diff_processing_queue (
        commit_id, repository_id, from_sha, to_sha, repo_local_path, priority
      ) VALUES ($1, $2, $3, $4, $5, $6)
      RETURNING id
    `;
    
    const result = await database.query(query, [
      commitId, repositoryId, fromSha, toSha, repoPath, priority
    ]);
    
    return result.rows[0].id;
  }

  // Get diff content for retrieval
  async getDiffContent(diffContentId) {
    const query = `
      SELECT storage_type, external_storage_path, file_path, change_type
      FROM diff_contents 
      WHERE id = $1
    `;
    
    const result = await database.query(query, [diffContentId]);
    
    if (result.rows.length === 0) {
      throw new Error(`Diff content not found: ${diffContentId}`);
    }
    
    const diff = result.rows[0];
    
    // We only support external storage now
    return fs.readFileSync(diff.external_storage_path, 'utf8');
  }

  // Get diffs for a commit
  async getCommitDiffs(commitId) {
    const query = `
      SELECT dc.*, rcf.change_type as file_change_type
      FROM diff_contents dc
      JOIN repository_commit_files rcf ON dc.file_change_id = rcf.id
      WHERE dc.commit_id = $1
      ORDER BY dc.file_path
    `;
    
    const result = await database.query(query, [commitId]);
    return result.rows;
  }

  // Get repository diff statistics
  async getRepositoryDiffStats(repositoryId, daysBack = 30) {
    const query = `
      SELECT * FROM get_repository_diff_stats($1, $2)
    `;
    
    const result = await database.query(query, [repositoryId, daysBack]);
    return result.rows[0];
  }

  // Clean up old external diff files
  async cleanupOldDiffs(daysBack = 30) {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - daysBack);
    
    // Get old external diffs
    const query = `
      SELECT external_storage_path 
      FROM diff_contents 
      WHERE storage_type = 'external' 
        AND created_at < $1
    `;
    
    const result = await database.query(query, [cutoffDate]);
    
    // Delete files
    let deletedCount = 0;
    for (const row of result.rows) {
      try {
        if (fs.existsSync(row.external_storage_path)) {
          fs.unlinkSync(row.external_storage_path);
          deletedCount++;
        }
      } catch (error) {
        console.warn(`Failed to delete diff file ${row.external_storage_path}:`, error.message);
      }
    }
    
    // Delete database records
    const deleteQuery = `
      DELETE FROM diff_contents 
      WHERE storage_type = 'external' 
        AND created_at < $1
    `;
    
    await database.query(deleteQuery, [cutoffDate]);
    
    return deletedCount;
  }
}

module.exports = DiffProcessingService;
