import { prisma } from './db'
import { createAuditLog } from './audit'
import { sendDiscordLog } from './discord-webhook'
import type { UpdateStatus } from '@prisma/client'
import AdmZip from 'adm-zip'
import fs from 'fs/promises'
import path from 'path'
import { execSync, spawn } from 'child_process'
import crypto from 'crypto'

// Update package structure expected in ZIP
export interface UpdateManifest {
  version: string
  name: string
  description?: string
  releaseDate: string
  minVersion?: string
  maxVersion?: string
  requiresRestart: boolean
  requiresRebuild: boolean
  requiresMigration: boolean
  preservePaths: string[]
  deleteFiles?: string[]
  preInstallScript?: string
  postInstallScript?: string
  changelog?: string[]
}

export interface UpdateValidationResult {
  valid: boolean
  manifest?: UpdateManifest
  errors: string[]
  warnings: string[]
  fileCount?: number
  totalSize?: number
}

export interface UpdateProgress {
  status: UpdateStatus
  progress: number
  message: string
  error?: string
}

// Paths configuration
const APP_ROOT = process.cwd()
const UPDATES_DIR = path.join(APP_ROOT, 'updates')
const BACKUPS_DIR = path.join(APP_ROOT, 'backups')
const TEMP_DIR = path.join(APP_ROOT, 'temp')

// Files and directories that should never be overwritten
const PROTECTED_PATHS = [
  '.env',
  '.env.local',
  'prisma/migrations',
  'uploads',
  'backups',
  'config/installed.json',
]

// Default paths to preserve during updates
const DEFAULT_PRESERVE_PATHS = [
  '.env',
  '.env.local',
  'uploads',
  'backups',
  'config/installed.json',
  'config/branding.json',
]

export async function ensureDirectories(): Promise<void> {
  await fs.mkdir(UPDATES_DIR, { recursive: true })
  await fs.mkdir(BACKUPS_DIR, { recursive: true })
  await fs.mkdir(TEMP_DIR, { recursive: true })
}

export async function calculateFileChecksum(filePath: string): Promise<string> {
  const fileBuffer = await fs.readFile(filePath)
  return crypto.createHash('sha256').update(fileBuffer).digest('hex')
}

export async function validateUpdatePackage(zipBuffer: Buffer): Promise<UpdateValidationResult> {
  const errors: string[] = []
  const warnings: string[] = []

  try {
    const zip = new AdmZip(zipBuffer)
    const entries = zip.getEntries()

    // Check for manifest.json
    const manifestEntry = entries.find(e => e.entryName === 'manifest.json' || e.entryName.endsWith('/manifest.json'))
    
    if (!manifestEntry) {
      errors.push('Missing manifest.json in update package')
      return { valid: false, errors, warnings }
    }

    // Parse manifest
    let manifest: UpdateManifest
    try {
      const manifestContent = manifestEntry.getData().toString('utf8')
      manifest = JSON.parse(manifestContent)
    } catch {
      errors.push('Invalid manifest.json format - must be valid JSON')
      return { valid: false, errors, warnings }
    }

    // Validate manifest fields
    if (!manifest.version) {
      errors.push('manifest.json missing required field: version')
    }

    if (!manifest.name) {
      errors.push('manifest.json missing required field: name')
    }

    if (!manifest.releaseDate) {
      warnings.push('manifest.json missing releaseDate')
    }

    // Version format validation (semver-like)
    if (manifest.version && !/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(manifest.version)) {
      errors.push('Invalid version format in manifest (expected semver format like 1.0.0)')
    }

    // Check minimum version compatibility
    if (manifest.minVersion) {
      const currentVersion = await getCurrentVersion()
      if (compareVersions(currentVersion, manifest.minVersion) < 0) {
        errors.push(`This update requires at least version ${manifest.minVersion}. Current version: ${currentVersion}`)
      }
    }

    // Check for potentially dangerous files
    for (const entry of entries) {
      const name = entry.entryName.toLowerCase()
      if (name.includes('..') || name.startsWith('/')) {
        errors.push(`Potentially dangerous path detected: ${entry.entryName}`)
      }
      if (name.endsWith('.exe') || name.endsWith('.bat') || name.endsWith('.cmd')) {
        warnings.push(`Executable file detected: ${entry.entryName}`)
      }
    }

    // Calculate file stats
    let totalSize = 0
    for (const entry of entries) {
      totalSize += entry.header.size
    }

    if (errors.length > 0) {
      return { valid: false, manifest, errors, warnings, fileCount: entries.length, totalSize }
    }

    return {
      valid: true,
      manifest,
      errors,
      warnings,
      fileCount: entries.length,
      totalSize,
    }
  } catch (error) {
    errors.push(`Failed to read ZIP file: ${error instanceof Error ? error.message : 'Unknown error'}`)
    return { valid: false, errors, warnings }
  }
}

export async function getCurrentVersion(): Promise<string> {
  try {
    const packageJson = JSON.parse(await fs.readFile(path.join(APP_ROOT, 'package.json'), 'utf8'))
    return packageJson.version || '1.0.0'
  } catch {
    return '1.0.0'
  }
}

function compareVersions(v1: string, v2: string): number {
  const parts1 = v1.split('.').map(Number)
  const parts2 = v2.split('.').map(Number)

  for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
    const p1 = parts1[i] || 0
    const p2 = parts2[i] || 0
    if (p1 > p2) return 1
    if (p1 < p2) return -1
  }
  return 0
}

export async function createBackup(backupName: string): Promise<string> {
  await ensureDirectories()
  
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
  const backupPath = path.join(BACKUPS_DIR, `${backupName}-${timestamp}.zip`)

  const zip = new AdmZip()

  // Backup essential directories
  const backupDirs = ['app', 'components', 'lib', 'prisma', 'public']
  const backupFiles = ['package.json', 'next.config.mjs', 'tsconfig.json', 'tailwind.config.ts']

  for (const dir of backupDirs) {
    const dirPath = path.join(APP_ROOT, dir)
    try {
      await fs.access(dirPath)
      zip.addLocalFolder(dirPath, dir)
    } catch {
      // Directory doesn't exist, skip
    }
  }

  for (const file of backupFiles) {
    const filePath = path.join(APP_ROOT, file)
    try {
      await fs.access(filePath)
      zip.addLocalFile(filePath)
    } catch {
      // File doesn't exist, skip
    }
  }

  zip.writeZip(backupPath)
  return backupPath
}

export async function installUpdate(
  packageId: string,
  zipBuffer: Buffer,
  onProgress: (progress: UpdateProgress) => void
): Promise<{ success: boolean; error?: string }> {
  const fromVersion = await getCurrentVersion()
  let historyId: string | undefined
  let backupPath: string | undefined

  try {
    // Create update history entry
    const validation = await validateUpdatePackage(zipBuffer)
    if (!validation.valid || !validation.manifest) {
      throw new Error(validation.errors.join(', '))
    }

    const manifest = validation.manifest
    const toVersion = manifest.version

    // Create history record
    const history = await prisma.updateHistory.create({
      data: {
        packageId,
        fromVersion,
        toVersion,
        status: 'VALIDATING',
        performedBy: 'system',
        details: { manifest },
      },
    })
    historyId = history.id

    // Update status: Validating
    onProgress({ status: 'VALIDATING', progress: 5, message: 'Validating update package...' })
    await updateHistoryStatus(historyId, 'VALIDATING')

    // Update status: Backing up
    onProgress({ status: 'BACKING_UP', progress: 15, message: 'Creating backup...' })
    await updateHistoryStatus(historyId, 'BACKING_UP')
    
    backupPath = await createBackup(`pre-update-${toVersion}`)
    await prisma.updateHistory.update({
      where: { id: historyId },
      data: { backupPath, rollbackAvailable: true },
    })

    // Update status: Extracting
    onProgress({ status: 'EXTRACTING', progress: 30, message: 'Extracting files...' })
    await updateHistoryStatus(historyId, 'EXTRACTING')

    const zip = new AdmZip(zipBuffer)
    const tempExtractPath = path.join(TEMP_DIR, `update-${Date.now()}`)
    await fs.mkdir(tempExtractPath, { recursive: true })
    zip.extractAllTo(tempExtractPath, true)

    // Get preserved paths (from manifest + defaults + protected)
    const preservePaths = [
      ...PROTECTED_PATHS,
      ...DEFAULT_PRESERVE_PATHS,
      ...(manifest.preservePaths || []),
    ]

    // Update status: Installing
    onProgress({ status: 'INSTALLING', progress: 50, message: 'Installing update files...' })
    await updateHistoryStatus(historyId, 'INSTALLING')

    // Copy files from extracted update (excluding manifest and preserved paths)
    await copyUpdateFiles(tempExtractPath, APP_ROOT, preservePaths, manifest.deleteFiles)

    // Run migrations if required
    if (manifest.requiresMigration) {
      onProgress({ status: 'MIGRATING', progress: 65, message: 'Running database migrations...' })
      await updateHistoryStatus(historyId, 'MIGRATING')
      await runMigrations()
    }

    // Reinstall dependencies if package.json changed
    const newPackageJson = path.join(tempExtractPath, 'package.json')
    try {
      await fs.access(newPackageJson)
      onProgress({ status: 'INSTALLING', progress: 70, message: 'Installing dependencies...' })
      await runCommand('pnpm install --frozen-lockfile || pnpm install')
    } catch {
      // No package.json in update, skip dependency install
    }

    // Build if required
    if (manifest.requiresRebuild) {
      onProgress({ status: 'BUILDING', progress: 80, message: 'Building application...' })
      await updateHistoryStatus(historyId, 'BUILDING')
      await runCommand('pnpm build')
    }

    // Update version in package.json
    await updatePackageVersion(toVersion)

    // Clean up temp directory
    await fs.rm(tempExtractPath, { recursive: true, force: true })

    // Run post-install script if specified
    if (manifest.postInstallScript) {
      try {
        await runCommand(manifest.postInstallScript)
      } catch (error) {
        console.error('[Update] Post-install script failed:', error)
        // Don't fail the entire update for post-install script failure
      }
    }

    // Update status: Restarting
    if (manifest.requiresRestart) {
      onProgress({ status: 'RESTARTING', progress: 95, message: 'Restarting services...' })
      await updateHistoryStatus(historyId, 'RESTARTING')
      
      // Schedule restart via PM2
      scheduleRestart()
    }

    // Mark as completed
    onProgress({ status: 'COMPLETED', progress: 100, message: 'Update installed successfully!' })
    await prisma.updateHistory.update({
      where: { id: historyId },
      data: { status: 'COMPLETED', completedAt: new Date() },
    })

    // Update package status
    await prisma.updatePackage.update({
      where: { id: packageId },
      data: { status: 'COMPLETED' },
    })

    // Log to audit and Discord
    await createAuditLog({
      action: 'UPDATE_INSTALLED',
      category: 'UPDATE',
      details: { fromVersion, toVersion, backupPath },
      discordLog: {
        title: 'Update Installed Successfully',
        description: `SKG UCP has been updated from ${fromVersion} to ${toVersion}`,
        fields: [
          { name: 'From Version', value: fromVersion, inline: true },
          { name: 'To Version', value: toVersion, inline: true },
          { name: 'Backup', value: backupPath ? 'Created' : 'Skipped', inline: true },
        ],
      },
    })

    return { success: true }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error'

    if (historyId) {
      await prisma.updateHistory.update({
        where: { id: historyId },
        data: { 
          status: 'FAILED', 
          errorMessage,
          completedAt: new Date(),
        },
      })
    }

    // Update package status
    await prisma.updatePackage.update({
      where: { id: packageId },
      data: { status: 'FAILED' },
    })

    onProgress({ status: 'FAILED', progress: 0, message: 'Update failed', error: errorMessage })

    // Log failure
    await createAuditLog({
      action: 'UPDATE_FAILED',
      category: 'UPDATE',
      details: { error: errorMessage, backupPath },
      discordLog: {
        title: 'Update Failed',
        description: `Update installation failed: ${errorMessage}`,
        fields: backupPath ? [{ name: 'Backup Available', value: 'Yes - rollback possible' }] : [],
      },
    })

    return { success: false, error: errorMessage }
  }
}

async function copyUpdateFiles(
  sourcePath: string,
  targetPath: string,
  preservePaths: string[],
  deleteFiles?: string[]
): Promise<void> {
  const entries = await fs.readdir(sourcePath, { withFileTypes: true })

  for (const entry of entries) {
    const sourceEntry = path.join(sourcePath, entry.name)
    const targetEntry = path.join(targetPath, entry.name)
    const relativePath = path.relative(sourcePath, sourceEntry)

    // Skip manifest file
    if (entry.name === 'manifest.json') continue

    // Check if path should be preserved
    const shouldPreserve = preservePaths.some(
      p => relativePath === p || relativePath.startsWith(p + '/')
    )

    if (shouldPreserve) {
      console.log(`[Update] Preserving: ${relativePath}`)
      continue
    }

    if (entry.isDirectory()) {
      await fs.mkdir(targetEntry, { recursive: true })
      await copyUpdateFiles(sourceEntry, targetEntry, preservePaths, deleteFiles)
    } else {
      await fs.copyFile(sourceEntry, targetEntry)
      console.log(`[Update] Copied: ${relativePath}`)
    }
  }

  // Delete specified files
  if (deleteFiles) {
    for (const file of deleteFiles) {
      const filePath = path.join(targetPath, file)
      try {
        await fs.rm(filePath, { recursive: true, force: true })
        console.log(`[Update] Deleted: ${file}`)
      } catch {
        // File doesn't exist, skip
      }
    }
  }
}

async function updateHistoryStatus(historyId: string, status: UpdateStatus): Promise<void> {
  await prisma.updateHistory.update({
    where: { id: historyId },
    data: { status },
  })
}

async function runMigrations(): Promise<void> {
  try {
    execSync('npx prisma migrate deploy', { cwd: APP_ROOT, stdio: 'inherit' })
    execSync('npx prisma generate', { cwd: APP_ROOT, stdio: 'inherit' })
  } catch (error) {
    console.error('[Update] Migration failed:', error)
    throw new Error('Database migration failed')
  }
}

async function runCommand(command: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const child = spawn('sh', ['-c', command], {
      cwd: APP_ROOT,
      stdio: 'inherit',
    })

    child.on('close', (code) => {
      if (code === 0) {
        resolve()
      } else {
        reject(new Error(`Command failed with code ${code}`))
      }
    })

    child.on('error', reject)
  })
}

async function updatePackageVersion(version: string): Promise<void> {
  const packageJsonPath = path.join(APP_ROOT, 'package.json')
  const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'))
  packageJson.version = version
  await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
}

function scheduleRestart(): void {
  // Schedule restart via PM2 after response is sent
  setTimeout(() => {
    try {
      execSync('pm2 restart skg-ucp || pm2 restart all', { stdio: 'inherit' })
    } catch (error) {
      console.error('[Update] Failed to restart via PM2:', error)
      // Try alternative restart methods
      try {
        execSync('touch /tmp/skg-ucp-restart', { stdio: 'inherit' })
      } catch {
        console.error('[Update] Failed to schedule restart')
      }
    }
  }, 2000)
}

export async function rollbackUpdate(historyId: string): Promise<{ success: boolean; error?: string }> {
  try {
    const history = await prisma.updateHistory.findUnique({
      where: { id: historyId },
    })

    if (!history) {
      return { success: false, error: 'Update history not found' }
    }

    if (!history.rollbackAvailable || !history.backupPath) {
      return { success: false, error: 'Rollback not available for this update' }
    }

    // Verify backup exists
    try {
      await fs.access(history.backupPath)
    } catch {
      return { success: false, error: 'Backup file not found' }
    }

    // Extract backup
    const zip = new AdmZip(history.backupPath)
    const tempPath = path.join(TEMP_DIR, `rollback-${Date.now()}`)
    await fs.mkdir(tempPath, { recursive: true })
    zip.extractAllTo(tempPath, true)

    // Copy files back (preserve current config)
    await copyUpdateFiles(tempPath, APP_ROOT, PROTECTED_PATHS)

    // Restore version
    await updatePackageVersion(history.fromVersion)

    // Clean up
    await fs.rm(tempPath, { recursive: true, force: true })

    // Update history
    await prisma.updateHistory.update({
      where: { id: historyId },
      data: { status: 'ROLLED_BACK' },
    })

    // Log rollback
    await createAuditLog({
      action: 'UPDATE_ROLLED_BACK',
      category: 'UPDATE',
      details: { fromVersion: history.toVersion, toVersion: history.fromVersion },
      discordLog: {
        title: 'Update Rolled Back',
        description: `SKG UCP has been rolled back from ${history.toVersion} to ${history.fromVersion}`,
      },
    })

    // Schedule restart
    scheduleRestart()

    return { success: true }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error'
    return { success: false, error: errorMessage }
  }
}

export async function getUpdateHistory(limit = 20) {
  return prisma.updateHistory.findMany({
    include: {
      package: true,
    },
    orderBy: { startedAt: 'desc' },
    take: limit,
  })
}

export async function cleanupOldBackups(keepCount = 5): Promise<void> {
  try {
    const files = await fs.readdir(BACKUPS_DIR)
    const backups = files
      .filter(f => f.endsWith('.zip'))
      .map(f => ({
        name: f,
        path: path.join(BACKUPS_DIR, f),
        time: f.match(/\d{4}-\d{2}-\d{2}/)?.[0] || '',
      }))
      .sort((a, b) => b.time.localeCompare(a.time))

    // Remove old backups
    for (const backup of backups.slice(keepCount)) {
      await fs.rm(backup.path, { force: true })
      console.log(`[Update] Removed old backup: ${backup.name}`)
    }
  } catch (error) {
    console.error('[Update] Failed to cleanup old backups:', error)
  }
}
