// services/vcs-webhook.service.js
const database = require('../config/database');
const providerRegistry = require('./provider-registry');
const DiffProcessingService = require('./diff-processing.service');

class VcsWebhookService {
  constructor() {
    this._schemaChecked = false;
    this._webhookEventColumns = new Map();
    this.diffService = new DiffProcessingService();
  }

  // Process webhook events for any VCS provider
  async processWebhookEvent(providerKey, eventType, payload) {
    console.log(`Processing ${providerKey} webhook event: ${eventType}`);
    
    try {
      switch (eventType) {
        case 'push':
          await this.handlePushEvent(providerKey, payload);
          break;
        case 'pull_request':
        case 'merge_request':
          await this.handlePullRequestEvent(providerKey, payload);
          break;
        case 'repository':
          await this.handleRepositoryEvent(providerKey, payload);
          break;
        case 'ping':
          await this.handlePingEvent(providerKey, payload);
          break;
        default:
          console.log(`Unhandled webhook event type: ${eventType} for provider: ${providerKey}`);
      }
    } catch (error) {
      console.error(`Error processing ${providerKey} webhook event ${eventType}:`, error);
      throw error;
    }
  }

  // Handle push events for any provider
  async handlePushEvent(providerKey, payload) {
       let { repository, project, ref, commits, pusher, user } = payload;

    // Normalize Bitbucket push payload structure to the common fields
    if (providerKey === 'bitbucket' && payload && payload.push && Array.isArray(payload.push.changes)) {
      try {
        const firstChange = payload.push.changes[0] || {};
        const newRef = firstChange.new || {};
        const oldRef = firstChange.old || {};
        // Branch name
        const branchName = newRef.name || oldRef.name || null;
        // Compose a git-like ref
        ref = branchName ? `refs/heads/${branchName}` : ref;
        // Aggregate commits across changes
        const allCommits = [];
        for (const change of payload.push.changes) {
          if (Array.isArray(change.commits)) {
            allCommits.push(...change.commits);
          }
        }
        commits = allCommits;
        // Surface before/after hashes for persistence
        payload.before = oldRef.target?.hash || payload.before || null;
        payload.after = newRef.target?.hash || payload.after || null;
        // Bitbucket sets repository at top-level; keep as-is if present
      } catch (_) {}
    }
    
    // Build a provider-normalized repo object for extraction
    let repoForExtraction = repository || {};
    if (providerKey === 'gitlab') {
      // GitLab push payload includes repository (limited fields) and project (full namespace)
      // Prefer project.path_with_namespace when available
      repoForExtraction = {
        path_with_namespace: project?.path_with_namespace || repository?.path_with_namespace,
        full_name: project?.path_with_namespace || repository?.name,
        default_branch: project?.default_branch || repository?.default_branch
      };
    }
    
    // Extract provider-specific data
    const repoData = this.extractRepositoryData(providerKey, repoForExtraction);
    const commitData = this.extractCommitData(providerKey, commits);
    const branchFromRef = this.extractBranchFromRef(providerKey, ref, repoForExtraction);
    
    console.log(`Push event received for ${repoData.full_name} on ${branchFromRef}`);
    console.log(`Pusher: ${pusher?.name || user?.name || 'Unknown'}, Commits: ${commitData.length}`);

    // Persist raw webhook and commit SHAs
    try {
      // Find repository_id in our DB if attached
      const repoLookup = await database.query(
        'SELECT id FROM all_repositories WHERE owner_name = $1 AND repository_name = $2 ORDER BY created_at DESC LIMIT 1',
        [repoData.owner, repoData.name]
      );
      const repoId = repoLookup.rows[0]?.id || null;

      // Insert into provider-specific webhooks table
      await this.insertWebhookEvent(providerKey, {
        delivery_id: payload.delivery_id || payload.object_attributes?.id || null,
        event_type: 'push',
        action: null,
        owner_name: repoData.owner,
        repository_name: repoData.name,
        repository_id: repoId,
        ref: ref,
        before_sha: payload.before || null,
        after_sha: payload.after || null,
        commit_count: commitData.length,
        payload: JSON.stringify(payload)
      });

      if (repoId) {
     

        // Persist per-commit details and file paths
        if (commitData.length > 0) {
          for (const commit of commitData) {
            try {
              const commitInsert = await database.query(
                `INSERT INTO repository_commit_details (repository_id, commit_sha, author_name, author_email, message, url)
                 VALUES ($1, $2, $3, $4, $5, $6)
                 ON CONFLICT (repository_id, commit_sha) DO UPDATE SET
                   author_name = EXCLUDED.author_name,
                   author_email = EXCLUDED.author_email,
                   message = EXCLUDED.message,
                   url = EXCLUDED.url
                 RETURNING id`,
                [
                  repoId,
                  commit.id,
                  commit.author?.name || null,
                  commit.author?.email || null,
                  commit.message || null,
                  commit.url || null
                ]
              );
              const commitId = commitInsert.rows[0].id;
               // For Bitbucket, we'll skip file change insertion during webhook processing
              // since the webhook doesn't include file changes. The background sync will
              // handle this properly by fetching the changes from git directly.
              if (providerKey !== 'bitbucket') {
                // For other providers (GitHub, GitLab, Gitea), use the webhook data
                const addFiles = (paths = [], changeType) => paths.forEach(async (p) => {
                  try {
                    await database.query(
                      `INSERT INTO repository_commit_files (commit_id, change_type, file_path)
                       VALUES ($1, $2, $3)`,
                      [commitId, changeType, p]
                    );
                  } catch (_) {}
                });
                addFiles(commit.added || [], 'added');
                addFiles(commit.modified || [], 'modified');
                addFiles(commit.removed || [], 'removed');
              }
            } catch (commitErr) {
              console.warn('Failed to persist commit details:', commitErr.message);
            }
          }
        }

        // Kick off background re-sync
        setImmediate(async () => {
          try {
            const provider = providerRegistry.resolve(providerKey);
            
            // Mark syncing
            await database.query(
              'UPDATE all_repositories SET sync_status = $1, updated_at = NOW() WHERE id = $2',
              ['syncing', repoId]
            );

            // Clean existing storage then git-sync and re-index
            await provider.cleanupRepositoryStorage(repoId);
            const downloadResult = await provider.syncRepositoryWithFallback(
              repoData.owner,
              repoData.name,
              branchFromRef,
              repoId
            );

               // Process diffs for each commit after successful sync
               if (downloadResult.success && downloadResult.targetDir) {
                await this.processCommitDiffs(repoId, commitData, downloadResult.targetDir, providerKey);
              }
            await database.query(
              'UPDATE all_repositories SET sync_status = $1, last_synced_at = NOW(), updated_at = NOW() WHERE id = $2',
              [downloadResult.success ? 'synced' : 'error', repoId]
            );
          } catch (syncErr) {
            console.warn('Auto-sync failed:', syncErr.message);
            try {
              await database.query(
                'UPDATE all_repositories SET sync_status = $1, updated_at = NOW() WHERE id = $2',
                ['error', repoId]
              );
            } catch (_) {}
          }
        });
      }
    } catch (e) {
      console.warn('Failed to persist push webhook details:', e.message);
    }
  }

  // Extract repository data based on provider
  extractRepositoryData(providerKey, repository) {
    switch (providerKey) {
      case 'github':
        return {
          owner: repository.owner.login,
          name: repository.name,
          full_name: repository.full_name
        };
      case 'gitlab':
        {
          const ns = repository?.path_with_namespace || repository?.full_name || '';
          const parts = typeof ns === 'string' ? ns.split('/') : [];
          return {
            owner: parts[0] || null,
            name: parts[1] || repository?.name || null,
            full_name: ns || [parts[0], parts[1]].filter(Boolean).join('/')
          };
        }
      case 'bitbucket':
        return {
          owner: repository.full_name.split('/')[0],
          name: repository.full_name.split('/')[1],
          full_name: repository.full_name
        };
      case 'gitea':
        return {
          owner: repository.full_name.split('/')[0],
          name: repository.full_name.split('/')[1],
          full_name: repository.full_name
        };
      default:
        return { owner: 'unknown', name: 'unknown', full_name: 'unknown/unknown' };
    }
  }

  // Extract commit data based on provider
  extractCommitData(providerKey, commits) {
    if (!Array.isArray(commits)) return [];
    
    switch (providerKey) {
      case 'github':
        return commits.map(commit => ({
          id: commit.id,
          author: commit.author,
          message: commit.message,
          url: commit.url,
          added: commit.added || [],
          modified: commit.modified || [],
          removed: commit.removed || []
        }));
      case 'gitlab':
        return commits.map(commit => ({
          id: commit.id,
          author: {
            name: commit.author?.name,
            email: commit.author?.email
          },
          message: commit.message,
          url: commit.url,
          added: commit.added || [],
          modified: commit.modified || [],
          removed: commit.removed || []
        }));
      case 'bitbucket':
        return commits.map(commit => ({
          id: commit.hash,
          author: {
            name: commit.author?.user?.display_name,
            email: commit.author?.user?.email_address
          },
          message: commit.message,
          url: commit.links?.html?.href,
           // Bitbucket webhook doesn't include file changes in commit objects
          // We'll fetch these from git directly during processing
          added: [],
          modified: [],
          removed: []
        }));
      case 'gitea':
        return commits.map(commit => ({
          id: commit.id,
          author: {
            name: commit.author?.name,
            email: commit.author?.email
          },
          message: commit.message,
          url: commit.url,
          added: commit.added || [],
          modified: commit.modified || [],
          removed: commit.removed || []
        }));
      default:
        return [];
    }
  }

  // Extract branch from ref based on provider
  extractBranchFromRef(providerKey, ref, repository) {
    if (!ref) return repository?.default_branch || 'main';
    
    switch (providerKey) {
      case 'github':
      case 'gitlab':
      case 'gitea':
        return ref.startsWith('refs/heads/') ? ref.replace('refs/heads/', '') : ref;
      case 'bitbucket':
        return ref.startsWith('refs/heads/') ? ref.replace('refs/heads/', '') : ref;
      default:
        return 'main';
    }
  }

  // Insert webhook event into provider-specific table
  async insertWebhookEvent(providerKey, eventData) {
    const tableName = `${providerKey}_webhooks`;
    const query = `
      INSERT INTO ${tableName} (delivery_id, event_type, action, owner_name, repository_name, repository_id, ref, before_sha, after_sha, commit_count, payload)
      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
    `;
    
    await database.query(query, [
      eventData.delivery_id,
      eventData.event_type,
      eventData.action,
      eventData.owner_name,
      eventData.repository_name,
      eventData.repository_id,
      eventData.ref,
      eventData.before_sha,
      eventData.after_sha,
      eventData.commit_count,
      eventData.payload
    ]);
  }

  // Handle pull/merge request events
  async handlePullRequestEvent(providerKey, payload) {
    const { action, pull_request, merge_request } = payload;
    const pr = pull_request || merge_request;
    const repository = payload.repository;
    
    console.log(`Pull/Merge request ${action} for ${repository?.full_name || repository?.path_with_namespace}: #${pr?.number || pr?.iid}`);
    
    // Log PR events for potential future integration
    await this.logWebhookEvent(providerKey, 'pull_request', action, repository?.full_name || repository?.path_with_namespace, {
      pr_number: pr?.number || pr?.iid,
      pr_title: pr?.title,
      pr_state: pr?.state,
      pr_url: pr?.html_url || pr?.web_url
    });
  }

  // Handle repository events
  async handleRepositoryEvent(providerKey, payload) {
    const { action, repository } = payload;
    
    console.log(`Repository ${action} event for ${repository?.full_name || repository?.path_with_namespace}`);
    
    if (action === 'deleted') {
      // Handle repository deletion
      const repoData = this.extractRepositoryData(providerKey, repository);
      const query = `
        SELECT gr.id, gr.template_id
        FROM all_repositories gr
        WHERE gr.owner_name = $1 AND gr.repository_name = $2
      `;
      
      const result = await database.query(query, [repoData.owner, repoData.name]);
      
      if (result.rows.length > 0) {
        console.log(`Repository ${repoData.full_name} was deleted, marking as inactive`);
        await database.query(
          'UPDATE all_repositories SET sync_status = $1, updated_at = NOW() WHERE id = $2',
          ['deleted', result.rows[0].id]
        );
      }
    }
  }

  // Handle ping events
  async handlePingEvent(providerKey, payload) {
    console.log(`${providerKey} webhook ping received - webhook is working correctly`);
    console.log(`Repository: ${payload.repository?.full_name || payload.repository?.path_with_namespace || 'Unknown'}`);
  }

  // Process diffs for commits in a push event (same as GitHub webhook service)
  async processCommitDiffs(repositoryId, commits, repoLocalPath, providerKey = 'github') {
    console.log(`🔄 Processing diffs for ${commits.length} commits in repository ${repositoryId}`);

    if (!Array.isArray(commits) || commits.length === 0) {
      console.log('⚠️  No commits to process');
      return;
    }

    for (const commit of commits) {
      try {
        console.log(`📝 Processing diff for commit: ${commit.id}`);
        const commitQuery = `
          SELECT id FROM repository_commit_details
          WHERE repository_id = $1 AND commit_sha = $2
        `;
        const commitResult = await database.query(commitQuery, [repositoryId, commit.id]);
        if (commitResult.rows.length === 0) {
          console.warn(`⚠️  Commit ${commit.id} not found in database, skipping diff processing`);
          continue;
        }
        const commitId = commitResult.rows[0].id;
        const parentSha = commit.parents && commit.parents.length > 0 ? commit.parents[0].id : null;

         // For Bitbucket, we need to ensure file changes are in the database first
         if (providerKey === 'bitbucket') {
          await this.ensureBitbucketFileChanges(commitId, commit, repoLocalPath);
        }
        const diffResult = await this.diffService.processCommitDiffs(
          commitId,
          repositoryId,
          repoLocalPath,
          parentSha,
          commit.id
        );

        if (diffResult.success) {
          console.log(`✅ Successfully processed ${diffResult.processedFiles} file diffs for commit ${commit.id}`);
        } else {
          console.warn(`⚠️  Failed to process diffs for commit ${commit.id}: ${diffResult.error || diffResult.reason}`);
        }
      } catch (error) {
        console.error(`❌ Error processing diff for commit ${commit.id}:`, error.message);

      }
    }
  }

  // Ensure Bitbucket file changes are stored in the database
  async ensureBitbucketFileChanges(commitId, commit, repoLocalPath) {
    try {
      // Check if file changes already exist for this commit
      const existingFilesQuery = `
        SELECT COUNT(*) as count FROM repository_commit_files WHERE commit_id = $1
      `;
      const existingResult = await database.query(existingFilesQuery, [commitId]);
      
      if (parseInt(existingResult.rows[0].count) > 0) {
        console.log(`📁 File changes already exist for commit ${commit.id}`);
        return;

      }

      // Get file changes from git
      const GitRepoService = require('./git-repo.service');
      const gitService = new GitRepoService();

      const parentSha = commit.parents && commit.parents.length > 0 ? commit.parents[0].id : null;
      const fromSha = parentSha || `${commit.id}~1`;
      const fileChanges = await gitService.getChangedFilesSince(repoLocalPath, fromSha);

      // Parse the git diff output and categorize files
      const added = [];
      const modified = [];
      const removed = [];
      
      for (const change of fileChanges) {
        const { status, filePath } = change;
        switch (status) {
          case 'A':
            added.push(filePath);
            break;
          case 'M':
            modified.push(filePath);
            break;
          case 'D':
            removed.push(filePath);
            break;
          case 'R':
            // Renamed files - treat as modified for now
            modified.push(filePath);
            break;
          default:
            // Unknown status, treat as modified
            modified.push(filePath);
        }
      }
      // Insert file changes
      const addFiles = (paths = [], changeType) => paths.forEach(async (p) => {
        try {
          await database.query(
            `INSERT INTO repository_commit_files (commit_id, change_type, file_path)
             VALUES ($1, $2, $3)`,
            [commitId, changeType, p]
          );
        } catch (err) {
          console.warn(`Failed to insert file change: ${err.message}`);
        }
      });
    addFiles(added, 'added');
    addFiles(modified, 'modified');
    addFiles(removed, 'removed');

    
      console.log(`📁 Inserted ${added.length + modified.length + removed.length} file changes for Bitbucket commit ${commit.id}`);
     
    } catch (error) {
      console.warn(`Failed to ensure Bitbucket file changes for commit ${commit.id}:`, error.message);
    }
  }
}

module.exports = VcsWebhookService;
