import { exec } from 'child_process'
import { promisify } from 'util'
import { mkdir, writeFile, rm } from 'fs/promises'
import path from 'path'
import { JobConfig } from './config-generator'

const execAsync = promisify(exec)

/**
 * Docker spawner configuration from settings
 */
export interface DockerSpawnerConfig {
  registryUrl: string
  imageName: string
  imageTag: string
  networkMode: string
  maxConcurrent: number
}

/**
 * Result of spawning a container
 */
export interface SpawnResult {
  success: boolean
  containerName: string
  containerId?: string
  error?: string
}

/**
 * Default Docker configuration
 */
const DEFAULT_CONFIG: DockerSpawnerConfig = {
  // Use nullish coalescing to allow empty string for local images
  registryUrl: process.env.DOCKER_REGISTRY_URL ?? 'code.letsbe.solutions',
  imageName: process.env.DOCKER_IMAGE_NAME || 'letsbe/ansible-runner',
  imageTag: process.env.DOCKER_IMAGE_TAG || 'latest',
  networkMode: process.env.DOCKER_NETWORK_MODE || 'bridge',
  maxConcurrent: parseInt(process.env.DOCKER_MAX_CONCURRENT || '3', 10),
}

/**
 * Get the jobs directory for storing config files (container path)
 */
function getJobsDir(): string {
  return process.env.JOBS_DIR || path.join(process.cwd(), 'jobs')
}

/**
 * Get the logs directory for container output (container path)
 */
function getLogsDir(): string {
  return process.env.LOGS_DIR || path.join(process.cwd(), 'logs')
}

/**
 * Get the host jobs directory for Docker mounts
 * When running in a container, we need the host path for mounting to spawned containers
 */
function getHostJobsDir(): string {
  return process.env.JOBS_HOST_DIR || getJobsDir()
}

/**
 * Get the host logs directory for Docker mounts
 */
function getHostLogsDir(): string {
  return process.env.LOGS_HOST_DIR || getLogsDir()
}

/**
 * Write the job config to a file that will be mounted into the container
 */
async function writeJobConfig(jobId: string, config: JobConfig): Promise<string> {
  const jobsDir = getJobsDir()
  const jobDir = path.join(jobsDir, jobId)

  // Create job directory
  await mkdir(jobDir, { recursive: true })

  // Write config file
  const configPath = path.join(jobDir, 'config.json')
  await writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8')

  return configPath
}

/**
 * Get the full image name including registry
 */
function getFullImageName(config: DockerSpawnerConfig = DEFAULT_CONFIG): string {
  const { registryUrl, imageName, imageTag } = config
  if (registryUrl) {
    return `${registryUrl}/${imageName}:${imageTag}`
  }
  return `${imageName}:${imageTag}`
}

/**
 * Count currently running provisioning containers
 */
async function countRunningContainers(): Promise<number> {
  try {
    const { stdout } = await execAsync(
      'docker ps --filter "name=letsbe-runner-" --format "{{.Names}}" | wc -l'
    )
    return parseInt(stdout.trim(), 10) || 0
  } catch {
    return 0
  }
}

/**
 * Check if Docker is available
 */
export async function isDockerAvailable(): Promise<boolean> {
  try {
    await execAsync('docker info')
    return true
  } catch {
    return false
  }
}

/**
 * Pull the latest runner image
 */
export async function pullRunnerImage(
  config: DockerSpawnerConfig = DEFAULT_CONFIG
): Promise<{ success: boolean; error?: string }> {
  try {
    const imageName = getFullImageName(config)
    console.log(`Pulling image: ${imageName}`)
    await execAsync(`docker pull ${imageName}`)
    return { success: true }
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown error'
    return { success: false, error: message }
  }
}

/**
 * Spawn a provisioning container for a job
 */
export async function spawnProvisioningContainer(
  jobId: string,
  jobConfig: JobConfig,
  runnerToken: string,
  hubApiUrl: string,
  dockerConfig: DockerSpawnerConfig = DEFAULT_CONFIG
): Promise<SpawnResult> {
  const containerName = `letsbe-runner-${jobId}`

  try {
    // Check Docker availability
    if (!await isDockerAvailable()) {
      return {
        success: false,
        containerName,
        error: 'Docker is not available',
      }
    }

    // Check concurrent limit
    const runningCount = await countRunningContainers()
    if (runningCount >= dockerConfig.maxConcurrent) {
      return {
        success: false,
        containerName,
        error: `Maximum concurrent containers (${dockerConfig.maxConcurrent}) reached`,
      }
    }

    // Write config file (to container path)
    await writeJobConfig(jobId, jobConfig)

    // Ensure logs directory exists (in container)
    const containerLogsDir = path.join(getLogsDir(), jobId)
    await mkdir(containerLogsDir, { recursive: true })

    // Get HOST paths for Docker mounts (spawned containers mount from host, not from this container)
    const hostConfigPath = path.join(getHostJobsDir(), jobId, 'config.json')
    const hostLogsDir = path.join(getHostLogsDir(), jobId)

    // Build docker run command
    const imageName = getFullImageName(dockerConfig)
    const networkArgs = dockerConfig.networkMode === 'host'
      ? '--network host'
      : `--network ${dockerConfig.networkMode} --add-host=host.docker.internal:host-gateway`

    // Convert Windows paths to Docker-compatible paths if needed
    const dockerConfigPath = hostConfigPath.replace(/\\/g, '/')
    const dockerLogsPath = hostLogsDir.replace(/\\/g, '/')

    const dockerCommand = [
      'docker run -d',
      `--name ${containerName}`,
      networkArgs,
      `-e HUB_API_URL=${hubApiUrl}`,
      `-e JOB_ID=${jobId}`,
      `-e RUNNER_TOKEN=${runnerToken}`,
      `-e JOB_CONFIG_PATH=/job/config.json`,
      `-v "${dockerConfigPath}:/job/config.json:ro"`,
      `-v "${dockerLogsPath}:/logs"`,
      '--rm', // Auto-remove container when it exits
      '--cpus=1.0',
      '--memory=512m',
      imageName,
    ].join(' ')

    console.log(`Spawning container: ${containerName}`)
    console.log(`Command: ${dockerCommand}`)

    const { stdout } = await execAsync(dockerCommand)
    const containerId = stdout.trim()

    console.log(`Container spawned: ${containerId}`)

    return {
      success: true,
      containerName,
      containerId,
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown error'
    console.error(`Failed to spawn container ${containerName}:`, message)
    return {
      success: false,
      containerName,
      error: message,
    }
  }
}

/**
 * Get container status
 */
export async function getContainerStatus(
  containerName: string
): Promise<'running' | 'exited' | 'not_found'> {
  try {
    const { stdout } = await execAsync(
      `docker inspect --format '{{.State.Status}}' ${containerName} 2>/dev/null`
    )
    const status = stdout.trim()
    if (status === 'running') return 'running'
    if (status === 'exited') return 'exited'
    return 'not_found'
  } catch {
    return 'not_found'
  }
}

/**
 * Get container logs
 */
export async function getContainerLogs(
  containerName: string,
  tail: number = 100
): Promise<string> {
  try {
    const { stdout, stderr } = await execAsync(
      `docker logs --tail ${tail} ${containerName} 2>&1`
    )
    return stdout + stderr
  } catch {
    return ''
  }
}

/**
 * Stop and remove a container
 */
export async function stopContainer(containerName: string): Promise<boolean> {
  try {
    await execAsync(`docker stop ${containerName}`)
    return true
  } catch {
    return false
  }
}

/**
 * Clean up old job directories (call periodically)
 */
export async function cleanupOldJobs(maxAgeHours: number = 24): Promise<void> {
  const jobsDir = getJobsDir()
  const logsDir = getLogsDir()
  const maxAgeMs = maxAgeHours * 60 * 60 * 1000
  const now = Date.now()

  try {
    const { readdir, stat } = await import('fs/promises')

    // Clean jobs directory
    const jobDirs = await readdir(jobsDir)
    for (const dir of jobDirs) {
      const dirPath = path.join(jobsDir, dir)
      const stats = await stat(dirPath)
      if (now - stats.mtimeMs > maxAgeMs) {
        await rm(dirPath, { recursive: true, force: true })
      }
    }

    // Clean logs directory
    const logDirs = await readdir(logsDir)
    for (const dir of logDirs) {
      const dirPath = path.join(logsDir, dir)
      const stats = await stat(dirPath)
      if (now - stats.mtimeMs > maxAgeMs) {
        await rm(dirPath, { recursive: true, force: true })
      }
    }
  } catch (error) {
    console.error('Failed to cleanup old jobs:', error)
  }
}
