import { spawn, type ChildProcess } from 'node:child_process';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { createRequire } from 'node:module';
import { initdbCached } from '@tooling/pg-template';
import { sql } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/node-postgres';
import type { Querier, TransactionalDatabase } from '@platform-modules/db';
import pg from 'pg';
import { pushSchema as catalogPushSchema, catalogSchema } from '@platform-modules/commerce-catalog';
import { pushSchema as ordersPushSchema, ordersSchema } from '@platform-modules/commerce-orders';
import { pushSchema as inventoryPushSchema, inventorySchema } from '@platform-modules/commerce-inventory';
import { CREATE_SETTINGS_TABLE_SQL } from './settings.js';
import { CREATE_CHARGE_INTENTS_TABLE_SQL, CREATE_WEBHOOK_EVENTS_TABLE_SQL } from './checkout-store.js';

// Unix-domain socket only — no TCP port allocation, no TOCTOU race.
const PG_PORT = 5432;

export type StorefrontSchema = typeof catalogSchema & typeof ordersSchema & typeof inventorySchema;

function embeddedPlatformPackage(): string {
  const { platform, arch } = process;
  if (platform === 'linux' && arch === 'x64') return 'linux-x64';
  if (platform === 'linux' && arch === 'arm64') return 'linux-arm64';
  if (platform === 'linux' && arch === 'arm') return 'linux-arm';
  if (platform === 'linux' && arch === 'ia32') return 'linux-ia32';
  if (platform === 'linux' && arch === 'ppc64') return 'linux-ppc64';
  if (platform === 'darwin' && arch === 'arm64') return 'darwin-arm64';
  if (platform === 'darwin' && arch === 'x64') return 'darwin-x64';
  if (platform === 'win32' && arch === 'x64') return 'windows-x64';
  throw new Error(`unsupported embedded-postgres platform: ${platform}-${arch}`);
}

function resolveBinDir(): string {
  const require = createRequire(import.meta.url);
  const pkg = `@embedded-postgres/${embeddedPlatformPackage()}`;
  const entry = require.resolve(pkg);
  return join(dirname(entry), '..', 'native', 'bin');
}

const PG_ENV = { ...process.env, LC_ALL: 'C', LANG: 'C' };

async function waitForPostgres(pool: pg.Pool, attempts = 60, delayMs = 250): Promise<void> {
  let lastErr: unknown;
  for (let i = 0; i < attempts; i++) {
    try {
      await pool.query('SELECT 1');
      return;
    } catch (e) {
      lastErr = e;
      await new Promise((r) => setTimeout(r, delayMs));
    }
  }
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
}

export async function startPg(): Promise<{
  db: TransactionalDatabase<StorefrontSchema>;
  stop: () => Promise<void>;
}> {
  const binDir = resolveBinDir();
  const ext = process.platform === 'win32' ? '.exe' : '';
  const initdbBin = join(binDir, `initdb${ext}`);
  const postgresBin = join(binDir, `postgres${ext}`);
  const dataDir = await mkdtemp(join(tmpdir(), 'mod-storefront-pg-'));
  const user = 'test';

  await initdbCached({ initdbBin, dataDir, user, args: ['-A', 'trust', '--no-sync'], env: PG_ENV });

  // `listen_addresses=` (empty) disables TCP — clients connect via the Unix socket at
  // `-k dataDir`. The hardcoded PG_PORT only names the socket file, not a TCP bind.
  // Cast to ChildProcess: spawn with a typed stdio tuple returns ChildProcessByStdio<...>
  // which TS doesn't expose .on() on at the call-site level, even though it extends ChildProcess.
  const proc = spawn(
    postgresBin,
    ['-D', dataDir, '-k', dataDir, '-F', '-p', String(PG_PORT), '-c', 'listen_addresses='],
    { stdio: ['ignore', 'ignore', 'pipe'], env: PG_ENV },
  ) as unknown as ChildProcess;

  // Keep the tail of stderr so an early death reports WHY, not just a timeout.
  let stderrTail = '';
  proc.stderr?.on('data', (chunk: Buffer) => {
    stderrTail = (stderrTail + chunk.toString()).slice(-2000);
  });

  // An early death must surface immediately, not as a silent 15s waitForPostgres timeout.
  // After `ready`, SIGKILL from stop() is expected — guard so teardown never rejects.
  let ready = false;
  const earlyExit = new Promise<never>((_, reject) => {
    proc.on('error', (err: Error) => {
      if (!ready) reject(err);
    });
    proc.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {
      if (!ready) {
        const detail = stderrTail.trim();
        reject(
          new Error(
            `postgres exited before ready (code=${code}, signal=${signal})${detail ? `\n${detail}` : ''}`,
          ),
        );
      }
    });
  });

  const pool = new pg.Pool({ host: dataDir, user, database: 'postgres', port: PG_PORT, max: 10 });

  try {
    await Promise.race([waitForPostgres(pool), earlyExit]);
  } catch (err) {
    await pool.end().catch(() => undefined);
    if (!proc.killed) proc.kill('SIGKILL');
    await rm(dataDir, { recursive: true, force: true });
    throw err;
  }
  ready = true;

  const schema = { ...catalogSchema, ...ordersSchema, ...inventorySchema };
  const db = drizzle(pool, { schema }) as unknown as TransactionalDatabase<StorefrontSchema>;

  const querier = db as unknown as Querier<StorefrontSchema>;
  await catalogPushSchema(querier);
  await ordersPushSchema(querier);
  await inventoryPushSchema(querier);
  await db.execute(sql.raw(CREATE_SETTINGS_TABLE_SQL));
  await db.execute(sql.raw(CREATE_CHARGE_INTENTS_TABLE_SQL));
  await db.execute(sql.raw(CREATE_WEBHOOK_EVENTS_TABLE_SQL));

  return {
    db,
    stop: async () => {
      await pool.end().catch(() => undefined);
      if (!proc.killed) proc.kill('SIGKILL');
      await rm(dataDir, { recursive: true, force: true });
    },
  };
}
