import { execSync, spawn, type ChildProcess } from 'node:child_process'
import { mkdtemp, rm } from 'node:fs/promises'
import { createServer } from 'node:net'
import { arch, platform, tmpdir } from 'node:os'
import { join } from 'node:path'
import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg'
import type { TransactionalDatabase } from '@platform-modules/db'
import { withTransactionIdentity } from '@platform-modules/db'
import { initdbCached } from '@tooling/pg-template'
import { fieldsSchema, type FieldsSchema } from './schema.js'

async function getBins(): Promise<{ initdb: string; postgres: string }> {
  if (platform() === 'linux' && arch() === 'x64') {
    return import('@embedded-postgres/linux-x64')
  }
  throw new Error(`unsupported platform for embedded-postgres harness: ${platform()}-${arch()}`)
}

async function freePort(): Promise<number> {
  return new Promise((resolve, reject) => {
    const server = createServer()
    server.once('error', reject)
    server.listen(0, '127.0.0.1', () => {
      const addr = server.address()
      if (!addr || typeof addr === 'string') {
        server.close(() => reject(new Error('failed to allocate port')))
        return
      }
      const { port } = addr
      server.close(() => resolve(port))
    })
  })
}

function bestLocale(): string {
  if (platform() === 'win32') return 'C'
  try {
    const available = new Set(
      execSync('locale -a', { encoding: 'utf-8', env: { ...process.env, LC_ALL: 'C', LANG: 'C' } })
        .split(/\r?\n/)
        .map((l) => l.trim())
        .filter(Boolean),
    )
    if (available.has('en_US.UTF-8')) return 'en_US.UTF-8'
    if (available.has('C.UTF-8')) return 'C.UTF-8'
    if (available.has('en_US.utf8')) return 'en_US.utf8'
  } catch {
    // fall through
  }
  return 'C'
}

async function waitForPg(conn: { host: string; port: number }, timeoutMs = 30_000): Promise<void> {
  const deadline = Date.now() + timeoutMs
  while (Date.now() < deadline) {
    try {
      const client = new pg.Client({ host: conn.host, port: conn.port, user: 'test', database: 'postgres' })
      await client.connect()
      await client.end()
      return
    } catch {
      await new Promise((r) => setTimeout(r, 200))
    }
  }
  throw new Error('postgres did not become ready in time')
}

export async function startPg(): Promise<{
  db: TransactionalDatabase<FieldsSchema>
  stop: () => Promise<void>
}> {
  const { initdb, postgres: postgresBin } = await getBins()
  const port = await freePort()
  const dataDir = await mkdtemp(join(tmpdir(), 'fields-pg-'))

  const locale = bestLocale()
  const localeEnv = { ...process.env, LC_ALL: 'C', LC_MESSAGES: locale, LANG: 'C' }

  await initdbCached({
    initdbBin: initdb,
    dataDir,
    user: 'test',
    args: ['-A', 'trust', '--no-sync', `--lc-messages=${locale}`],
    env: localeEnv,
  })

  let stderrTail = ''
  let proc!: ChildProcess
  await new Promise<void>((resolve, reject) => {
    proc = spawn(
      postgresBin,
      ['-D', dataDir, '-k', dataDir, '-F', '-p', String(port), '-c', 'listen_addresses=', '-c', 'dynamic_shared_memory_type=mmap'],
      { stdio: ['ignore', 'ignore', 'pipe'], env: localeEnv },
    )
    const timer = setTimeout(() => {
      proc.kill('SIGKILL')
      reject(new Error('postgres did not become ready in time'))
    }, 30_000)
    proc.stderr?.on('data', (chunk: Buffer) => {
      const message = chunk.toString('utf-8')
      stderrTail = (stderrTail + message).slice(-2000)
      if (message.includes('database system is ready to accept connections')) {
        clearTimeout(timer)
        resolve()
      }
    })
    proc.on('error', (err) => {
      clearTimeout(timer)
      reject(err)
    })
    proc.on('exit', (code) => {
      if (code !== null && code !== 0) {
        clearTimeout(timer)
        reject(new Error(`postgres exited ${code}${stderrTail.trim() ? `\n${stderrTail.trim()}` : ''}`))
      }
    })
  })

  await waitForPg({ host: dataDir, port })

  const pool = new pg.Pool({ host: dataDir, port, user: 'test', database: 'postgres', max: 1 })
  const db = drizzle(pool, { schema: fieldsSchema })

  return {
    db: withTransactionIdentity(db) as unknown as TransactionalDatabase<FieldsSchema>,
    stop: async () => {
      await pool.end()
      proc.kill('SIGKILL')
      await rm(dataDir, { recursive: true, force: true })
    },
  }
}

/** Test helper — spins embedded PG and returns db + teardown. */
export async function makePgHarness(): Promise<{
  db: TransactionalDatabase<FieldsSchema>
  teardown: () => Promise<void>
}> {
  const { db, stop } = await startPg()
  return { db, teardown: stop }
}
