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

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

  // 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) {
    const { repository, project, ref, commits, pusher, user } = payload;
    
    // 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 github_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) {
        // Insert into repository_commit_events
        await database.query(
          `INSERT INTO repository_commit_events (repository_id, ref, before_sha, after_sha, commit_count)
           VALUES ($1, $2, $3, $4, $5)`,
          [repoId, ref, payload.before || null, payload.after || null, commitData.length]
        );

        // 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;

              // 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 (_) {}
              });

              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 github_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
            );

            await database.query(
              'UPDATE github_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 github_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,
          added: commit.added || [],
          modified: commit.modified || [],
          removed: commit.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 github_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 github_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'}`);
  }

  // Log webhook events for debugging and analytics
  async logWebhookEvent(providerKey, eventType, action, repositoryFullName, metadata = {}, deliveryId = null, fullPayload = null) {
    try {
      await this._ensureWebhookEventsSchemaCached();

      // Build a flexible INSERT based on existing columns
      const columns = [];
      const placeholders = [];
      const values = [];
      let i = 1;

      columns.push('event_type');
      placeholders.push(`$${i++}`);
      values.push(eventType);

      if (this._webhookEventColumns.has('action')) {
        columns.push('action');
        placeholders.push(`$${i++}`);
        values.push(action || null);
      }
      if (this._webhookEventColumns.has('repository_full_name')) {
        columns.push('repository_full_name');
        placeholders.push(`$${i++}`);
        values.push(repositoryFullName || null);
      }
      if (this._webhookEventColumns.has('delivery_id')) {
        columns.push('delivery_id');
        placeholders.push(`$${i++}`);
        values.push(deliveryId || null);
      }
      if (this._webhookEventColumns.has('metadata')) {
        columns.push('metadata');
        placeholders.push(`$${i++}`);
        values.push(JSON.stringify({ ...metadata, provider: providerKey }));
      }
      if (this._webhookEventColumns.has('event_payload')) {
        columns.push('event_payload');
        placeholders.push(`$${i++}`);
        values.push(JSON.stringify(fullPayload || {}));
      }
      if (this._webhookEventColumns.has('received_at')) {
        columns.push('received_at');
        placeholders.push(`$${i++}`);
        values.push(new Date());
      }
      if (this._webhookEventColumns.has('processing_status')) {
        columns.push('processing_status');
        placeholders.push(`$${i++}`);
        values.push('pending');
      }

      const query = `INSERT INTO webhook_events (${columns.join(', ')}) VALUES (${placeholders.join(', ')})`;
      await database.query(query, values);
    } catch (error) {
      console.warn('Failed to log webhook event:', error.message);
    }
  }

  async _ensureWebhookEventsSchemaCached() {
    if (this._schemaChecked) return;
    try {
      const result = await database.query(
        "SELECT column_name, is_nullable FROM information_schema.columns WHERE table_schema='public' AND table_name='webhook_events'"
      );
      for (const row of result.rows) {
        this._webhookEventColumns.set(row.column_name, row.is_nullable);
      }
    } catch (e) {
      console.warn('Could not introspect webhook_events schema:', e.message);
    } finally {
      this._schemaChecked = true;
    }
  }

  // Get recent webhook events
  async getRecentWebhookEvents(limit = 50) {
    try {
      const query = `
        SELECT * FROM webhook_events
        ORDER BY received_at DESC
        LIMIT $1
      `;
      
      const result = await database.query(query, [limit]);
      return result.rows;
    } catch (error) {
      console.error('Failed to get webhook events:', error.message);
      return [];
    }
  }
}

module.exports = VcsWebhookService;
