import { execFile } from 'node:child_process'
import { createHash } from 'node:crypto'
import { cp, chmod, mkdir, mkdtemp, readdir, rename, rm, stat } from 'node:fs/promises'
import { homedir } from 'node:os'
import { basename, join } from 'node:path'
import { promisify } from 'node:util'

const execFileAsync = promisify(execFile)

function cacheBase(env) {
  return env?.XDG_CACHE_HOME || join(homedir(), '.cache')
}

// Every env var initdb bakes into the cluster (locale set + TZ ends up in
// postgresql.conf) must be part of the key, or same-key templates diverge.
const KEYED_ENV = [
  'LC_ALL',
  'LC_COLLATE',
  'LC_CTYPE',
  'LC_MESSAGES',
  'LC_MONETARY',
  'LC_NUMERIC',
  'LC_TIME',
  'LANG',
  'TZ',
]

function keyFor({ version, binStat, user, args, env }) {
  const hash = createHash('sha256')
    .update(version)
    .update('\0')
    .update(binStat)
    .update('\0')
    .update(user)
    .update('\0')
    .update(args.join('\0'))
  for (const name of KEYED_ENV) {
    hash.update('\0').update(env?.[name] || '')
  }
  return hash.digest('hex')
}

function isRaceWinnerError(error) {
  return error?.code === 'EEXIST' || error?.code === 'ENOTEMPTY' || error?.code === 'EPERM'
}

function warnAndFallback(error) {
  const message = error instanceof Error ? error.message : String(error)
  process.stderr.write(`[pg-template] cache unavailable; running initdb directly: ${message}\n`)
}

async function runInitdb({ initdbBin, dataDir, user, args, env }) {
  await execFileAsync(initdbBin, ['-D', dataDir, '-U', user, ...args], { env })
}

async function emptyDir(dir) {
  const entries = await readdir(dir, { withFileTypes: true })
  await Promise.all(
    entries.map((entry) => rm(join(dir, entry.name), { recursive: true, force: true })),
  )
}

async function runDirectAfterCacheError(options, error) {
  warnAndFallback(error)
  await emptyDir(options.dataDir).catch(() => undefined)
  await runInitdb(options)
}

/**
 * Initializes an embedded Postgres data directory from a deterministic initdb template cache.
 * Falls back to running initdb directly when the cache cannot be used.
 */
export async function initdbCached({ initdbBin, dataDir, user, args = [], env = process.env }) {
  const options = { initdbBin, dataDir, user, args, env }

  // Same contract as initdb itself: refuse a non-empty data directory. Without
  // this, fs.cp would silently overlay the template onto existing files.
  let existingEntries = []
  try {
    existingEntries = await readdir(dataDir)
  } catch (error) {
    if (error?.code !== 'ENOENT') throw error
  }
  if (existingEntries.length > 0) {
    throw new Error(`initdbCached: dataDir is not empty: ${dataDir}`)
  }

  let version
  let binStat
  try {
    const result = await execFileAsync(initdbBin, ['--version'], { env })
    version = `${result.stdout || ''}${result.stderr || ''}`.trim()
    // --version alone under-identifies the binary (a package rebuild keeps the
    // upstream version string); size+mtime invalidates on any binary swap.
    const st = await stat(initdbBin)
    binStat = `${st.size}:${Math.trunc(st.mtimeMs)}`
  } catch (error) {
    await runDirectAfterCacheError(options, error)
    return
  }

  const templatesRoot = join(cacheBase(env), 'platform-embedded-pg', 'templates')
  const templateDir = join(templatesRoot, keyFor({ version, binStat, user, args, env }))

  try {
    await mkdir(templatesRoot, { recursive: true })
    await cp(templateDir, dataDir, { recursive: true, force: false, errorOnExist: false })
    // postgres refuses data directories that are readable by other users.
    await chmod(dataDir, 0o700)
    return
  } catch (hitError) {
    if (hitError?.code !== 'ENOENT') {
      await runDirectAfterCacheError(options, hitError)
      return
    }
  }

  const tempDir = await mkdtemp(join(templatesRoot, `${basename(templateDir)}-`)).catch(async (error) => {
    await runDirectAfterCacheError(options, error)
    return undefined
  })
  if (!tempDir) return

  try {
    await runInitdb({ initdbBin, dataDir: tempDir, user, args, env })
    // Atomic rename is the lock-free race: one process wins, the rest use the winner.
    await rename(tempDir, templateDir)
  } catch (error) {
    await rm(tempDir, { recursive: true, force: true }).catch(() => undefined)
    if (!isRaceWinnerError(error)) {
      await runDirectAfterCacheError(options, error)
      return
    }
  }

  try {
    await cp(templateDir, dataDir, { recursive: true, force: false, errorOnExist: false })
    // postgres refuses data directories that are readable by other users.
    await chmod(dataDir, 0o700)
  } catch (error) {
    await runDirectAfterCacheError(options, error)
  }
}
