import { execFile } from 'child_process'
import { promisify } from 'util'
import * as fs from 'fs/promises'
import * as path from 'path'
import { prisma as _prisma } from '../lib/prisma'

const db = _prisma as any

const execFileAsync = promisify(execFile)
let composeCommand: 'docker-compose' | 'docker' | null = null

// ── Config ────────────────────────────────────────────────────────────────────

const COMPOSE_DIR = process.env.COMPANIES_COMPOSE_DIR || '/opt/companies'
const COMPOSE_FILE = path.join(COMPOSE_DIR, 'docker-compose.companies.yml')
const SERVICES_FILE = path.join(COMPOSE_DIR, 'companies-services.json') // our source of truth

const DASHBOARD_IMAGE = process.env.DASHBOARD_CONTAINER_IMAGE || 'rentaldrivego/dashboard:latest'
const PORT_RANGE_START = parseInt(process.env.CONTAINER_PORT_START || '5100', 10)
const PORT_RANGE_END = parseInt(process.env.CONTAINER_PORT_END || '5999', 10)
const API_INTERNAL_URL = process.env.API_INTERNAL_URL || 'http://api:4000'
const CONTAINER_NETWORK = process.env.CONTAINER_NETWORK || 'rentaldrivego_default'

// ── Types ─────────────────────────────────────────────────────────────────────

interface ServiceDef {
  companyId: string
  slug: string
  image: string
  port: number
}

type ServicesMap = Record<string, ServiceDef> // key = service name e.g. "company-slug"

type ServiceStatus = 'RUNNING' | 'STOPPED' | 'RESTARTING' | 'ERROR'

// ── Internal helpers ──────────────────────────────────────────────────────────

function serviceName(slug: string) {
  return `company-${slug}`
}

function containerName(slug: string) {
  return `rdg-company-${slug}`
}

async function ensureDir(): Promise<void> {
  await fs.mkdir(COMPOSE_DIR, { recursive: true })
}

async function readServices(): Promise<ServicesMap> {
  try {
    const raw = await fs.readFile(SERVICES_FILE, 'utf8')
    return JSON.parse(raw)
  } catch {
    return {}
  }
}

async function writeServices(services: ServicesMap): Promise<void> {
  await ensureDir()
  await fs.writeFile(SERVICES_FILE, JSON.stringify(services, null, 2), 'utf8')
  await fs.writeFile(COMPOSE_FILE, buildComposeYaml(services), 'utf8')
}

async function composeFilesExist(): Promise<boolean> {
  try {
    await fs.access(COMPOSE_FILE)
    await fs.access(SERVICES_FILE)
    return true
  } catch {
    return false
  }
}

async function rebuildServicesFromDatabase(): Promise<ServicesMap> {
  const records = await db.companyContainer.findMany({
    include: { company: { select: { slug: true } } },
  })

  return Object.fromEntries(
    records.map((record: any) => [
      serviceName(record.company.slug),
      {
        companyId: record.companyId,
        slug: record.company.slug,
        image: record.image,
        port: record.port,
      },
    ]),
  )
}

async function ensureComposeFiles(): Promise<void> {
  if (await composeFilesExist()) return

  const services = await rebuildServicesFromDatabase()
  await writeServices(services)
}

function buildComposeYaml(services: ServicesMap): string {
  const lines: string[] = ['services:']

  for (const [name, svc] of Object.entries(services)) {
    lines.push(`  ${name}:`)
    lines.push(`    image: "${svc.image}"`)
    lines.push(`    container_name: "${containerName(svc.slug)}"`)
    lines.push(`    restart: unless-stopped`)
    lines.push(`    environment:`)
    lines.push(`      COMPANY_ID: "${svc.companyId}"`)
    lines.push(`      COMPANY_SLUG: "${svc.slug}"`)
    lines.push(`      API_URL: "${API_INTERNAL_URL}"`)
    lines.push(`      NEXT_PUBLIC_API_URL: "${API_INTERNAL_URL}"`)
    lines.push(`      PORT: "3000"`)
    lines.push(`      NODE_ENV: "production"`)
    lines.push(`    ports:`)
    lines.push(`      - "${svc.port}:3000"`)
    lines.push(`    networks:`)
    lines.push(`      - ${CONTAINER_NETWORK}`)
    lines.push(`    labels:`)
    lines.push(`      rdg.managed: "true"`)
    lines.push(`      rdg.company.id: "${svc.companyId}"`)
    lines.push(`      rdg.company.slug: "${svc.slug}"`)
  }

  lines.push('')
  lines.push('networks:')
  lines.push(`  ${CONTAINER_NETWORK}:`)
  lines.push(`    external: true`)
  lines.push('')

  return lines.join('\n')
}

async function resolveComposeCommand(): Promise<'docker-compose' | 'docker'> {
  if (composeCommand) return composeCommand

  try {
    await execFileAsync('docker-compose', ['version'])
    composeCommand = 'docker-compose'
    return composeCommand
  } catch {
    try {
      await execFileAsync('docker', ['compose', 'version'])
      composeCommand = 'docker'
      return composeCommand
    } catch (err) {
      throw mapDockerError(err)
    }
  }
}

async function compose(...args: string[]): Promise<{ stdout: string; stderr: string }> {
  try {
    await ensureComposeFiles()
    const command = await resolveComposeCommand()
    if (command === 'docker-compose') {
      return await execFileAsync('docker-compose', ['-f', COMPOSE_FILE, ...args])
    }
    return await execFileAsync('docker', ['compose', '-f', COMPOSE_FILE, ...args])
  } catch (err) {
    throw mapDockerError(err)
  }
}

function dockerUnavailableError(message: string, details?: string) {
  const err = Object.assign(new Error(message), {
    statusCode: 503,
    code: 'docker_unavailable',
  })

  if (details) {
    ;(err as Error & { details?: string }).details = details
  }

  return err
}

function mapDockerError(err: unknown) {
  if (err && typeof err === 'object') {
    const error = err as NodeJS.ErrnoException & { stderr?: string; stdout?: string }
    const stderr = error.stderr?.trim()

    if (error.code === 'ENOENT' && error.path === 'docker') {
      return dockerUnavailableError(
        'Docker CLI is not available to the API service.',
        'Install Docker in the API container or run the API on a host with Docker available in PATH.',
      )
    }

    if (error.code === 'ENOENT' && error.path === 'docker-compose') {
      return dockerUnavailableError(
        'Docker Compose is not available to the API service.',
        'Install Docker Compose in the API container or switch the service to a runtime that supports `docker compose`.',
      )
    }

    if (
      stderr?.includes("docker: 'compose' is not a docker command.") ||
      stderr?.includes("unknown shorthand flag: 'f' in -f")
    ) {
      return dockerUnavailableError(
        'Docker Compose is not available to the API service.',
        'Install Docker Compose in the API container or switch the service to a runtime that supports `docker compose`.',
      )
    }

    if (
      stderr?.includes('Cannot connect to the Docker daemon') ||
      stderr?.includes('permission denied while trying to connect to the Docker daemon socket') ||
      stderr?.includes('error during connect')
    ) {
      return dockerUnavailableError(
        'Docker daemon is not reachable from the API service.',
        'Mount `/var/run/docker.sock` into the API container and ensure the Docker daemon is running.',
      )
    }

    // Catch-all: docker ran but exited non-zero — surface stderr as a readable 502
    if (typeof error.code === 'number' && error.code !== 0) {
      const detail = stderr || error.stdout?.trim() || 'docker compose exited with a non-zero status'
      return Object.assign(new Error(detail), { statusCode: 502, code: 'docker_error' })
    }
  }

  return err
}

async function setContainerError(companyId: string, message: string): Promise<void> {
  await db.companyContainer.update({
    where: { companyId },
    data: { status: 'ERROR', errorMessage: message },
  }).catch(() => null)
}

async function runContainerAction(
  companyId: string,
  slug: string,
  command: 'start' | 'stop' | 'restart',
  successStatus: Exclude<ServiceStatus, 'ERROR'>,
  preStatus?: Exclude<ServiceStatus, 'ERROR'>,
): Promise<void> {
  if (preStatus) {
    await db.companyContainer.update({ where: { companyId }, data: { status: preStatus, errorMessage: null } })
  }

  try {
    // Use `up -d --no-recreate` for start so it creates the container if it doesn't exist yet
    const args = command === 'start'
      ? ['up', '-d', '--no-recreate', serviceName(slug)]
      : [command, serviceName(slug)]
    await compose(...args)
    await db.companyContainer.update({
      where: { companyId },
      data: { status: successStatus, errorMessage: null },
    })
  } catch (err) {
    const message = err instanceof Error ? err.message : `Failed to ${command} container`
    await setContainerError(companyId, message)
    throw err
  }
}

async function allocatePort(): Promise<number> {
  const last = await db.companyContainer.findFirst({
    orderBy: { port: 'desc' },
    select: { port: true },
  })
  const next = last ? last.port + 1 : PORT_RANGE_START
  if (next > PORT_RANGE_END) throw new Error('No available ports in container port range')
  return next
}

async function getDockerServiceId(slug: string): Promise<string | null> {
  try {
    const name = serviceName(slug)
    const { stdout } = await compose('ps', '--format', 'json', name)
    const line = stdout.trim().split('\n')[0]
    if (!line) return null
    const info = JSON.parse(line)
    return info.ID ?? info.Id ?? null
  } catch {
    return null
  }
}

// Maps docker compose State strings to our DB enum
function mapDockerState(state: string): ServiceStatus {
  switch (state.toLowerCase()) {
    case 'running':    return 'RUNNING'
    case 'restarting': return 'RESTARTING'
    case 'exited':
    case 'created':
    case 'paused':     return 'STOPPED'
    default:           return 'ERROR'
  }
}

// ── Public API ────────────────────────────────────────────────────────────────

export async function createCompanyContainer(company: {
  id: string
  slug: string
  name: string
}): Promise<void> {
  const name = serviceName(company.slug)
  const port = await allocatePort()

  const record = await db.companyContainer.create({
    data: {
      companyId: company.id,
      containerName: containerName(company.slug),
      status: 'CREATING',
      port,
      image: DASHBOARD_IMAGE,
    },
  })

  try {
    const services = await readServices()
    services[name] = { companyId: company.id, slug: company.slug, image: DASHBOARD_IMAGE, port }
    await writeServices(services)

    await compose('up', '-d', '--no-recreate', name)

    const dockerId = await getDockerServiceId(company.slug)
    await db.companyContainer.update({
      where: { id: record.id },
      data: { dockerId, status: 'RUNNING' },
    })
  } catch (err) {
    await db.companyContainer.update({
      where: { id: record.id },
      data: {
        status: 'ERROR',
        errorMessage: err instanceof Error ? err.message : 'Unknown error during service creation',
      },
    })
    throw err
  }
}

export async function startCompanyContainer(companyId: string): Promise<void> {
  const record = await db.companyContainer.findUniqueOrThrow({
    where: { companyId },
    include: { company: { select: { slug: true } } },
  })

  await runContainerAction(companyId, record.company.slug, 'start', 'RUNNING')
}

export async function stopCompanyContainer(companyId: string): Promise<void> {
  const record = await db.companyContainer.findUniqueOrThrow({
    where: { companyId },
    include: { company: { select: { slug: true } } },
  })

  await runContainerAction(companyId, record.company.slug, 'stop', 'STOPPED')
}

export async function restartCompanyContainer(companyId: string): Promise<void> {
  const record = await db.companyContainer.findUniqueOrThrow({
    where: { companyId },
    include: { company: { select: { slug: true } } },
  })

  await runContainerAction(companyId, record.company.slug, 'restart', 'RUNNING', 'RESTARTING')
}

export async function removeCompanyContainer(companyId: string): Promise<void> {
  const record = await db.companyContainer.findUniqueOrThrow({
    where: { companyId },
    include: { company: { select: { slug: true } } },
  })

  const name = serviceName(record.company.slug)
  await db.companyContainer.update({ where: { companyId }, data: { status: 'REMOVING' } })

  try {
    await compose('stop', name)
  } catch { /* already stopped */ }

  try {
    await compose('rm', '-f', name)
  } catch { /* already removed */ }

  const services = await readServices()
  delete services[name]
  await writeServices(services)

  await db.companyContainer.delete({ where: { companyId } })
}

export async function redeployCompanyContainer(company: {
  id: string
  slug: string
  name: string
}): Promise<void> {
  const name = serviceName(company.slug)

  const existing = await db.companyContainer.findUnique({ where: { companyId: company.id } })
  if (existing) {
    try { await compose('stop', name) } catch { /* ok */ }
    try { await compose('rm', '-f', name) } catch { /* ok */ }
    await db.companyContainer.delete({ where: { companyId: company.id } })
  }

  // Remove from compose file too, then recreate
  const services = await readServices()
  delete services[name]
  await writeServices(services)

  await createCompanyContainer(company)
}

export async function getContainerLogs(companyId: string, tail = 150): Promise<string> {
  const record = await db.companyContainer.findUniqueOrThrow({
    where: { companyId },
    include: { company: { select: { slug: true } } },
  })

  try {
    const { stdout } = await compose(
      'logs',
      '--no-log-prefix',
      `--tail=${tail}`,
      serviceName(record.company.slug),
    )
    return stdout
  } catch {
    return ''
  }
}

export async function syncContainerStatuses(): Promise<void> {
  const records = await db.companyContainer.findMany({
    where: { status: { notIn: ['PENDING', 'CREATING', 'REMOVING'] } },
    include: { company: { select: { slug: true } } },
  })

  try {
    // Get all compose service states in one shot
    const { stdout } = await compose('ps', '--format', 'json')
    const rows = stdout
      .trim()
      .split('\n')
      .filter(Boolean)
      .map((line) => {
        try { return JSON.parse(line) } catch { return null }
      })
      .filter(Boolean) as Array<{ Service: string; State: string }>

    const stateByService = Object.fromEntries(rows.map((r) => [r.Service, r.State]))

    await Promise.allSettled(
      records.map(async (rec: any) => {
        const name = serviceName(rec.company.slug)
        const dockerState = stateByService[name]
        const status = dockerState ? mapDockerState(dockerState) : 'STOPPED'

        if (rec.status !== status) {
          await db.companyContainer.update({ where: { id: rec.id }, data: { status } })
        }
      }),
    )
  } catch {
    // Docker daemon unreachable — leave statuses as-is
  }
}
