import crypto from 'crypto';
import { exec as execCb } from 'child_process';
import { promisify } from 'util';
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import { prisma } from '../lib/prisma';
import { encrypt, decrypt } from '../utils/encryption';
import { logger } from '../utils/logger';

const exec = promisify(execCb);

const CA_VALIDITY_DAYS = 3650; // ~10 years
const AGENT_CERT_VALIDITY_DAYS = 730; // ~2 years

/**
 * Shell/cert-injection guard. Slug flows into the OpenSSL -subj `/CN=...` string
 * and into SAN DNS entries. An unvalidated slug like `foo/O=EvilOrg/CN=` would
 * let callers forge arbitrary DN components. Added 2026-04-12.
 */
const SAFE_SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
function assertSafeSlug(slug: string): void {
  if (!SAFE_SLUG.test(slug)) {
    throw new Error(`Invalid agent slug: must match ${SAFE_SLUG}`);
  }
}

function computeFingerprint(certPem: string): string {
  const der = Buffer.from(
    certPem
      .replace(/-----BEGIN CERTIFICATE-----/g, '')
      .replace(/-----END CERTIFICATE-----/g, '')
      .replace(/\s/g, ''),
    'base64'
  );
  return crypto.createHash('sha256').update(der).digest('hex');
}

/**
 * Run openssl commands in a temp directory, then clean up.
 */
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'ccp-cert-'));
  try {
    return await fn(dir);
  } finally {
    await fs.rm(dir, { recursive: true, force: true });
  }
}

/**
 * Ensure a Certificate Authority exists. Creates one if none exists.
 */
export async function ensureCA() {
  const existing = await prisma.ccpCertificateAuthority.findFirst({
    orderBy: { createdAt: 'desc' },
  });

  if (existing && existing.expiresAt > new Date()) {
    return existing;
  }

  logger.info('Generating new CCP Certificate Authority...');

  const { keyPem, certPem } = await withTempDir(async (dir) => {
    const keyFile = path.join(dir, 'ca.key');
    const certFile = path.join(dir, 'ca.crt');

    // Generate CA key + self-signed cert
    await exec(
      `openssl req -x509 -newkey rsa:4096 -keyout "${keyFile}" -out "${certFile}" ` +
      `-days ${CA_VALIDITY_DAYS} -nodes ` +
      `-subj "/CN=CCP Certificate Authority/O=Changemaker Control Panel"`,
      { timeout: 30_000 }
    );

    return {
      keyPem: await fs.readFile(keyFile, 'utf-8'),
      certPem: await fs.readFile(certFile, 'utf-8'),
    };
  });

  const fingerprint = computeFingerprint(certPem);
  const expiresAt = new Date();
  expiresAt.setDate(expiresAt.getDate() + CA_VALIDITY_DAYS);

  const ca = await prisma.ccpCertificateAuthority.create({
    data: {
      commonName: 'CCP Certificate Authority',
      encryptedKey: encrypt(keyPem),
      certPem,
      fingerprint,
      expiresAt,
    },
  });

  logger.info(`CA created: fingerprint=${fingerprint.substring(0, 16)}...`);
  return ca;
}

/**
 * Issue a certificate for a remote agent, signed by the CA.
 * Returns the certificate materials (plaintext) for one-time display.
 */
export async function issueAgentCert(instanceId: string, slug: string, agentUrl?: string) {
  assertSafeSlug(slug);
  const ca = await ensureCA();
  const caKeyPem = decrypt(ca.encryptedKey);

  const commonName = `ccp-agent-${slug}`;

  const { agentKeyPem, agentCertPem } = await withTempDir(async (dir) => {
    const caKeyFile = path.join(dir, 'ca.key');
    const caCertFile = path.join(dir, 'ca.crt');
    const agentKeyFile = path.join(dir, 'agent.key');
    const agentCsrFile = path.join(dir, 'agent.csr');
    const agentCertFile = path.join(dir, 'agent.crt');
    const serialFile = path.join(dir, 'serial');
    const extFile = path.join(dir, 'ext.cnf');

    // Write CA materials
    await fs.writeFile(caKeyFile, caKeyPem);
    await fs.writeFile(caCertFile, ca.certPem);
    await fs.writeFile(serialFile, crypto.randomBytes(16).toString('hex'));

    // Build SAN entries from the agent URL hostname
    const sanEntries: string[] = [];
    if (agentUrl) {
      try {
        const hostname = new URL(agentUrl).hostname;
        // Guard against SAN injection via crafted hostname (commas/newlines would
        // inject extra SAN entries into the extfile). Added 2026-04-12.
        if (/[,\n\r\0]/.test(hostname)) {
          throw new Error('Invalid hostname');
        }
        // Detect IP vs DNS name
        if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) || hostname.includes(':')) {
          sanEntries.push(`IP:${hostname}`);
        } else {
          sanEntries.push(`DNS:${hostname}`);
        }
      } catch { /* ignore invalid URL */ }
    }
    sanEntries.push(`DNS:${commonName}`);

    // Extensions for server+client auth with SANs
    const extLines = [
      'basicConstraints=CA:FALSE',
      'keyUsage=digitalSignature,keyEncipherment',
      'extendedKeyUsage=serverAuth,clientAuth',
      `subjectAltName=${sanEntries.join(',')}`,
    ];
    await fs.writeFile(extFile, extLines.join('\n'));

    // Generate agent key
    await exec(
      `openssl genrsa -out "${agentKeyFile}" 2048`,
      { timeout: 15_000 }
    );

    // Generate CSR
    await exec(
      `openssl req -new -key "${agentKeyFile}" -out "${agentCsrFile}" ` +
      `-subj "/CN=${commonName}/O=Changemaker Lite Agent"`,
      { timeout: 15_000 }
    );

    // Sign CSR with CA
    await exec(
      `openssl x509 -req -in "${agentCsrFile}" ` +
      `-CA "${caCertFile}" -CAkey "${caKeyFile}" ` +
      `-CAserial "${serialFile}" ` +
      `-out "${agentCertFile}" -days ${AGENT_CERT_VALIDITY_DAYS} ` +
      `-extfile "${extFile}"`,
      { timeout: 15_000 }
    );

    return {
      agentKeyPem: await fs.readFile(agentKeyFile, 'utf-8'),
      agentCertPem: await fs.readFile(agentCertFile, 'utf-8'),
    };
  });

  const fingerprint = computeFingerprint(agentCertPem);
  const expiresAt = new Date();
  expiresAt.setDate(expiresAt.getDate() + AGENT_CERT_VALIDITY_DAYS);

  // Revoke any existing cert for this instance
  await prisma.issuedAgentCert.deleteMany({ where: { instanceId } });

  // Store the issued cert
  await prisma.issuedAgentCert.create({
    data: {
      caId: ca.id,
      instanceId,
      commonName,
      encryptedKey: encrypt(agentKeyPem),
      certPem: agentCertPem,
      fingerprint,
      expiresAt,
    },
  });

  // Update instance with the agent fingerprint
  await prisma.instance.update({
    where: { id: instanceId },
    data: { agentFingerprint: fingerprint },
  });

  logger.info(`Agent cert issued for ${slug}: fingerprint=${fingerprint.substring(0, 16)}...`);

  return {
    caCertPem: ca.certPem,
    agentCertPem,
    agentKeyPem, // Plaintext — display once, never retrievable again
    fingerprint,
    caFingerprint: ca.fingerprint,
  };
}

/**
 * Revoke an agent's certificate.
 */
export async function revokeAgentCert(instanceId: string) {
  const cert = await prisma.issuedAgentCert.findUnique({ where: { instanceId } });
  if (!cert) return;

  await prisma.issuedAgentCert.update({
    where: { id: cert.id },
    data: { revokedAt: new Date() },
  });

  await prisma.instance.update({
    where: { id: instanceId },
    data: { agentFingerprint: null },
  });

  logger.info(`Agent cert revoked for instance ${instanceId}`);
}

/**
 * Get the mTLS materials CCP needs to present when calling a remote agent.
 */
export async function getAgentClientMaterials(instanceId: string) {
  const cert = await prisma.issuedAgentCert.findUnique({
    where: { instanceId },
    include: { ca: true },
  });

  if (!cert || cert.revokedAt) return null;

  return {
    agentCertPem: cert.certPem,
    agentKeyPem: decrypt(cert.encryptedKey),
    caCertPem: cert.ca.certPem,
    fingerprint: cert.fingerprint,
    expiresAt: cert.expiresAt,
  };
}

/**
 * Get the CA public certificate (for manual agent setup).
 */
export async function getCACert() {
  const ca = await ensureCA();
  return {
    certPem: ca.certPem,
    fingerprint: ca.fingerprint,
    expiresAt: ca.expiresAt,
  };
}
