/**
 * Production-Grade Dokploy API Client
 *
 * Features:
 * - Correct API parameter usage (environmentId, not projectId)
 * - Retry logic with exponential backoff
 * - Circuit breaker pattern
 * - Comprehensive error handling
 * - Idempotency checks
 * - Structured logging
 * - Rollback mechanisms
 */

interface DokployProject {
  projectId: string;
  name: string;
  description?: string;
  createdAt: string;
  organizationId: string;
  env: string;
}

interface DokployEnvironment {
  environmentId: string;
  name: string;
  projectId: string;
  isDefault: boolean;
  env?: string;
  createdAt: string;
}

interface DokployApplication {
  applicationId: string;
  name: string;
  environmentId: string;
  applicationStatus: 'idle' | 'running' | 'done' | 'error';
  dockerImage?: string;
  sourceType?: 'docker' | 'git' | 'github';
  createdAt: string;
}

interface DokployDomain {
  domainId: string;
  host: string;
  applicationId: string;
  https: boolean;
  port: number;
}

interface RetryConfig {
  maxRetries: number;
  initialDelay: number;
  maxDelay: number;
  multiplier: number;
}

interface CircuitBreakerConfig {
  threshold: number;
  timeout: number;
  halfOpenAttempts: number;
}

interface LogEntry {
  timestamp: string;
  level: 'info' | 'warn' | 'error';
  phase: string;
  action: string;
  message: string;
  duration_ms?: number;
  error?: {
    code: string;
    message: string;
    apiResponse?: unknown;
  };
}

class CircuitBreaker {
  private failures = 0;
  private lastFailureTime: number | null = null;
  private state: 'closed' | 'open' | 'half-open' = 'closed';

  constructor(private config: CircuitBreakerConfig) {}

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      const now = Date.now();
      if (this.lastFailureTime && now - this.lastFailureTime < this.config.timeout) {
        throw new Error('Circuit breaker is OPEN - too many failures');
      }
      this.state = 'half-open';
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }

  private onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
    if (this.failures >= this.config.threshold) {
      this.state = 'open';
    }
  }

  getState() {
    return this.state;
  }
}

export class DokployProductionClient {
  private baseUrl: string;
  private apiToken: string;
  private retryConfig: RetryConfig;
  private circuitBreaker: CircuitBreaker;
  private logs: LogEntry[] = [];

  constructor(
    baseUrl: string,
    apiToken: string,
    retryConfig?: Partial<RetryConfig>,
    circuitBreakerConfig?: Partial<CircuitBreakerConfig>
  ) {
    if (!baseUrl) throw new Error('DOKPLOY_URL is required');
    if (!apiToken) throw new Error('DOKPLOY_API_TOKEN is required');

    this.baseUrl = baseUrl.replace(/\/$/, '');
    this.apiToken = apiToken;

    this.retryConfig = {
      maxRetries: retryConfig?.maxRetries ?? 5,
      initialDelay: retryConfig?.initialDelay ?? 1000,
      maxDelay: retryConfig?.maxDelay ?? 16000,
      multiplier: retryConfig?.multiplier ?? 2,
    };

    this.circuitBreaker = new CircuitBreaker({
      threshold: circuitBreakerConfig?.threshold ?? 5,
      timeout: circuitBreakerConfig?.timeout ?? 60000,
      halfOpenAttempts: circuitBreakerConfig?.halfOpenAttempts ?? 3,
    });
  }

  private log(entry: Omit<LogEntry, 'timestamp'>) {
    const logEntry: LogEntry = {
      ...entry,
      timestamp: new Date().toISOString(),
    };
    this.logs.push(logEntry);

    // Output to console for monitoring
    const level = entry.level.toUpperCase();
    const msg = `[${level}] ${entry.phase}/${entry.action}: ${entry.message}`;
    if (entry.level === 'error') {
      console.error(msg, entry.error);
    } else {
      console.log(msg);
    }
  }

  getLogs(): LogEntry[] {
    return [...this.logs];
  }

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

  private isRetryableError(error: unknown, statusCode?: number): boolean {
    // Retry on network errors and 5xx server errors
    if (statusCode && statusCode >= 500) return true;
    if (statusCode === 429) return true; // Rate limiting
    if (error instanceof TypeError && error.message.includes('fetch')) return true;
    return false;
  }

  private async request<T>(
    method: string,
    endpoint: string,
    body?: unknown,
    phase = 'api',
    action = 'request'
  ): Promise<T> {
    const url = `${this.baseUrl}/api${endpoint}`;
    const startTime = Date.now();

    return this.circuitBreaker.execute(async () => {
      let lastError: Error | null = null;
      let delay = this.retryConfig.initialDelay;

      for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
        try {
          const response = await fetch(url, {
            method,
            headers: {
              'x-api-key': this.apiToken,
              'Content-Type': 'application/json',
            },
            body: body ? JSON.stringify(body) : undefined,
          });

          const text = await response.text();

          if (!response.ok) {
            let errorMessage = `API error (${response.status})`;
            let apiResponse: unknown;

            try {
              apiResponse = JSON.parse(text);
              errorMessage = (apiResponse as { message?: string }).message || errorMessage;
            } catch {
              errorMessage = text || errorMessage;
            }

            // Don't retry 4xx errors (except 429)
            if (response.status >= 400 && response.status < 500 && response.status !== 429) {
              const duration = Date.now() - startTime;
              this.log({
                level: 'error',
                phase,
                action,
                message: `Request failed: ${errorMessage}`,
                duration_ms: duration,
                error: {
                  code: `HTTP_${response.status}`,
                  message: errorMessage,
                  apiResponse,
                },
              });
              throw new Error(errorMessage);
            }

            // Retry 5xx and 429
            if (attempt < this.retryConfig.maxRetries && this.isRetryableError(null, response.status)) {
              this.log({
                level: 'warn',
                phase,
                action,
                message: `Retrying after ${response.status} (attempt ${attempt + 1}/${this.retryConfig.maxRetries})`,
              });
              await this.sleep(delay);
              delay = Math.min(delay * this.retryConfig.multiplier, this.retryConfig.maxDelay);
              continue;
            }

            throw new Error(errorMessage);
          }

          const duration = Date.now() - startTime;
          this.log({
            level: 'info',
            phase,
            action,
            message: 'Request successful',
            duration_ms: duration,
          });

          return text ? (JSON.parse(text) as T) : ({} as T);

        } catch (error) {
          lastError = error instanceof Error ? error : new Error(String(error));

          if (attempt < this.retryConfig.maxRetries && this.isRetryableError(error)) {
            this.log({
              level: 'warn',
              phase,
              action,
              message: `Retrying after error (attempt ${attempt + 1}/${this.retryConfig.maxRetries}): ${lastError.message}`,
            });
            await this.sleep(delay);
            delay = Math.min(delay * this.retryConfig.multiplier, this.retryConfig.maxDelay);
            continue;
          }

          const duration = Date.now() - startTime;
          this.log({
            level: 'error',
            phase,
            action,
            message: `Request failed: ${lastError.message}`,
            duration_ms: duration,
            error: {
              code: 'REQUEST_FAILED',
              message: lastError.message,
            },
          });

          throw lastError;
        }
      }

      throw lastError || new Error('Request failed after all retries');
    });
  }

  // ==================== PRODUCTION API METHODS ====================

  async createProject(
    name: string,
    description?: string
  ): Promise<{ project: DokployProject; environment: DokployEnvironment }> {
    const response = await this.request<{ project: DokployProject; environment: DokployEnvironment }>(
      'POST',
      '/project.create',
      { name, description },
      'project',
      'create'
    );
    // API returns both project and default environment in one call
    return response;
  }

  async getProjects(): Promise<DokployProject[]> {
    return this.request<DokployProject[]>('GET', '/project.all', undefined, 'project', 'list');
  }

  async findProjectByName(name: string): Promise<{ project: DokployProject; environmentId?: string } | null> {
    const projects = await this.getProjects();
    const project = projects.find(p => p.name === name);

    if (!project) return null;

    // Try to get environment ID for this project
    try {
      const env = await this.getDefaultEnvironment(project.projectId);
      return { project, environmentId: env.environmentId };
    } catch {
      // If we can't get environment, just return project
      return { project };
    }
  }

  async getEnvironmentsByProjectId(projectId: string): Promise<DokployEnvironment[]> {
    return this.request<DokployEnvironment[]>(
      'GET',
      `/environment.byProjectId?projectId=${projectId}`,
      undefined,
      'environment',
      'query'
    );
  }

  async getDefaultEnvironment(projectId: string): Promise<DokployEnvironment> {
    const environments = await this.getEnvironmentsByProjectId(projectId);
    const defaultEnv = environments.find(e => e.isDefault);

    if (!defaultEnv) {
      throw new Error(`No default environment found for project ${projectId}`);
    }

    return defaultEnv;
  }

  async createApplication(name: string, environmentId: string): Promise<DokployApplication> {
    return this.request<DokployApplication>(
      'POST',
      '/application.create',
      { name, environmentId },
      'application',
      'create'
    );
  }

  async updateApplication(
    applicationId: string,
    updates: {
      dockerImage?: string;
      sourceType?: 'docker' | 'git' | 'github';
      [key: string]: unknown;
    }
  ): Promise<DokployApplication> {
    return this.request<DokployApplication>(
      'POST',
      '/application.update',
      { applicationId, ...updates },
      'application',
      'update'
    );
  }

  async setApplicationEnv(applicationId: string, env: string): Promise<void> {
    await this.request(
      'POST',
      '/application.update',
      { applicationId, env },
      'application',
      'set-env'
    );
  }

  async getApplication(applicationId: string): Promise<DokployApplication> {
    return this.request<DokployApplication>(
      'GET',
      `/application.one?applicationId=${applicationId}`,
      undefined,
      'application',
      'get'
    );
  }

  async getApplicationsByEnvironmentId(environmentId: string): Promise<DokployApplication[]> {
    const env = await this.request<{ applications: DokployApplication[] }>(
      'GET',
      `/environment.one?environmentId=${environmentId}`,
      undefined,
      'environment',
      'get-apps'
    );
    return env.applications || [];
  }

  async findApplicationByName(environmentId: string, name: string): Promise<DokployApplication | null> {
    const apps = await this.getApplicationsByEnvironmentId(environmentId);
    return apps.find(a => a.name === name) || null;
  }

  async createDomain(
    host: string,
    applicationId: string,
    https = true,
    port = 8080
  ): Promise<DokployDomain> {
    return this.request<DokployDomain>(
      'POST',
      '/domain.create',
      { host, applicationId, https, port },
      'domain',
      'create'
    );
  }

  async deployApplication(applicationId: string): Promise<void> {
    await this.request(
      'POST',
      '/application.deploy',
      { applicationId },
      'deploy',
      'trigger'
    );
  }

  async deleteApplication(applicationId: string): Promise<void> {
    await this.request(
      'POST',
      '/application.delete',
      { applicationId },
      'application',
      'delete'
    );
  }

  async deleteProject(projectId: string): Promise<void> {
    await this.request(
      'POST',
      '/project.remove',
      { projectId },
      'project',
      'delete'
    );
  }

  async deleteDomain(domainId: string): Promise<void> {
    await this.request(
      'POST',
      '/domain.delete',
      { domainId },
      'domain',
      'delete'
    );
  }

  async createMount(
    applicationId: string,
    volumeName: string,
    mountPath: string
  ): Promise<{ mountId: string }> {
    return this.request<{ mountId: string }>(
      'POST',
      '/mounts.create',
      {
        type: 'volume',
        volumeName,
        mountPath,
        serviceId: applicationId,
        serviceType: 'application',
      },
      'mount',
      'create'
    );
  }

  getCircuitBreakerState() {
    return this.circuitBreaker.getState();
  }
}

export function createProductionDokployClient(): DokployProductionClient {
  const baseUrl = process.env.DOKPLOY_URL;
  const apiToken = process.env.DOKPLOY_API_TOKEN;

  if (!baseUrl) {
    throw new Error('DOKPLOY_URL environment variable is not set');
  }
  if (!apiToken) {
    throw new Error('DOKPLOY_API_TOKEN environment variable is not set');
  }

  return new DokployProductionClient(baseUrl, apiToken);
}
