import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import axios, { AxiosInstance, AxiosError } from 'axios';
import FormData from 'form-data';
import { CredentialsService } from '../../credentials/credentials.service';
import {
  IntegrationConnectionError,
  IntegrationAuthError,
  IntegrationRateLimitError,
  IntegrationApiError,
  IntegrationConfigError,
} from '../../errors';

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

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;
import {
  EcoDmsSession,
  EcoDmsDocument,
  EcoDmsFolder,
  EcoDmsClassification,
  EcoDmsSearchResult,
  EcoDmsApiResponse,
  SearchDocumentsDto,
  UploadDocumentDto,
  UpdateDocumentDto,
  ListDocumentsDto,
  CreateFolderDto,
  SearchOperator,
  SortOrder,
} from './ecodms.types';

/**
 * ecoDMS API Connector
 *
 * Provides integration with ecoDMS document management system.
 * Features:
 * - Session-based authentication
 * - Document CRUD operations
 * - Full-text search
 * - Folder management
 * - Classification/Category management
 * - Document preview and download
 *
 * API Documentation: https://www.ecodms.de/index.php/en/ecodms-api
 */
@Injectable()
export class EcoDmsConnector {
  private readonly logger = new Logger(EcoDmsConnector.name);
  private readonly integrationName = 'ecoDMS';
  private httpClient: AxiosInstance;
  private readonly retryConfig: RetryConfig;
  private configuredState: boolean = false;
  private initialized = false;

  private baseUrl: string = '';
  private username: string = '';
  private password: string = '';
  private readonly apiVersion: string = 'v1';

  // Session management
  private session: EcoDmsSession | null = null;
  private sessionRefreshTimer: NodeJS.Timeout | null = null;

  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('ECODMS');

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

    this.baseUrl = result.credentials.apiUrl || '';
    this.username = result.credentials.username || '';
    this.password = result.credentials.password || '';
    this.configuredState = !!(this.baseUrl && this.username && this.password);

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

    this.initialized = true;
  }

  async reload(): Promise<void> {
    this.initialized = false;
    this.configuredState = false;
    this.session = null;
    if (this.sessionRefreshTimer) {
      clearTimeout(this.sessionRefreshTimer);
      this.sessionRefreshTimer = null;
    }
    await this.ensureInitialized();
  }

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

  /**
   * Setup axios interceptors
   */
  private setupInterceptors(): void {
    // Request interceptor to add session ID
    this.httpClient.interceptors.request.use(
      async (config) => {
        // Don't add session header for login request
        if (!config.url?.includes('/session/login')) {
          await this.ensureSession();
          if (this.session) {
            config.headers['X-Session-Id'] = this.session.sessionId;
          }
        }

        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;
      },
      async (error) => {
        // Handle session expiration
        if (error.response?.status === 401 && this.session) {
          this.logger.warn('[ecoDMS] Session expired, attempting to re-authenticate');
          this.session = null;
          // Don't retry login failures
          if (!error.config?.url?.includes('/session/login')) {
            try {
              await this.login();
              // Retry the original request with new session
              // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
              const newSession = this.session as EcoDmsSession | null;
              error.config.headers['X-Session-Id'] = newSession?.sessionId;
              return this.httpClient.request(error.config);
            } catch (loginError) {
              this.logger.error('[ecoDMS] Re-authentication failed');
            }
          }
        }
        return Promise.reject(error);
      },
    );
  }

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

  /**
   * Ensure we have a valid session
   */
  private async ensureSession(): Promise<void> {
    if (!this.session) {
      await this.login();
    }
  }

  /**
   * 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 (will be handled by interceptor)
    if (status === 401) {
      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 EcoDmsApiResponse<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));
  }

  // ============ Authentication ============

  /**
   * Login and obtain session
   */
  private async login(): Promise<void> {
    this.ensureConfigured();

    try {
      const response = await this.httpClient.post<EcoDmsApiResponse<EcoDmsSession>>(
        '/session/login',
        {
          username: this.username,
          password: this.password,
        },
      );

      if (response.data.success && response.data.data) {
        this.session = response.data.data;
        this.logger.log(`[ecoDMS] Logged in as ${this.session.userName}`);

        // Schedule session refresh before expiry
        this.scheduleSessionRefresh();
      } else {
        throw new IntegrationAuthError(
          this.integrationName,
          response.data.error?.message || 'Login failed',
        );
      }
    } catch (error) {
      if (error instanceof IntegrationAuthError) {
        throw error;
      }
      throw this.mapError(error as AxiosError, 'login');
    }
  }

  /**
   * Schedule session refresh
   */
  private scheduleSessionRefresh(): void {
    if (this.sessionRefreshTimer) {
      clearTimeout(this.sessionRefreshTimer);
    }

    if (this.session?.validUntil) {
      const validUntil = new Date(this.session.validUntil);
      const refreshTime = validUntil.getTime() - Date.now() - 60000; // Refresh 1 minute before expiry

      if (refreshTime > 0) {
        this.sessionRefreshTimer = setTimeout(async () => {
          this.logger.debug('[ecoDMS] Refreshing session');
          try {
            await this.login();
          } catch (error) {
            this.logger.error('[ecoDMS] Session refresh failed');
          }
        }, refreshTime);
      }
    }
  }

  /**
   * Logout and invalidate session
   */
  async logout(): Promise<void> {
    if (this.session) {
      try {
        await this.httpClient.post('/session/logout');
      } catch (error) {
        this.logger.warn('[ecoDMS] Logout request failed, clearing session anyway');
      }
      this.session = null;
      if (this.sessionRefreshTimer) {
        clearTimeout(this.sessionRefreshTimer);
        this.sessionRefreshTimer = null;
      }
      this.logger.log('[ecoDMS] Logged out');
    }
  }

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

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

    const startTime = Date.now();
    try {
      await this.ensureSession();
      // Try to get classifications as a health check
      await this.httpClient.get('/classifications');
      return {
        status: 'connected',
        lastCheck: new Date(),
        latency: Date.now() - startTime,
        details: {
          sessionUser: this.session?.userName,
        },
      };
    } 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 {
      await this.login();
      const latency = Date.now() - startTime;

      return {
        success: true,
        message: `Successfully connected as ${this.session?.userName}`,
        latency,
      };
    } catch (error) {
      throw this.mapError(error as AxiosError, 'testConnection');
    }
  }

  /**
   * Check if connector is configured (last known state)
   */
  getIsConfigured(): boolean {
    return this.configuredState;
  }

  // ============ Documents API ============

  /**
   * List documents
   */
  async listDocuments(params: ListDocumentsDto = {}): Promise<{
    documents: EcoDmsDocument[];
    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 || 50,
        sortBy: params.sortBy || 'modifiedAt',
        sortOrder: params.sortOrder || SortOrder.DESC,
      };

      if (params.folderId !== undefined) {
        queryParams.folderId = params.folderId;
      }
      if (params.classifyId !== undefined) {
        queryParams.classifyId = params.classifyId;
      }
      if (params.recursive !== undefined) {
        queryParams.recursive = params.recursive;
      }

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

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

      const { documents, total } = response.data.data;
      const page = params.page || 1;
      const pageSize = params.pageSize || 50;

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

  /**
   * Get a single document by ID
   */
  async getDocument(id: number): Promise<EcoDmsDocument> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.get<EcoDmsApiResponse<EcoDmsDocument>>(
        `/documents/${id}`,
      );

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

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

  /**
   * Search documents
   */
  async searchDocuments(params: SearchDocumentsDto): Promise<EcoDmsSearchResult> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const searchPayload: Record<string, unknown> = {
        page: params.page || 1,
        pageSize: params.pageSize || 50,
        sortBy: params.sortBy || 'modifiedAt',
        sortOrder: params.sortOrder || SortOrder.DESC,
        operator: params.operator || SearchOperator.AND,
      };

      if (params.query) {
        searchPayload.fullText = params.query;
      }
      if (params.classifyId !== undefined) {
        searchPayload.classifyId = params.classifyId;
      }
      if (params.folderId !== undefined) {
        searchPayload.folderId = params.folderId;
      }
      if (params.dateFrom) {
        searchPayload.dateFrom = params.dateFrom;
      }
      if (params.dateTo) {
        searchPayload.dateTo = params.dateTo;
      }
      if (params.fileExtension) {
        searchPayload.fileExtension = params.fileExtension;
      }
      if (params.attributes) {
        searchPayload.attributes = params.attributes;
      }

      const response = await this.httpClient.post<EcoDmsApiResponse<EcoDmsSearchResult>>(
        '/documents/search',
        searchPayload,
      );

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Search failed',
        );
      }

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

  /**
   * Upload a document
   */
  async uploadDocument(
    file: Buffer,
    fileName: string,
    mimeType: string,
    metadata: UploadDocumentDto,
  ): Promise<EcoDmsDocument> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const formData = new FormData();
      formData.append('file', file, {
        filename: fileName,
        contentType: mimeType,
      });
      formData.append('title', metadata.title);
      formData.append('classifyId', metadata.classifyId.toString());

      if (metadata.folderId) {
        formData.append('folderId', metadata.folderId.toString());
      }
      if (metadata.fileName) {
        formData.append('fileName', metadata.fileName);
      }
      if (metadata.documentDate) {
        formData.append('documentDate', metadata.documentDate);
      }
      if (metadata.enableOcr !== undefined) {
        formData.append('enableOcr', metadata.enableOcr.toString());
      }
      if (metadata.attributes) {
        formData.append('attributes', JSON.stringify(metadata.attributes));
      }

      const response = await this.httpClient.post<EcoDmsApiResponse<EcoDmsDocument>>(
        '/documents/upload',
        formData,
        {
          headers: {
            ...formData.getHeaders(),
          },
          timeout: 120000, // 2 minutes for uploads
        },
      );

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Upload failed',
        );
      }

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

  /**
   * Update document metadata
   */
  async updateDocument(id: number, data: UpdateDocumentDto): Promise<EcoDmsDocument> {
    await this.ensureInitialized();
    this.ensureConfigured();

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

      if (!response.data.success || !response.data.data) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Update failed',
        );
      }

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

  /**
   * Delete a document
   */
  async deleteDocument(id: number): Promise<void> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.delete<EcoDmsApiResponse<void>>(
        `/documents/${id}`,
      );

      if (!response.data.success) {
        throw new IntegrationApiError(
          this.integrationName,
          response.data.error?.message || 'Delete failed',
        );
      }
    }, 'deleteDocument');
  }

  /**
   * Download document content
   */
  async downloadDocument(id: number): Promise<{ content: Buffer; fileName: string; mimeType: string }> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.get(`/documents/${id}/download`, {
        responseType: 'arraybuffer',
      });

      const contentDisposition = response.headers['content-disposition'] || '';
      const fileNameMatch = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
      const fileName = fileNameMatch ? fileNameMatch[1].replace(/['"]/g, '') : `document_${id}`;
      const mimeType = response.headers['content-type'] || 'application/octet-stream';

      return {
        content: Buffer.from(response.data),
        fileName,
        mimeType,
      };
    }, 'downloadDocument');
  }

  /**
   * Get document preview (thumbnail or PDF preview)
   */
  async getDocumentPreview(id: number, page: number = 1): Promise<Buffer> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.get(`/documents/${id}/preview`, {
        params: { page },
        responseType: 'arraybuffer',
      });

      return Buffer.from(response.data);
    }, 'getDocumentPreview');
  }

  // ============ Folders API ============

  /**
   * List folders
   */
  async listFolders(parentId?: number): Promise<EcoDmsFolder[]> {
    await this.ensureInitialized();
    this.ensureConfigured();

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

      const response = await this.httpClient.get<EcoDmsApiResponse<EcoDmsFolder[]>>(
        '/folders',
        { params },
      );

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

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

  /**
   * Get folder tree
   */
  async getFolderTree(): Promise<EcoDmsFolder[]> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.get<EcoDmsApiResponse<EcoDmsFolder[]>>(
        '/folders/tree',
      );

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

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

  /**
   * Create a folder
   */
  async createFolder(data: CreateFolderDto): Promise<EcoDmsFolder> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.post<EcoDmsApiResponse<EcoDmsFolder>>(
        '/folders',
        data,
      );

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

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

  /**
   * Delete a folder
   */
  async deleteFolder(id: number): Promise<void> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.delete<EcoDmsApiResponse<void>>(
        `/folders/${id}`,
      );

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

  // ============ Classifications API ============

  /**
   * List classifications (document types/categories)
   */
  async listClassifications(): Promise<EcoDmsClassification[]> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.get<EcoDmsApiResponse<EcoDmsClassification[]>>(
        '/classifications',
      );

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

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

  /**
   * Get classification details
   */
  async getClassification(id: number): Promise<EcoDmsClassification> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.httpClient.get<EcoDmsApiResponse<EcoDmsClassification>>(
        `/classifications/${id}`,
      );

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

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