import { and, eq, isNull } from 'drizzle-orm';

import { db } from '@/lib/db';
import { systemSettings } from '@/lib/db/schema';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { NotFoundError } from '@/lib/errors';
import { emitToRoom } from '@/lib/socket/server';

export async function listSettings(portId: string) {
  // Get port-specific settings
  const portSettings = await db
    .select()
    .from(systemSettings)
    .where(eq(systemSettings.portId, portId))
    .orderBy(systemSettings.key);

  // Get global settings (portId is null)
  const globalSettings = await db
    .select()
    .from(systemSettings)
    .where(isNull(systemSettings.portId))
    .orderBy(systemSettings.key);

  return { portSettings, globalSettings };
}

export async function getSetting(key: string, portId: string) {
  // Try port-specific first, fall back to global
  const setting = await db.query.systemSettings.findFirst({
    where: and(eq(systemSettings.key, key), eq(systemSettings.portId, portId)),
  });
  if (setting) return setting;

  const global = await db.query.systemSettings.findFirst({
    where: and(eq(systemSettings.key, key), isNull(systemSettings.portId)),
  });
  return global ?? null;
}

export async function upsertSetting(key: string, value: unknown, portId: string, meta: AuditMeta) {
  // Read existing first for the audit-log diff (before/after). The actual
  // write goes through onConflictDoUpdate so two concurrent calls can't
  // both observe `existing=null` and both INSERT - the (key, port_id)
  // unique index now treats NULLs as equal (migration 0047).
  const existing = await db.query.systemSettings.findFirst({
    where: and(eq(systemSettings.key, key), eq(systemSettings.portId, portId)),
  });

  await db
    .insert(systemSettings)
    .values({
      key,
      value: value as Record<string, unknown>,
      portId,
      updatedBy: meta.userId,
    })
    .onConflictDoUpdate({
      target: [systemSettings.key, systemSettings.portId],
      set: {
        value: value as Record<string, unknown>,
        updatedBy: meta.userId,
        updatedAt: new Date(),
      },
    });

  // H-06: keys ending with `_encrypted` carry AES-GCM ciphertext that's only
  // useful with EMAIL_CREDENTIAL_KEY. Recording the ciphertext verbatim in
  // audit_logs.new_value would turn the audit log (readable by any admin
  // with `admin.view_audit_log`) into a credential bundle - if the
  // encryption key is ever rotated/leaked the history exfils every
  // configured password. Mask the value but keep the audit trail itself
  // (who toggled what, when).
  const isEncryptedKey = key.endsWith('_encrypted');
  const auditOldValue = existing
    ? { value: isEncryptedKey ? '[redacted]' : existing.value }
    : undefined;
  const auditNewValue = { value: isEncryptedKey ? '[redacted]' : value };

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: existing ? 'update' : 'create',
    entityType: 'setting',
    entityId: key,
    oldValue: auditOldValue,
    newValue: auditNewValue,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'system:alert', {
    alertType: 'setting:updated',
    message: `Setting "${key}" updated`,
    severity: 'info',
  });

  return { key, value, portId };
}

export async function deleteSetting(key: string, portId: string, meta: AuditMeta) {
  const existing = await db.query.systemSettings.findFirst({
    where: and(eq(systemSettings.key, key), eq(systemSettings.portId, portId)),
  });
  if (!existing) throw new NotFoundError('Setting');

  await db
    .delete(systemSettings)
    .where(and(eq(systemSettings.key, key), eq(systemSettings.portId, portId)));

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'delete',
    entityType: 'setting',
    entityId: key,
    oldValue: { value: key.endsWith('_encrypted') ? '[redacted]' : existing.value },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });
}
