#!/usr/bin/env node

/**
 * Universal Zero Downtime Deployment Script
 * Supports: development, staging, production
 * All configurations loaded from environment variables
 */
import { execSync } from 'child_process';
import { config } from 'dotenv';
import fs from 'fs';
import http from 'http';
import path from 'path';
import { fileURLToPath } from 'url';

// Load environment-specific .env file
function loadEnvConfig(environment) {
  const envFiles = [`.env.${environment}.local`, `.env.${environment}`, '.env.local', '.env'];

  for (const envFile of envFiles) {
    const envPath = path.join(process.cwd(), envFile);
    if (fs.existsSync(envPath)) {
      logMessage(`📄 Loading config from: ${envFile}`);
      config({ path: envPath });
      break;
    }
  }
}

// Environment Configuration (loaded from .env files)
const getConfig = (environment) => ({
  // App Configuration
  APP_NAME: process.env.APP_NAME || `app-${environment}`,
  APP_PORT: parseInt(process.env.APP_PORT || '9020', 10),

  // PM2 Configuration
  PM2_INSTANCES: parseInt(process.env.PM2_INSTANCES || '2', 10),
  PM2_EXEC_MODE: process.env.PM2_EXEC_MODE || 'cluster',
  PM2_MAX_MEMORY: process.env.PM2_MAX_MEMORY || '500M',
  PM2_WAIT_READY: process.env.PM2_WAIT_READY === 'true',
  PM2_LISTEN_TIMEOUT: parseInt(process.env.PM2_LISTEN_TIMEOUT || '10000', 10),
  PM2_KILL_TIMEOUT: parseInt(process.env.PM2_KILL_TIMEOUT || '5000', 10),

  // Build Configuration
  BUILD_COMMAND: process.env.BUILD_COMMAND || 'yarn build',
  INSTALL_COMMAND: process.env.INSTALL_COMMAND || 'yarn install --frozen-lockfile',
  TMP_BUILD_DIR: process.env.TMP_BUILD_DIR || 'dist_building',
  FINAL_DIST_DIR: process.env.FINAL_DIST_DIR || 'dist',
  BACKUP_DIST_DIR: process.env.BACKUP_DIST_DIR || 'dist_backup',

  // Health Check Configuration
  HEALTH_CHECK_ENABLED: process.env.HEALTH_CHECK_ENABLED !== 'false',
  HEALTH_CHECK_PATH: process.env.HEALTH_CHECK_PATH || '/',
  HEALTH_CHECK_RETRIES: parseInt(process.env.HEALTH_CHECK_RETRIES || '5', 10),
  HEALTH_CHECK_INTERVAL: parseInt(process.env.HEALTH_CHECK_INTERVAL || '2000', 10),
  HEALTH_CHECK_TIMEOUT: parseInt(process.env.HEALTH_CHECK_TIMEOUT || '5000', 10),

  // Deployment Configuration
  AUTO_ROLLBACK: process.env.AUTO_ROLLBACK !== 'false',
  CLEANUP_BACKUP: process.env.CLEANUP_BACKUP !== 'false',
  PM2_SAVE: process.env.PM2_SAVE !== 'false',

  // Logging Configuration
  LOG_DIR: process.env.LOG_DIR || 'logs',
  LOG_FILE_NAME: process.env.LOG_FILE_NAME || 'deployment.log',
  ENABLE_CONSOLE_LOG: process.env.ENABLE_CONSOLE_LOG !== 'false',

  // Environment
  ENVIRONMENT: environment,
  NODE_ENV: process.env.NODE_ENV || environment,
});

let CONFIG;

// Helper functions
// eslint-disable-next-line @typescript-eslint/no-shadow
function logMessage(message, config = CONFIG) {
  const timestamp = new Date().toISOString();

  // eslint-disable-next-line @typescript-eslint/no-shadow
  const logMessage = `[${timestamp}] [${config?.ENVIRONMENT || 'INIT'}] ${message}`;

  if (!config || config.ENABLE_CONSOLE_LOG) {
    console.log(logMessage);
  }

  if (config) {
    const logDir = path.join(process.cwd(), config.LOG_DIR);
    if (!fs.existsSync(logDir)) {
      fs.mkdirSync(logDir, { recursive: true });
    }
    const logFile = path.join(logDir, config.LOG_FILE_NAME);
    fs.appendFileSync(logFile, `${logMessage}\n`);
  }
}

function executeCommand(command, throwOnError = true) {
  logMessage(`⚙️  Executing: ${command}`);
  try {
    const output = execSync(command, {
      encoding: 'utf8',
      stdio: CONFIG.ENABLE_CONSOLE_LOG ? 'inherit' : 'pipe',
    });
    return output ? output.trim() : '';
  } catch (error) {
    logMessage(`❌ Error executing command: ${error.message}`);
    if (throwOnError) throw error;
    return null;
  }
}

function checkAppStatus(appName) {
  try {
    const status = executeCommand(`pm2 describe ${appName}`, false);
    return !!status;
  } catch (error) {
    return false;
  }
}

// Health check function
async function checkHealth(retries = CONFIG.HEALTH_CHECK_RETRIES) {
  if (!CONFIG.HEALTH_CHECK_ENABLED) {
    logMessage('⚠️  Health check disabled, skipping...');
    return true;
  }

  logMessage(`🏥 Starting health check (${retries} retries)...`);

  for (let i = 0; i < retries; i++) {
    try {
      await new Promise((resolve, reject) => {
        const url = `http://localhost:${CONFIG.APP_PORT}${CONFIG.HEALTH_CHECK_PATH}`;
        const req = http.get(url, (res) => {
          if (res.statusCode === 200) {
            logMessage(`✅ Health check passed (attempt ${i + 1}/${retries})`);
            resolve(true);
          } else {
            reject(new Error(`Status code: ${res.statusCode}`));
          }
        });

        req.on('error', reject);
        req.setTimeout(CONFIG.HEALTH_CHECK_TIMEOUT, () => {
          req.destroy();
          reject(new Error('Timeout'));
        });
      });

      return true;
    } catch (error) {
      logMessage(`⚠️  Health check failed (attempt ${i + 1}/${retries}): ${error.message}`);

      if (i < retries - 1) {
        logMessage(`⏳ Waiting ${CONFIG.HEALTH_CHECK_INTERVAL}ms before retry...`);
        await new Promise((resolve) => setTimeout(resolve, CONFIG.HEALTH_CHECK_INTERVAL));
      }
    }
  }

  return false;
}

// Atomic folder switch with backup
function atomicFolderSwitch() {
  logMessage('🔄 Performing atomic folder switch...');

  const tmpPath = path.join(process.cwd(), CONFIG.TMP_BUILD_DIR);
  const finalPath = path.join(process.cwd(), CONFIG.FINAL_DIST_DIR);
  const backupPath = path.join(process.cwd(), CONFIG.BACKUP_DIST_DIR);

  // Preserve old assets to prevent 404s during transition
  logMessage('📦 Preserving old assets to prevent 404s...');
  const oldAssetsDir = path.join(finalPath, 'assets');
  const newAssetsDir = path.join(tmpPath, 'assets');

  if (fs.existsSync(oldAssetsDir) && fs.existsSync(newAssetsDir)) {
    try {
      const files = fs.readdirSync(oldAssetsDir);
      let copiedCount = 0;

      for (const file of files) {
        const srcFile = path.join(oldAssetsDir, file);
        const destFile = path.join(newAssetsDir, file);

        if (!fs.existsSync(destFile)) {
          fs.copyFileSync(srcFile, destFile);
          copiedCount += 1;
        }
      }
      logMessage(`✅ Preserved ${copiedCount} old asset files.`);
    } catch (error) {
      logMessage(`⚠️ Failed to preserve assets: ${error.message}`);
    }
  }

  // Create backup of current dist
  if (fs.existsSync(finalPath)) {
    if (fs.existsSync(backupPath)) {
      fs.rmSync(backupPath, { recursive: true, force: true });
    }
    fs.renameSync(finalPath, backupPath);
    logMessage(`📦 Created backup: ${CONFIG.FINAL_DIST_DIR} → ${CONFIG.BACKUP_DIST_DIR}`);
  }

  // Switch to new build (atomic operation)
  if (!fs.existsSync(tmpPath)) {
    throw new Error(`Build directory ${CONFIG.TMP_BUILD_DIR} not found!`);
  }

  fs.renameSync(tmpPath, finalPath);
  logMessage(`✅ Switched to new build: ${CONFIG.TMP_BUILD_DIR} → ${CONFIG.FINAL_DIST_DIR}`);
}

// Rollback function
function rollback() {
  if (!CONFIG.AUTO_ROLLBACK) {
    logMessage('⚠️  Auto rollback disabled, skipping...');
    return;
  }

  logMessage('🔙 ROLLBACK: Restoring previous version...');

  const finalPath = path.join(process.cwd(), CONFIG.FINAL_DIST_DIR);
  const backupPath = path.join(process.cwd(), CONFIG.BACKUP_DIST_DIR);

  if (fs.existsSync(backupPath)) {
    if (fs.existsSync(finalPath)) {
      fs.rmSync(finalPath, { recursive: true, force: true });
    }
    fs.renameSync(backupPath, finalPath);
    logMessage('✅ Rollback completed. Reloading PM2...');

    try {
      executeCommand(`pm2 reload ${CONFIG.APP_NAME}`);
      logMessage('✅ PM2 reloaded with previous version');
    } catch (error) {
      logMessage('❌ Failed to reload PM2 after rollback');
    }
  } else {
    logMessage('❌ No backup found for rollback!');
  }
}

// Generate PM2 ecosystem config
function generateEcosystemConfig() {
  const configPath = path.join(process.cwd(), `ecosystem.${CONFIG.ENVIRONMENT}.config.cjs`);

  const ecosystemConfig = `module.exports = {
  apps: [
    {
      name: '${CONFIG.APP_NAME}',
      script: 'npx',
      args: 'serve ${CONFIG.FINAL_DIST_DIR} -s -l ${CONFIG.APP_PORT}',
      
      // Instance Configuration
      instances: ${CONFIG.PM2_INSTANCES},
      exec_mode: '${CONFIG.PM2_EXEC_MODE}',
      
      // Zero downtime reload settings
      wait_ready: ${CONFIG.PM2_WAIT_READY},
      listen_timeout: ${CONFIG.PM2_LISTEN_TIMEOUT},
      kill_timeout: ${CONFIG.PM2_KILL_TIMEOUT},
      
      // Auto restart settings
      autorestart: true,
      max_restarts: 10,
      min_uptime: '10s',
      
      // Resource limits
      max_memory_restart: '${CONFIG.PM2_MAX_MEMORY}',
      
      // Environment
      env: {
        NODE_ENV: '${CONFIG.NODE_ENV}',
        PORT: ${CONFIG.APP_PORT},
        ENVIRONMENT: '${CONFIG.ENVIRONMENT}',
      },
      
      // Logging
      error_file: './${CONFIG.LOG_DIR}/pm2-error.log',
      out_file: './${CONFIG.LOG_DIR}/pm2-out.log',
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      merge_logs: true,
    },
  ],
};`;

  fs.writeFileSync(configPath, ecosystemConfig);
  logMessage(`📝 Generated PM2 config: ${configPath}`);

  return configPath;
}

// Main deployment function
// eslint-disable-next-line consistent-return
async function deployApp(environment = 'production') {
  const startTime = new Date();

  // Load environment-specific configuration
  loadEnvConfig(environment);
  CONFIG = getConfig(environment);

  logMessage('═'.repeat(60));
  logMessage(`🚀 STARTING ZERO DOWNTIME DEPLOYMENT`);
  logMessage(`📍 Environment: ${CONFIG.ENVIRONMENT}`);
  logMessage(`📦 App Name: ${CONFIG.APP_NAME}`);
  logMessage(`🔌 Port: ${CONFIG.APP_PORT}`);
  logMessage(`⚙️  Instances: ${CONFIG.PM2_INSTANCES} (${CONFIG.PM2_EXEC_MODE} mode)`);
  logMessage('═'.repeat(60));

  try {
    // Step 1: Build the application
    logMessage('');
    logMessage('📦 STEP 1: Building application...');
    logMessage('─'.repeat(60));

    const tmpPath = path.join(process.cwd(), CONFIG.TMP_BUILD_DIR);

    if (fs.existsSync(tmpPath)) {
      logMessage(`🧹 Cleaning up temp folder: ${CONFIG.TMP_BUILD_DIR}`);
      fs.rmSync(tmpPath, { recursive: true, force: true });
    }

    logMessage(`📥 Running: ${CONFIG.INSTALL_COMMAND}`);
    executeCommand(CONFIG.INSTALL_COMMAND);

    const buildCmd = `${CONFIG.BUILD_COMMAND} -- --mode ${CONFIG.ENVIRONMENT} --outDir ${CONFIG.TMP_BUILD_DIR}`;
    logMessage(`🔨 Running: ${buildCmd}`);
    executeCommand(buildCmd);

    logMessage('✅ Build completed successfully');

    // Step 2: Generate PM2 config
    logMessage('');
    logMessage('📝 STEP 2: Generating PM2 configuration...');
    logMessage('─'.repeat(60));

    const configFile = generateEcosystemConfig();

    // Step 3: Check if app is running
    logMessage('');
    logMessage('🔍 STEP 3: Checking application status...');
    logMessage('─'.repeat(60));

    const isRunning = checkAppStatus(CONFIG.APP_NAME);
    logMessage(`App status: ${isRunning ? '🟢 Running' : '🔴 Not running'}`);

    if (isRunning) {
      // Step 4: Zero-downtime deployment
      logMessage('');
      logMessage('🔄 STEP 4: Performing zero-downtime deployment...');
      logMessage('─'.repeat(60));

      // 4a: Atomic folder switch
      atomicFolderSwitch();

      // 4b: Reload PM2
      logMessage('🔄 Reloading PM2...');
      executeCommand(`pm2 reload ${configFile} --env ${CONFIG.ENVIRONMENT}`);

      // 4c: Wait for PM2 to stabilize
      const waitTime = 3000;
      logMessage(`⏳ Waiting ${waitTime}ms for PM2 to stabilize...`);
      await new Promise((resolve) => setTimeout(resolve, waitTime));

      // 4d: Health check
      logMessage('');
      logMessage('🏥 STEP 5: Running health check...');
      logMessage('─'.repeat(60));

      const isHealthy = await checkHealth();

      if (!isHealthy) {
        logMessage('❌ Health check failed! Initiating rollback...');
        rollback();
        throw new Error('Deployment failed: Health check unsuccessful');
      }

      logMessage('✅ Health check passed!');

      // 4e: Cleanup backup after successful deployment
      if (CONFIG.CLEANUP_BACKUP) {
        const backupPath = path.join(process.cwd(), CONFIG.BACKUP_DIST_DIR);
        if (fs.existsSync(backupPath)) {
          fs.rmSync(backupPath, { recursive: true, force: true });
          logMessage('🧹 Cleaned up backup folder');
        }
      }
    } else {
      // Step 4: First time deployment
      logMessage('');
      logMessage('🚀 STEP 4: Starting application for the first time...');
      logMessage('─'.repeat(60));

      atomicFolderSwitch();
      executeCommand(`pm2 start ${configFile} --env ${CONFIG.ENVIRONMENT}`);

      // Wait and health check
      logMessage('⏳ Waiting for application to start...');
      await new Promise((resolve) => setTimeout(resolve, 5000));

      const isHealthy = await checkHealth();

      if (!isHealthy) {
        logMessage('❌ Health check failed on first start!');
        executeCommand(`pm2 delete ${CONFIG.APP_NAME}`, false);
        throw new Error('Deployment failed: Health check unsuccessful');
      }

      logMessage('✅ Application started successfully!');
    }

    // Save PM2 process list
    if (CONFIG.PM2_SAVE) {
      logMessage('💾 Saving PM2 process list...');
      executeCommand('pm2 save');
    }

    const endTime = new Date();
    const deploymentTime = ((endTime - startTime) / 1000).toFixed(2);

    logMessage('');
    logMessage('═'.repeat(60));
    logMessage(`✅ DEPLOYMENT COMPLETED SUCCESSFULLY!`);
    logMessage(`⏱️  Total time: ${deploymentTime} seconds`);
    logMessage(`🌍 Environment: ${CONFIG.ENVIRONMENT}`);
    logMessage(`🔌 App running on: http://localhost:${CONFIG.APP_PORT}`);
    logMessage('═'.repeat(60));
    logMessage('');
    logMessage('📊 Quick commands:');
    logMessage(`   pm2 status ${CONFIG.APP_NAME}`);
    logMessage(`   pm2 logs ${CONFIG.APP_NAME}`);
    logMessage(`   pm2 monit`);
    logMessage('');

    return {
      success: true,
      message: `Deployment to ${CONFIG.ENVIRONMENT} completed successfully`,
      deploymentTime: parseFloat(deploymentTime),
      config: CONFIG,
    };
  } catch (error) {
    logMessage('');
    logMessage('═'.repeat(60));
    logMessage(`❌ DEPLOYMENT FAILED!`);
    logMessage(`Error: ${error.message}`);
    logMessage('═'.repeat(60));
    logMessage('Stack trace:');
    logMessage(error.stack);
    process.exit(1);
  }
}

// Execute deployment if script is run directly
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const environment = process.argv[2] || 'production';

  // Validate environment
  const validEnvironments = ['development', 'staging', 'production'];
  if (!validEnvironments.includes(environment)) {
    console.error(`❌ Invalid environment: ${environment}`);
    console.error(`✅ Valid environments: ${validEnvironments.join(', ')}`);
    process.exit(1);
  }

  deployApp(environment);
}

export { deployApp };
