import { exec } from 'child_process';
import { promisify } from 'util';
import crypto from 'crypto';
import {
  DeploymentEnvironment,
  Deployment,
  DeploymentConfig,
  DeploymentPlan,
  DeploymentMetrics,
  DeploymentStatus,
  DeploymentType,
  DeploymentLog,
  HealthCheck,
  ReleaseNotes,
  EnvironmentStatus,
} from './types.js';

const execAsync = promisify(exec);

export class DeploymentManager {
  private config: DeploymentConfig;
  private activeDeployments: Map<string, Deployment> = new Map();

  constructor(config: DeploymentConfig) {
    this.config = config;
  }

  async deploy(
    environmentName: string,
    version: string,
    options: {
      force?: boolean;
      skipHealthChecks?: boolean;
      dryRun?: boolean;
      author?: string;
    } = {}
  ): Promise<Deployment> {
    const environment = this.config.environments.find(e => e.name === environmentName);
    if (!environment) {
      throw new Error(`Environment ${environmentName} not found`);
    }

    // Check if environment is ready
    if (environment.status === 'deploying' && !options.force) {
      throw new Error(`Environment ${environmentName} is currently deploying`);
    }

    // Create deployment record
    const deployment: Deployment = {
      id: crypto.randomUUID(),
      environmentId: environment.id,
      version,
      status: 'pending',
      type: 'manual',
      startedAt: new Date().toISOString(),
      author: options.author,
      logs: [],
    };

    // Add to active deployments
    this.activeDeployments.set(deployment.id, deployment);

    try {
      // Update environment status
      environment.status = 'deploying';

      // Run pre-deployment hooks
      if (this.config.hooks?.preDeploy && !options.dryRun) {
        await this.runHooks(this.config.hooks.preDeploy, deployment);
      }

      // Build phase
      deployment.status = 'building';
      this.addLog(deployment, 'info', `Starting build for version ${version}`);

      if (!options.dryRun) {
        await this.build(environment, deployment);
      }

      // Deploy phase
      deployment.status = 'deploying';
      this.addLog(deployment, 'info', `Deploying to ${environmentName}`);

      if (!options.dryRun) {
        await this.deployToProvider(environment, deployment);
      }

      // Health checks
      if (!options.skipHealthChecks && this.config.healthChecks) {
        this.addLog(deployment, 'info', 'Running health checks');
        await this.runHealthChecks(environment, deployment);
      }

      // Run post-deployment hooks
      if (this.config.hooks?.postDeploy && !options.dryRun) {
        await this.runHooks(this.config.hooks.postDeploy, deployment);
      }

      // Mark as successful
      deployment.status = 'success';
      deployment.completedAt = new Date().toISOString();
      deployment.duration = Date.now() - new Date(deployment.startedAt).getTime();
      
      environment.status = 'active';
      environment.lastDeployment = deployment;
      environment.updatedAt = new Date().toISOString();

      this.addLog(deployment, 'info', `Deployment completed successfully in ${deployment.duration}ms`);

    } catch (error: any) {
      deployment.status = 'failed';
      deployment.completedAt = new Date().toISOString();
      deployment.duration = Date.now() - new Date(deployment.startedAt).getTime();
      
      environment.status = 'failed';
      
      this.addLog(deployment, 'error', `Deployment failed: ${error.message}`);
      throw error;
    } finally {
      this.activeDeployments.delete(deployment.id);
    }

    return deployment;
  }

  async rollback(
    environmentName: string,
    targetVersion: string,
    options: { skipHealthChecks?: boolean; author?: string } = {}
  ): Promise<Deployment> {
    const environment = this.config.environments.find(e => e.name === environmentName);
    if (!environment) {
      throw new Error(`Environment ${environmentName} not found`);
    }

    if (!environment.lastDeployment) {
      throw new Error(`No previous deployment found for ${environmentName}`);
    }

    // Create rollback deployment
    const deployment: Deployment = {
      id: crypto.randomUUID(),
      environmentId: environment.id,
      version: targetVersion,
      status: 'pending',
      type: 'rollback',
      startedAt: new Date().toISOString(),
      rollbackFrom: environment.lastDeployment.version,
      author: options.author,
      logs: [],
    };

    try {
      // Run pre-rollback hooks
      if (this.config.hooks?.preRollback) {
        await this.runHooks(this.config.hooks.preRollback, deployment);
      }

      // Perform rollback
      deployment.status = 'deploying';
      this.addLog(deployment, 'info', `Rolling back from ${environment.lastDeployment.version} to ${targetVersion}`);
      
      await this.deployToProvider(environment, deployment);

      // Health checks
      if (!options.skipHealthChecks && this.config.healthChecks) {
        await this.runHealthChecks(environment, deployment);
      }

      // Run post-rollback hooks
      if (this.config.hooks?.postRollback) {
        await this.runHooks(this.config.hooks.postRollback, deployment);
      }

      deployment.status = 'success';
      deployment.completedAt = new Date().toISOString();
      deployment.duration = Date.now() - new Date(deployment.startedAt).getTime();
      
      environment.lastDeployment = deployment;
      environment.status = 'active';

    } catch (error: any) {
      deployment.status = 'failed';
      this.addLog(deployment, 'error', `Rollback failed: ${error.message}`);
      throw error;
    }

    return deployment;
  }

  async createDeploymentPlan(
    environmentName: string,
    version: string,
    previousVersion?: string
  ): Promise<DeploymentPlan> {
    const environment = this.config.environments.find(e => e.name === environmentName);
    if (!environment) {
      throw new Error(`Environment ${environmentName} not found`);
    }

    // Get changes between versions
    const changes = await this.getChanges(version, previousVersion || environment.lastDeployment?.version);

    // Assess risks
    const risks = this.assessRisks(changes, environment);

    // Estimate duration based on historical data
    const estimatedDuration = this.estimateDeploymentDuration(environment, changes);

    // Determine if approval is required
    const approvalRequired = environment.type === 'production' || risks.some(r => r.impact === 'high');

    // Create rollback plan
    const rollbackPlan = previousVersion ? {
      strategy: 'immediate' as const,
      targetVersion: previousVersion,
      steps: [
        'Capture current state',
        'Deploy previous version',
        'Verify health checks',
        'Update DNS/routing if needed',
      ],
      estimatedDuration: estimatedDuration * 0.5,
    } : undefined;

    return {
      environment: environmentName,
      version,
      changes,
      estimatedDuration,
      risks,
      approvalRequired,
      rollbackPlan,
    };
  }

  async getDeploymentMetrics(environmentName?: string): Promise<DeploymentMetrics> {
    // This would normally fetch from storage
    const deployments: Deployment[] = []; // Fetch from storage

    const metrics: DeploymentMetrics = {
      totalDeployments: deployments.length,
      successRate: 0,
      averageDuration: 0,
      failureReasons: {},
      deploymentsByEnvironment: {},
      deploymentsByType: {
        manual: 0,
        auto: 0,
        rollback: 0,
        hotfix: 0,
      },
      recentDeployments: [],
    };

    if (deployments.length === 0) {
      return metrics;
    }

    // Calculate metrics
    const successful = deployments.filter(d => d.status === 'success').length;
    metrics.successRate = (successful / deployments.length) * 100;

    // Average duration
    const durations = deployments
      .filter(d => d.duration)
      .map(d => d.duration!);
    metrics.averageDuration = durations.reduce((a, b) => a + b, 0) / durations.length;

    // Group by environment and type
    deployments.forEach(deployment => {
      const envName = this.config.environments.find(e => e.id === deployment.environmentId)?.name || 'unknown';
      metrics.deploymentsByEnvironment[envName] = (metrics.deploymentsByEnvironment[envName] || 0) + 1;
      metrics.deploymentsByType[deployment.type]++;
    });

    // Recent deployments
    metrics.recentDeployments = deployments
      .sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())
      .slice(0, 10);

    return metrics;
  }

  async generateReleaseNotes(version: string, previousVersion?: string): Promise<ReleaseNotes> {
    const changes = await this.getChanges(version, previousVersion);
    
    const releaseNotes: ReleaseNotes = {
      version,
      date: new Date().toISOString(),
      changes: {
        features: [],
        fixes: [],
        breaking: [],
        deprecated: [],
      },
      contributors: [],
    };

    // Categorize changes
    changes.forEach(change => {
      switch (change.type) {
        case 'feature':
          releaseNotes.changes.features.push(change.description);
          break;
        case 'bugfix':
          releaseNotes.changes.fixes.push(change.description);
          break;
      }
    });

    // Get contributors from git
    try {
      const { stdout } = await execAsync(
        `git log ${previousVersion}..${version} --pretty=format:"%an" | sort -u`
      );
      releaseNotes.contributors = stdout.split('\n').filter(Boolean);
    } catch {
      // Git not available
    }

    return releaseNotes;
  }

  private async build(environment: DeploymentEnvironment, deployment: Deployment): Promise<void> {
    const buildCommand = this.config.settings.buildCommand || 'npm run build';
    
    try {
      this.addLog(deployment, 'info', `Running build command: ${buildCommand}`);
      const { stdout, stderr } = await execAsync(buildCommand, {
        timeout: 300000, // 5 minutes
      });
      
      if (stdout) this.addLog(deployment, 'info', stdout);
      if (stderr) this.addLog(deployment, 'warning', stderr);
    } catch (error: any) {
      this.addLog(deployment, 'error', `Build failed: ${error.message}`);
      throw error;
    }
  }

  private async deployToProvider(environment: DeploymentEnvironment, deployment: Deployment): Promise<void> {
    switch (this.config.provider) {
      case 'vercel':
        await this.deployToVercel(environment, deployment);
        break;
      case 'netlify':
        await this.deployToNetlify(environment, deployment);
        break;
      case 'docker':
        await this.deployToDocker(environment, deployment);
        break;
      case 'custom':
        await this.deployCustom(environment, deployment);
        break;
      default:
        throw new Error(`Unsupported provider: ${this.config.provider}`);
    }
  }

  private async deployToVercel(environment: DeploymentEnvironment, deployment: Deployment): Promise<void> {
    const command = `vercel --token ${this.config.settings.apiKey} --prod=${environment.type === 'production'}`;
    
    try {
      const { stdout } = await execAsync(command);
      const deployUrl = stdout.match(/https:\/\/[^\s]+/)?.[0];
      if (deployUrl) {
        environment.url = deployUrl;
        this.addLog(deployment, 'info', `Deployed to ${deployUrl}`);
      }
    } catch (error: any) {
      throw new Error(`Vercel deployment failed: ${error.message}`);
    }
  }

  private async deployToNetlify(environment: DeploymentEnvironment, deployment: Deployment): Promise<void> {
    const command = `netlify deploy --auth ${this.config.settings.apiKey} --dir ${this.config.settings.outputDirectory || 'dist'} --prod`;
    
    try {
      const { stdout } = await execAsync(command);
      this.addLog(deployment, 'info', 'Netlify deployment completed');
    } catch (error: any) {
      throw new Error(`Netlify deployment failed: ${error.message}`);
    }
  }

  private async deployToDocker(environment: DeploymentEnvironment, deployment: Deployment): Promise<void> {
    const registry = this.config.settings.registry;
    const image = `${registry}/${this.config.settings.projectName}:${deployment.version}`;
    
    try {
      // Build image
      await execAsync(`docker build -t ${image} .`);
      this.addLog(deployment, 'info', `Built Docker image: ${image}`);
      
      // Push image
      await execAsync(`docker push ${image}`);
      this.addLog(deployment, 'info', `Pushed image to registry`);
      
      // Deploy (simplified - would use kubernetes/swarm/etc)
      this.addLog(deployment, 'info', `Deployment complete`);
    } catch (error: any) {
      throw new Error(`Docker deployment failed: ${error.message}`);
    }
  }

  private async deployCustom(environment: DeploymentEnvironment, deployment: Deployment): Promise<void> {
    const command = this.config.settings.customCommand;
    if (!command) {
      throw new Error('Custom deployment command not configured');
    }
    
    try {
      const { stdout, stderr } = await execAsync(command, {
        env: {
          ...process.env,
          DEPLOYMENT_ENV: environment.name,
          DEPLOYMENT_VERSION: deployment.version,
        },
      });
      
      if (stdout) this.addLog(deployment, 'info', stdout);
      if (stderr) this.addLog(deployment, 'warning', stderr);
    } catch (error: any) {
      throw new Error(`Custom deployment failed: ${error.message}`);
    }
  }

  private async runHooks(hooks: string[], deployment: Deployment): Promise<void> {
    for (const hook of hooks) {
      try {
        this.addLog(deployment, 'info', `Running hook: ${hook}`);
        const { stdout, stderr } = await execAsync(hook);
        if (stdout) this.addLog(deployment, 'info', stdout);
        if (stderr) this.addLog(deployment, 'warning', stderr);
      } catch (error: any) {
        this.addLog(deployment, 'error', `Hook failed: ${error.message}`);
        throw error;
      }
    }
  }

  private async runHealthChecks(environment: DeploymentEnvironment, deployment: Deployment): Promise<void> {
    if (!this.config.healthChecks || this.config.healthChecks.length === 0) {
      return;
    }

    for (const check of this.config.healthChecks) {
      try {
        await this.runHealthCheck(check, environment);
        this.addLog(deployment, 'info', `Health check passed: ${check.name}`);
      } catch (error: any) {
        this.addLog(deployment, 'error', `Health check failed: ${check.name} - ${error.message}`);
        throw error;
      }
    }
  }

  private async runHealthCheck(check: HealthCheck, environment: DeploymentEnvironment): Promise<void> {
    const maxRetries = check.retries || 3;
    const timeout = check.timeout || 30000;
    
    for (let i = 0; i < maxRetries; i++) {
      try {
        switch (check.type) {
          case 'http':
            await this.httpHealthCheck(check, environment);
            return;
          case 'tcp':
            await this.tcpHealthCheck(check, environment);
            return;
          case 'custom':
            await this.customHealthCheck(check);
            return;
        }
      } catch (error) {
        if (i === maxRetries - 1) throw error;
        await new Promise(resolve => setTimeout(resolve, check.interval || 5000));
      }
    }
  }

  private async httpHealthCheck(check: HealthCheck, environment: DeploymentEnvironment): Promise<void> {
    const url = check.endpoint || environment.url;
    if (!url) throw new Error('No URL for HTTP health check');

    const response = await fetch(url, {
      signal: AbortSignal.timeout(check.timeout || 30000),
    });

    const expectedStatus = check.expectedStatus || 200;
    if (response.status !== expectedStatus) {
      throw new Error(`Expected status ${expectedStatus}, got ${response.status}`);
    }
  }

  private async tcpHealthCheck(check: HealthCheck, environment: DeploymentEnvironment): Promise<void> {
    // Simplified TCP check - would use net module
    if (!check.port) throw new Error('No port specified for TCP health check');
    
    // In real implementation, would attempt TCP connection
    await new Promise(resolve => setTimeout(resolve, 100));
  }

  private async customHealthCheck(check: HealthCheck): Promise<void> {
    if (!check.command) throw new Error('No command for custom health check');
    
    await execAsync(check.command, {
      timeout: check.timeout || 30000,
    });
  }

  private async getChanges(version: string, previousVersion?: string): Promise<any[]> {
    const changes: any[] = [];
    
    if (!previousVersion) {
      changes.push({
        type: 'feature',
        description: 'Initial deployment',
        impact: 'high',
      });
      return changes;
    }

    try {
      // Get git commits between versions
      const { stdout } = await execAsync(
        `git log ${previousVersion}..${version} --pretty=format:"%s" --no-merges`
      );
      
      const commits = stdout.split('\n').filter(Boolean);
      
      commits.forEach(commit => {
        let type: 'feature' | 'bugfix' | 'config' | 'infrastructure' = 'feature';
        let impact: 'low' | 'medium' | 'high' = 'medium';
        
        if (commit.toLowerCase().includes('fix')) {
          type = 'bugfix';
          impact = 'low';
        } else if (commit.toLowerCase().includes('feat')) {
          type = 'feature';
          impact = 'medium';
        } else if (commit.toLowerCase().includes('breaking')) {
          impact = 'high';
        }
        
        changes.push({
          type,
          description: commit,
          impact,
        });
      });
    } catch {
      // Git not available
      changes.push({
        type: 'feature',
        description: `Update from ${previousVersion} to ${version}`,
        impact: 'medium',
      });
    }
    
    return changes;
  }

  private assessRisks(changes: any[], environment: DeploymentEnvironment): any[] {
    const risks: any[] = [];
    
    // High-impact changes in production
    if (environment.type === 'production' && changes.some(c => c.impact === 'high')) {
      risks.push({
        description: 'High-impact changes being deployed to production',
        probability: 'medium',
        impact: 'high',
        mitigation: 'Ensure thorough testing in staging environment',
      });
    }
    
    // Large number of changes
    if (changes.length > 20) {
      risks.push({
        description: 'Large number of changes in single deployment',
        probability: 'medium',
        impact: 'medium',
        mitigation: 'Consider breaking into smaller deployments',
      });
    }
    
    // Infrastructure changes
    if (changes.some(c => c.type === 'infrastructure')) {
      risks.push({
        description: 'Infrastructure changes may affect stability',
        probability: 'low',
        impact: 'high',
        mitigation: 'Monitor closely after deployment',
      });
    }
    
    return risks;
  }

  private estimateDeploymentDuration(environment: DeploymentEnvironment, changes: any[]): number {
    // Base duration in milliseconds
    let duration = 300000; // 5 minutes base
    
    // Add time based on environment
    if (environment.type === 'production') {
      duration += 180000; // +3 minutes for production checks
    }
    
    // Add time based on changes
    duration += changes.length * 10000; // 10 seconds per change
    
    // Add time for health checks
    if (this.config.healthChecks) {
      duration += this.config.healthChecks.length * 30000; // 30 seconds per check
    }
    
    return duration;
  }

  private addLog(deployment: Deployment, level: 'info' | 'warning' | 'error', message: string, details?: any): void {
    deployment.logs?.push({
      timestamp: new Date().toISOString(),
      level,
      message,
      details,
    });
  }
}