import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import axios, { AxiosInstance, AxiosError } from 'axios';
import { CredentialsService } from '../../credentials/credentials.service';
import {
  IntegrationConnectionError,
  IntegrationAuthError,
  IntegrationRateLimitError,
  IntegrationApiError,
  IntegrationConfigError,
} from '../../errors';
import {
  GembaAudit,
  GembaChecklist,
  GembaFinding,
  GembaStatistics,
  GembaTrendData,
  GembaApiResponse,
  ListAuditsDto,
  CreateAuditDto,
  UpdateAuditDto,
  ListFindingsDto,
  UpdateFindingDto,
  ResolveFindingDto,
  GetTrendsDto,
  AuditStatus,
  FindingStatus,
} from './gembadocs.types';

/**
 * Health status of the connector
 */
export interface ConnectorHealth {
  status: 'connected' | 'error' | 'not_configured';
  lastCheck: Date;
  latency?: number;
  error?: string;
  details?: Record<string, unknown>;
}

/**
 * Retry configuration
 */
interface RetryConfig {
  maxRetries: number;
  baseDelayMs: number;
  maxDelayMs: number;
  jitterFactor: number;
}

const DEFAULT_RETRY_CONFIG: RetryConfig = {
  maxRetries: 3,
  baseDelayMs: 1000,
  maxDelayMs: 30000,
  jitterFactor: 0.1,
};

const DEFAULT_TIMEOUT_MS = 30000;

/**
 * GembaDocs API Connector
 *
 * Provides integration with GembaDocs audit and checklist management system.
 * Features:
 * - API Key authentication
 * - Audits management (CRUD operations)
 * - Checklists and templates
 * - Findings/Actions tracking
 * - Statistics and trends
 *
 * API Documentation: GembaDocs API v1
 */
@Injectable()
export class GembaDocsConnector {
  private readonly logger = new Logger(GembaDocsConnector.name);
  private readonly integrationName = 'GembaDocs';
  private httpClient: AxiosInstance;
  private readonly retryConfig: RetryConfig;
  private isConfiguredFlag: boolean = false;
  private initialized = false;

  private baseUrl: string = '';
  private apiKey: string = '';

  constructor(private readonly credentialsService: CredentialsService) {
    this.retryConfig = { ...DEFAULT_RETRY_CONFIG };
    this.httpClient = axios.create();
  }

  async ensureInitialized(): Promise<void> {
    if (this.initialized) return;

    const result = await this.credentialsService.findActiveByType('GEMBADOCS');

    if (!result) {
      this.isConfiguredFlag = false;
      this.initialized = true;
      this.logger.warn('GembaDocs connector not configured - no active credentials found');
      return;
    }

    this.baseUrl = result.credentials.apiUrl || '';
    this.apiKey = result.credentials.apiKey || '';
    this.isConfiguredFlag = !!(this.baseUrl && this.apiKey);

    if (this.isConfiguredFlag) {
      this.httpClient = axios.create({
        baseURL: `${this.baseUrl}/api/v1`,
        timeout: DEFAULT_TIMEOUT_MS,
        headers: {
          'Content-Type': 'application/json',
          Accept: 'application/json',
        },
      });
      this.setupInterceptors();
      this.logger.log(`GembaDocs connector initialized with base URL: ${this.baseUrl}`);
    } else {
      this.logger.warn('GembaDocs connector not configured - missing apiUrl or apiKey');
    }

    this.initialized = true;
  }

  async reload(): Promise<void> {
    this.initialized = false;
    this.isConfiguredFlag = false;
    await this.ensureInitialized();
  }

  @OnEvent('credentials.changed')
  async onCredentialsChanged(payload: { type: string }): Promise<void> {
    if (payload.type === 'GEMBADOCS') {
      this.logger.log('GembaDocs credentials changed, reloading');
      await this.reload();
    }
  }

  /**
   * Setup axios interceptors
   */
  private setupInterceptors(): void {
    // Request interceptor to add API key
    this.httpClient.interceptors.request.use(
      (config) => {
        if (this.apiKey) {
          config.headers['X-API-Key'] = this.apiKey;
        }

        this.logger.debug(
          `[${this.integrationName}] Request: ${config.method?.toUpperCase()} ${config.url}`,
        );
        return config;
      },
      (error) => {
        this.logger.error(`[${this.integrationName}] Request error: ${error.message}`);
        return Promise.reject(error);
      },
    );

    // Response interceptor
    this.httpClient.interceptors.response.use(
      (response) => {
        this.logger.debug(`[${this.integrationName}] Response: ${response.status}`);
        return response;
      },
      (error) => {
        return Promise.reject(error);
      },
    );
  }

  private ensureConfigured(): void {
    if (!this.isConfiguredFlag) {
      throw new IntegrationConfigError(this.integrationName, ['apiUrl', 'apiKey']);
    }
  }

  /**
   * Execute request with retry logic
   */
  private async executeWithRetry<T>(
    requestFn: () => Promise<T>,
    operation: string,
  ): Promise<T> {
    let lastError: Error | undefined;
    let attempt = 0;

    while (attempt <= this.retryConfig.maxRetries) {
      try {
        return await requestFn();
      } catch (error) {
        lastError = error as Error;
        attempt++;

        const shouldRetry = this.shouldRetry(error as AxiosError, attempt);
        if (!shouldRetry) {
          break;
        }

        const delay = this.calculateDelay(attempt);
        this.logger.warn(
          `[${this.integrationName}] ${operation} failed (attempt ${attempt}/${this.retryConfig.maxRetries}), retrying in ${delay}ms`,
        );

        await this.sleep(delay);
      }
    }

    throw this.mapError(lastError as AxiosError, operation);
  }

  /**
   * Determine if request should be retried
   */
  private shouldRetry(error: AxiosError, attempt: number): boolean {
    if (attempt > this.retryConfig.maxRetries) {
      return false;
    }

    if (!error.response) {
      return true;
    }

    const status = error.response.status;
    // Don't retry auth errors
    if (status === 401 || status === 403) {
      return false;
    }
    return status >= 500 || status === 429;
  }

  /**
   * Calculate delay with exponential backoff and jitter
   */
  private calculateDelay(attempt: number): number {
    const delay = Math.min(
      this.retryConfig.baseDelayMs * Math.pow(2, attempt - 1),
      this.retryConfig.maxDelayMs,
    );
    const jitter = delay * this.retryConfig.jitterFactor * Math.random();
    return Math.floor(delay + jitter);
  }

  /**
   * Map axios errors to integration errors
   */
  private mapError(error: AxiosError, operation: string): Error {
    if (!error.response) {
      if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
        return new IntegrationConnectionError(
          this.integrationName,
          `Request timeout during ${operation}`,
          error,
        );
      }
      return new IntegrationConnectionError(
        this.integrationName,
        `Connection failed during ${operation}: ${error.message}`,
        error,
      );
    }

    const status = error.response.status;
    const responseData = error.response.data as GembaApiResponse<unknown>;
    const message = responseData?.error?.message || error.message;

    switch (status) {
      case 401:
        return new IntegrationAuthError(
          this.integrationName,
          `Authentication failed: ${message}`,
          error,
        );
      case 403:
        return new IntegrationAuthError(
          this.integrationName,
          `Access forbidden: ${message}`,
          error,
        );
      case 429:
        const retryAfter = parseInt(error.response.headers['retry-after'] || '60', 10);
        return new IntegrationRateLimitError(
          this.integrationName,
          'Rate limit exceeded',
          retryAfter,
          error,
        );
      default:
        return new IntegrationApiError(
          this.integrationName,
          `${operation} failed: ${message}`,
          status,
          responseData?.error?.code,
          error,
        );
    }
  }

  /**
   * Sleep helper
   */
  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  // ============ Health & Connection ============

  /**
   * Check connector health
   */
  async checkHealth(): Promise<ConnectorHealth> {
    await this.ensureInitialized();
    if (!this.isConfiguredFlag) {
      return {
        status: 'not_configured',
        lastCheck: new Date(),
        error: 'GembaDocs connector is not configured',
      };
    }

    const startTime = Date.now();
    try {
      // Use statistics endpoint as health check
      await this.httpClient.get('/health');
      return {
        status: 'connected',
        lastCheck: new Date(),
        latency: Date.now() - startTime,
      };
    } catch (error) {
      return {
        status: 'error',
        lastCheck: new Date(),
        latency: Date.now() - startTime,
        error: (error as Error).message,
      };
    }
  }

  /**
   * Test connection
   */
  async testConnection(): Promise<{ success: boolean; message: string; latency?: number }> {
    await this.ensureInitialized();
    this.ensureConfigured();

    const startTime = Date.now();
    try {
      const response = await this.httpClient.get<GembaApiResponse<{ version: string }>>(
        '/health',
      );
      const latency = Date.now() - startTime;

      if (response.data.success) {
        return {
          success: true,
          message: `Connected to GembaDocs API v${response.data.data?.version || 'unknown'}`,
          latency,
        };
      }

      return {
        success: false,
        message: response.data.error?.message || 'Connection test failed',
        latency,
      };
    } catch (error) {
      throw this.mapError(error as AxiosError, 'testConnection');
    }
  }

  /**
   * Check if connector is configured
   */
  isConfigured(): boolean {
    return this.isConfiguredFlag;
  }

  /**
   * Get list of missing configuration items
   */
  getMissingConfig(): string[] {
    const missing: string[] = [];
    if (!this.baseUrl) missing.push('apiUrl');
    if (!this.apiKey) missing.push('apiKey');
    return missing;
  }

  // ============ Audits API ============

  /**
   * List audits with optional filters
   */
  async listAudits(params: ListAuditsDto = {}): Promise<{
    audits: GembaAudit[];
    pagination: { page: number; pageSize: number; total: number; totalPages: number };
  }> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const queryParams: Record<string, unknown> = {
        page: params.page || 1,
        pageSize: params.pageSize || 20,
      };

      if (params.type) queryParams.type = params.type;
      if (params.status) queryParams.status = params.status;
      if (params.department) queryParams.department = params.department;
      if (params.location) queryParams.location = params.location;
      if (params.auditorId) queryParams.auditorId = params.auditorId;
      if (params.dateFrom) queryParams.dateFrom = params.dateFrom;
      if (params.dateTo) queryParams.dateTo = params.dateTo;

      const response = await this.httpClient.get<
        GembaApiResponse<{ audits: GembaAudit[]; total: number }>
      >('/audits', { params: queryParams });

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to list audits',
        );
      }

      const { audits, total } = response.data.data;
      const page = params.page || 1;
      const pageSize = params.pageSize || 20;

      return {
        audits,
        pagination: {
          page,
          pageSize,
          total,
          totalPages: Math.ceil(total / pageSize),
        },
      };
    }, 'listAudits');
  }

  /**
   * Get a single audit by ID
   */
  async getAudit(id: string): Promise<GembaAudit> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.get<GembaApiResponse<GembaAudit>>(
        `/audits/${id}`,
      );

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Audit not found',
          404,
        );
      }

      return response.data.data;
    }, 'getAudit');
  }

  /**
   * Get upcoming audits (scheduled for the future)
   */
  async getUpcomingAudits(days: number = 7, limit: number = 10): Promise<GembaAudit[]> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const today = new Date();
      const futureDate = new Date();
      futureDate.setDate(today.getDate() + days);

      const response = await this.httpClient.get<
        GembaApiResponse<{ audits: GembaAudit[] }>
      >('/audits', {
        params: {
          status: AuditStatus.SCHEDULED,
          dateFrom: today.toISOString().split('T')[0],
          dateTo: futureDate.toISOString().split('T')[0],
          pageSize: limit,
          sortBy: 'scheduledDate',
          sortOrder: 'asc',
        },
      });

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to fetch upcoming audits',
        );
      }

      return response.data.data.audits;
    }, 'getUpcomingAudits');
  }

  /**
   * Get overdue audits (scheduled in the past but not completed)
   */
  async getOverdueAudits(limit: number = 10): Promise<GembaAudit[]> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const today = new Date();

      const response = await this.httpClient.get<
        GembaApiResponse<{ audits: GembaAudit[] }>
      >('/audits', {
        params: {
          status: AuditStatus.SCHEDULED,
          dateTo: today.toISOString().split('T')[0],
          pageSize: limit,
          sortBy: 'scheduledDate',
          sortOrder: 'asc',
        },
      });

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to fetch overdue audits',
        );
      }

      // Filter to only include audits that are actually overdue
      return response.data.data.audits.filter((audit) => {
        const scheduledDate = new Date(audit.scheduledDate);
        return scheduledDate < today && audit.status === AuditStatus.SCHEDULED;
      });
    }, 'getOverdueAudits');
  }

  /**
   * Create a new audit
   */
  async createAudit(data: CreateAuditDto): Promise<GembaAudit> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.post<GembaApiResponse<GembaAudit>>(
        '/audits',
        data,
      );

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to create audit',
        );
      }

      return response.data.data;
    }, 'createAudit');
  }

  /**
   * Update an audit
   */
  async updateAudit(id: string, data: UpdateAuditDto): Promise<GembaAudit> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.put<GembaApiResponse<GembaAudit>>(
        `/audits/${id}`,
        data,
      );

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to update audit',
        );
      }

      return response.data.data;
    }, 'updateAudit');
  }

  // ============ Checklists API ============

  /**
   * List all checklist templates
   */
  async listChecklists(): Promise<GembaChecklist[]> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.get<
        GembaApiResponse<{ checklists: GembaChecklist[] }>
      >('/checklists');

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to list checklists',
        );
      }

      return response.data.data.checklists;
    }, 'listChecklists');
  }

  /**
   * Get a single checklist by ID
   */
  async getChecklist(id: string): Promise<GembaChecklist> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.get<GembaApiResponse<GembaChecklist>>(
        `/checklists/${id}`,
      );

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Checklist not found',
          404,
        );
      }

      return response.data.data;
    }, 'getChecklist');
  }

  // ============ Findings API ============

  /**
   * List findings with optional filters
   */
  async listFindings(params: ListFindingsDto = {}): Promise<{
    findings: GembaFinding[];
    pagination: { page: number; pageSize: number; total: number; totalPages: number };
  }> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const queryParams: Record<string, unknown> = {
        page: params.page || 1,
        pageSize: params.pageSize || 20,
      };

      if (params.auditId) queryParams.auditId = params.auditId;
      if (params.severity) queryParams.severity = params.severity;
      if (params.status) queryParams.status = params.status;
      if (params.assigneeId) queryParams.assigneeId = params.assigneeId;
      if (params.overdueOnly) queryParams.overdueOnly = params.overdueOnly;

      const response = await this.httpClient.get<
        GembaApiResponse<{ findings: GembaFinding[]; total: number }>
      >('/findings', { params: queryParams });

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to list findings',
        );
      }

      const { findings, total } = response.data.data;
      const page = params.page || 1;
      const pageSize = params.pageSize || 20;

      return {
        findings,
        pagination: {
          page,
          pageSize,
          total,
          totalPages: Math.ceil(total / pageSize),
        },
      };
    }, 'listFindings');
  }

  /**
   * Get open findings
   */
  async getOpenFindings(limit: number = 20): Promise<GembaFinding[]> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.get<
        GembaApiResponse<{ findings: GembaFinding[] }>
      >('/findings', {
        params: {
          status: FindingStatus.OPEN,
          pageSize: limit,
          sortBy: 'severity',
          sortOrder: 'desc',
        },
      });

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to fetch open findings',
        );
      }

      return response.data.data.findings;
    }, 'getOpenFindings');
  }

  /**
   * Update a finding
   */
  async updateFinding(id: string, data: UpdateFindingDto): Promise<GembaFinding> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.put<GembaApiResponse<GembaFinding>>(
        `/findings/${id}`,
        data,
      );

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to update finding',
        );
      }

      return response.data.data;
    }, 'updateFinding');
  }

  /**
   * Resolve a finding
   */
  async resolveFinding(id: string, data: ResolveFindingDto): Promise<GembaFinding> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.post<GembaApiResponse<GembaFinding>>(
        `/findings/${id}/resolve`,
        data,
      );

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to resolve finding',
        );
      }

      return response.data.data;
    }, 'resolveFinding');
  }

  // ============ Statistics & Trends API ============

  /**
   * Get audit statistics
   */
  async getStatistics(department?: string): Promise<GembaStatistics> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const params: Record<string, unknown> = {};
      if (department) params.department = department;

      const response = await this.httpClient.get<GembaApiResponse<GembaStatistics>>(
        '/statistics',
        { params },
      );

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to fetch statistics',
        );
      }

      return response.data.data;
    }, 'getStatistics');
  }

  /**
   * Get trend data for charts
   */
  async getTrends(params: GetTrendsDto = {}): Promise<GembaTrendData> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const queryParams: Record<string, unknown> = {
        period: params.period || 'month',
      };

      if (params.startDate) queryParams.startDate = params.startDate;
      if (params.endDate) queryParams.endDate = params.endDate;
      if (params.department) queryParams.department = params.department;
      if (params.auditType) queryParams.auditType = params.auditType;

      const response = await this.httpClient.get<GembaApiResponse<GembaTrendData>>(
        '/trends',
        { params: queryParams },
      );

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Failed to fetch trends',
        );
      }

      return response.data.data;
    }, 'getTrends');
  }
}
