import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import axios, { AxiosInstance, AxiosError } from 'axios';
import {
  IntegrationConnectionError,
  IntegrationAuthError,
  IntegrationRateLimitError,
  IntegrationApiError,
  IntegrationConfigError,
} from '../../errors';
import { CredentialsService } from '../../credentials/credentials.service';
import {
  NextcloudFile,
  NextcloudUser,
  NextcloudShare,
  NextcloudCalendarEvent,
  NextcloudCalendar,
  OCSResponse,
  FileType,
  ListFilesDto,
  CreateShareDto,
  ListEventsDto,
} from './nextcloud.types';

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;

/**
 * Nextcloud API Connector
 *
 * Provides integration with Nextcloud cloud platform.
 * Credentials are loaded lazily from the DB via CredentialsService.
 */
@Injectable()
export class NextcloudConnector {
  private readonly logger = new Logger(NextcloudConnector.name);
  private readonly integrationName = 'Nextcloud';
  private httpClient: AxiosInstance;
  private webdavClient: AxiosInstance;
  private readonly retryConfig: RetryConfig;
  private configuredState: boolean = false;
  private initialized = false;

  private baseUrl: string = '';
  private username: string = '';
  private password: string = '';

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

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

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

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

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

    if (this.configuredState) {
      const authHeader = Buffer.from(`${this.username}:${this.password}`).toString('base64');

      this.httpClient = axios.create({
        baseURL: `${this.baseUrl}/ocs/v2.php`,
        timeout: DEFAULT_TIMEOUT_MS,
        headers: {
          'Content-Type': 'application/json',
          Accept: 'application/json',
          'OCS-APIRequest': 'true',
          Authorization: `Basic ${authHeader}`,
        },
      });

      this.webdavClient = axios.create({
        baseURL: `${this.baseUrl}/remote.php/dav/files/${this.username}`,
        timeout: DEFAULT_TIMEOUT_MS,
        headers: { Authorization: `Basic ${authHeader}` },
      });

      this.setupInterceptors();
      this.logger.log(`Nextcloud connector initialized with base URL: ${this.baseUrl}`);
    } else {
      this.logger.warn('Nextcloud connector not configured - missing serverUrl, username, or password');
    }

    this.initialized = true;
  }

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

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

  private setupInterceptors(): void {
    const setup = (client: AxiosInstance, name: string) => {
      client.interceptors.request.use(
        (config) => {
          this.logger.debug(`[${this.integrationName}:${name}] ${config.method?.toUpperCase()} ${config.url}`);
          return config;
        },
        (error) => {
          this.logger.error(`[${this.integrationName}:${name}] Request error: ${error.message}`);
          return Promise.reject(error);
        },
      );
      client.interceptors.response.use(
        (response) => {
          this.logger.debug(`[${this.integrationName}:${name}] Response: ${response.status}`);
          return response;
        },
        (error) => {
          if (error.response) {
            this.logger.warn(`[${this.integrationName}:${name}] Error: ${error.response.status}`);
          }
          return Promise.reject(error);
        },
      );
    };

    setup(this.httpClient, 'OCS');
    setup(this.webdavClient, 'WebDAV');
  }

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

  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++;
        if (!this.shouldRetry(error as AxiosError, attempt)) 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);
  }

  private shouldRetry(error: AxiosError, attempt: number): boolean {
    if (attempt > this.retryConfig.maxRetries) return false;
    if (!error.response) return true;
    const status = error.response.status;
    return status >= 500 || status === 429;
  }

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

  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 Record<string, unknown>;
    const message = (responseData?.message as string) || error.message;
    switch (status) {
      case 401: return new IntegrationAuthError(this.integrationName, `Authentication failed: Invalid credentials`, 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, undefined, error);
    }
  }

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

  private parseWebDAVResponse(xmlData: string, basePath: string): NextcloudFile[] {
    const files: NextcloudFile[] = [];
    const responseRegex = /<d:response>([\s\S]*?)<\/d:response>/g;
    let match;

    while ((match = responseRegex.exec(xmlData)) !== null) {
      const response = match[1];
      const hrefMatch = /<d:href>(.*?)<\/d:href>/.exec(response);
      const href = hrefMatch ? decodeURIComponent(hrefMatch[1]) : '';
      const pathPrefix = `/remote.php/dav/files/${this.username}`;
      let path = href.replace(pathPrefix, '') || '/';
      if (path !== '/' && path.endsWith('/')) path = path.slice(0, -1);
      if (path === basePath && basePath !== '/') continue;
      if (path === basePath) continue;

      const isDirectory = response.includes('<d:collection');
      const contentLength = this.extractXmlValue(response, 'd:getcontentlength') || '0';
      const contentType = this.extractXmlValue(response, 'd:getcontenttype');
      const etag = this.extractXmlValue(response, 'd:getetag');
      const lastModified = this.extractXmlValue(response, 'd:getlastmodified');
      const displayName = this.extractXmlValue(response, 'd:displayname');
      const permissions = this.extractXmlValue(response, 'oc:permissions') || '';
      const fileId = this.extractXmlValue(response, 'oc:fileid');
      const favorite = this.extractXmlValue(response, 'oc:favorite');

      files.push({
        basename: displayName || path.split('/').pop() || '',
        filename: path,
        fileid: fileId ? parseInt(fileId, 10) : 0,
        type: isDirectory ? FileType.DIRECTORY : FileType.FILE,
        mime: isDirectory ? null : (contentType || 'application/octet-stream'),
        size: parseInt(contentLength, 10),
        etag: etag || '',
        lastmod: lastModified || '',
        permissions,
        favorite: favorite === '1',
        hasPreview: !isDirectory,
        path,
      });
    }
    return files;
  }

  private extractXmlValue(xml: string, tag: string): string | null {
    const regex = new RegExp(`<${tag}[^>]*>([^<]*)<\/${tag}>`, 'i');
    const match = regex.exec(xml);
    return match ? match[1].trim() : null;
  }

  private buildPropfindBody(): string {
    return [
      '<?xml version="1.0" encoding="UTF-8"?>',
      '<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">',
      '<d:prop>',
      '<d:resourcetype/><d:getcontentlength/><d:getcontenttype/>',
      '<d:getetag/><d:getlastmodified/><d:displayname/>',
      '<oc:fileid/><oc:permissions/><oc:favorite/><nc:has-preview/>',
      '</d:prop>',
      '</d:propfind>',
    ].join('');
  }

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

  async checkHealth(): Promise<ConnectorHealth> {
    await this.ensureInitialized();
    if (!this.configuredState) {
      return { status: 'not_configured', lastCheck: new Date(), error: 'Nextcloud connector is not configured' };
    }
    const startTime = Date.now();
    try {
      await this.httpClient.get('/cloud/user?format=json');
      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 };
    }
  }

  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<OCSResponse<NextcloudUser>>('/cloud/user?format=json');
      const latency = Date.now() - startTime;
      const user = response.data.ocs.data;
      return { success: true, message: `Successfully connected as ${user.displayName || user.id}`, latency };
    } catch (error) {
      throw this.mapError(error as AxiosError, 'testConnection');
    }
  }

  getIsConfigured(): boolean {
    return this.configuredState;
  }

  // ============ Files API (WebDAV) ============

  async listFiles(params: ListFilesDto = {}): Promise<NextcloudFile[]> {
    await this.ensureInitialized();
    this.ensureConfigured();
    const path = params.path || '/';

    return this.executeWithRetry(async () => {
      const response = await this.webdavClient.request({
        method: 'PROPFIND',
        url: path,
        headers: { Depth: (params.depth ?? 1).toString(), 'Content-Type': 'application/xml' },
        data: this.buildPropfindBody(),
      });
      let files = this.parseWebDAVResponse(response.data, path);
      if (params.filesOnly) files = files.filter((f) => f.type === FileType.FILE);
      if (params.dirsOnly) files = files.filter((f) => f.type === FileType.DIRECTORY);
      return files;
    }, 'listFiles');
  }

  async getFileInfo(path: string): Promise<NextcloudFile> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const response = await this.webdavClient.request({
        method: 'PROPFIND',
        url: path,
        headers: { Depth: '0', 'Content-Type': 'application/xml' },
        data: this.buildPropfindBody(),
      });
      const files = this.parseWebDAVResponse(response.data, path);
      if (files.length === 0) {
        const allFiles = this.parseWebDAVResponse(response.data, '');
        if (allFiles.length > 0) return allFiles[0];
        throw new IntegrationApiError(this.integrationName, `File not found: ${path}`, 404);
      }
      return files[0];
    }, 'getFileInfo');
  }

  async uploadFile(path: string, content: Buffer | string, contentType?: string): Promise<NextcloudFile> {
    await this.ensureInitialized();
    this.ensureConfigured();
    return this.executeWithRetry(async () => {
      await this.webdavClient.put(path, content, {
        headers: { 'Content-Type': contentType || 'application/octet-stream' },
      });
      return this.getFileInfo(path);
    }, 'uploadFile');
  }

  async downloadFile(path: string): Promise<Buffer> {
    await this.ensureInitialized();
    this.ensureConfigured();
    return this.executeWithRetry(async () => {
      const response = await this.webdavClient.get(path, { responseType: 'arraybuffer' });
      return Buffer.from(response.data);
    }, 'downloadFile');
  }

  async deleteFile(path: string): Promise<void> {
    await this.ensureInitialized();
    this.ensureConfigured();
    return this.executeWithRetry(async () => {
      await this.webdavClient.delete(path);
    }, 'deleteFile');
  }

  async createFolder(path: string): Promise<NextcloudFile> {
    await this.ensureInitialized();
    this.ensureConfigured();
    return this.executeWithRetry(async () => {
      await this.webdavClient.request({ method: 'MKCOL', url: path });
      return this.getFileInfo(path);
    }, 'createFolder');
  }

  async moveFile(source: string, destination: string, overwrite: boolean = false): Promise<void> {
    await this.ensureInitialized();
    this.ensureConfigured();
    return this.executeWithRetry(async () => {
      await this.webdavClient.request({
        method: 'MOVE',
        url: source,
        headers: {
          Destination: `${this.baseUrl}/remote.php/dav/files/${this.username}${destination}`,
          Overwrite: overwrite ? 'T' : 'F',
        },
      });
    }, 'moveFile');
  }

  async copyFile(source: string, destination: string, overwrite: boolean = false): Promise<void> {
    await this.ensureInitialized();
    this.ensureConfigured();
    return this.executeWithRetry(async () => {
      await this.webdavClient.request({
        method: 'COPY',
        url: source,
        headers: {
          Destination: `${this.baseUrl}/remote.php/dav/files/${this.username}${destination}`,
          Overwrite: overwrite ? 'T' : 'F',
        },
      });
    }, 'copyFile');
  }

  // ============ Share API ============

  async getShares(path?: string): Promise<NextcloudShare[]> {
    await this.ensureInitialized();
    this.ensureConfigured();
    return this.executeWithRetry(async () => {
      const params: Record<string, string> = { format: 'json' };
      if (path) params.path = path;
      const response = await this.httpClient.get<OCSResponse<NextcloudShare[]>>(
        '/apps/files_sharing/api/v1/shares',
        { params },
      );
      return response.data.ocs.data;
    }, 'getShares');
  }

  async createShare(data: CreateShareDto): Promise<NextcloudShare> {
    await this.ensureInitialized();
    this.ensureConfigured();
    return this.executeWithRetry(async () => {
      const formData = new URLSearchParams();
      formData.append('path', data.path);
      formData.append('shareType', data.shareType.toString());
      if (data.shareWith) formData.append('shareWith', data.shareWith);
      if (data.permissions !== undefined) formData.append('permissions', data.permissions.toString());
      if (data.password) formData.append('password', data.password);
      if (data.expireDate) formData.append('expireDate', data.expireDate);
      if (data.note) formData.append('note', data.note);
      const response = await this.httpClient.post<OCSResponse<NextcloudShare>>(
        '/apps/files_sharing/api/v1/shares?format=json',
        formData.toString(),
        { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
      );
      return response.data.ocs.data;
    }, 'createShare');
  }

  async deleteShare(shareId: number): Promise<void> {
    await this.ensureInitialized();
    this.ensureConfigured();
    return this.executeWithRetry(async () => {
      await this.httpClient.delete(`/apps/files_sharing/api/v1/shares/${shareId}?format=json`);
    }, 'deleteShare');
  }

  // ============ User API ============

  async getCurrentUser(): Promise<NextcloudUser> {
    await this.ensureInitialized();
    this.ensureConfigured();
    return this.executeWithRetry(async () => {
      const response = await this.httpClient.get<OCSResponse<NextcloudUser>>('/cloud/user?format=json');
      return response.data.ocs.data;
    }, 'getCurrentUser');
  }

  async getUserQuota(): Promise<{ used: number; total: number; free: number; relative: number }> {
    await this.ensureInitialized();
    this.ensureConfigured();
    const user = await this.getCurrentUser();
    return user.quota;
  }

  // ============ Calendar API (CalDAV) ============

  async listCalendars(): Promise<NextcloudCalendar[]> {
    await this.ensureInitialized();
    this.ensureConfigured();

    return this.executeWithRetry(async () => {
      const authB64 = Buffer.from(`${this.username}:${this.password}`).toString('base64');
      const calDavClient = axios.create({
        baseURL: `${this.baseUrl}/remote.php/dav/calendars/${this.username}`,
        timeout: DEFAULT_TIMEOUT_MS,
        headers: { Authorization: `Basic ${authB64}` },
      });

      const propfindBodyParts = [
        '<?xml version="1.0" encoding="UTF-8"?>',
        '<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:c="urn:ietf:params:xml:ns:caldav">',
        '<d:prop><d:displayname/><d:resourcetype/><cs:getctag/><c:supported-calendar-component-set/></d:prop>',
        '</d:propfind>',
      ];

      const response = await calDavClient.request({
        method: 'PROPFIND',
        url: '/',
        headers: { Depth: '1', 'Content-Type': 'application/xml' },
        data: propfindBodyParts.join(''),
      });

      const calendars: NextcloudCalendar[] = [];
      const responseRegex = /<d:response>([\s\S]*?)<\/d:response>/g;
      let match;

      while ((match = responseRegex.exec(response.data)) !== null) {
        const resp = match[1];
        if (!resp.includes('<cal:calendar')) continue;

        const hrefMatch = /<d:href>(.*?)<\/d:href>/.exec(resp);
        const href = hrefMatch ? decodeURIComponent(hrefMatch[1]) : '';
        const displayName = this.extractXmlValue(resp, 'd:displayname') || '';

        if (displayName) {
          calendars.push({
            id: href.split('/').filter(Boolean).pop() || '',
            displayName,
            color: '#0082c9',
            owner: this.username,
            permissions: ['read', 'write'],
            isWritable: true,
            isShared: false,
          });
        }
      }

      return calendars;
    }, 'listCalendars');
  }

  async listEvents(params: ListEventsDto = {}): Promise<NextcloudCalendarEvent[]> {
    await this.ensureInitialized();
    this.ensureConfigured();

    const calendarId = params.calendarId || 'personal';

    return this.executeWithRetry(async () => {
      const authB64 = Buffer.from(`${this.username}:${this.password}`).toString('base64');
      const calDavClient = axios.create({
        baseURL: `${this.baseUrl}/remote.php/dav/calendars/${this.username}/${calendarId}`,
        timeout: DEFAULT_TIMEOUT_MS,
        headers: { Authorization: `Basic ${authB64}` },
      });

      let timeRangeFilter = '';
      if (params.start || params.end) {
        const start = (params.start || '19700101T000000Z').replace(/[-:]/g, '');
        const end = (params.end || '20991231T235959Z').replace(/[-:]/g, '');
        timeRangeFilter = `<c:time-range start="${start}" end="${end}"/>`;
      }

      const reportBodyParts = [
        '<?xml version="1.0" encoding="UTF-8"?>',
        '<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">',
        '<d:prop><d:getetag/><c:calendar-data/></d:prop>',
        '<c:filter><c:comp-filter name="VCALENDAR">',
        `<c:comp-filter name="VEVENT">${timeRangeFilter}</c:comp-filter>`,
        '</c:comp-filter></c:filter>',
        '</c:calendar-query>',
      ];

      const response = await calDavClient.request({
        method: 'REPORT',
        url: '/',
        headers: { Depth: '1', 'Content-Type': 'application/xml' },
        data: reportBodyParts.join(''),
      });

      const events: NextcloudCalendarEvent[] = [];
      const calDataRegex = /<cal:calendar-data[^>]*>([\s\S]*?)<\/cal:calendar-data>/g;
      let match;

      while ((match = calDataRegex.exec(response.data)) !== null) {
        const event = this.parseICalEvent(match[1], calendarId);
        if (event) events.push(event);
      }

      return events;
    }, 'listEvents');
  }

  private parseICalEvent(icalData: string, calendarId: string): NextcloudCalendarEvent | null {
    const uidMatch = /UID:(.+)/i.exec(icalData);
    const summaryMatch = /SUMMARY:(.+)/i.exec(icalData);
    const descriptionMatch = /DESCRIPTION:(.+)/i.exec(icalData);
    const locationMatch = /LOCATION:(.+)/i.exec(icalData);
    const dtStartMatch = /DTSTART[^:]*:(.+)/i.exec(icalData);
    const dtEndMatch = /DTEND[^:]*:(.+)/i.exec(icalData);
    const createdMatch = /CREATED:(.+)/i.exec(icalData);
    const lastModMatch = /LAST-MODIFIED:(.+)/i.exec(icalData);

    if (!uidMatch || !summaryMatch || !dtStartMatch) return null;

    return {
      id: uidMatch[1].trim(),
      calendarId,
      summary: summaryMatch[1].trim(),
      description: descriptionMatch ? descriptionMatch[1].trim() : null,
      location: locationMatch ? locationMatch[1].trim() : null,
      start: dtStartMatch[1].trim(),
      end: dtEndMatch ? dtEndMatch[1].trim() : dtStartMatch[1].trim(),
      allDay: icalData.includes('VALUE=DATE'),
      status: 'CONFIRMED',
      rrule: null,
      attendees: [],
      organizer: null,
      created: createdMatch ? createdMatch[1].trim() : '',
      lastModified: lastModMatch ? lastModMatch[1].trim() : '',
    };
  }
}
