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

import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { db } from '@/lib/db';
import { systemSettings } from '@/lib/db/schema';
import { NotFoundError, ValidationError } from '@/lib/errors';
import { decrypt, encrypt } from '@/lib/utils/encryption';

import { registryFor } from './registry';
import type { ResolvedSetting, SettingEntry, SettingSource } from './types';

/**
 * Stored shape for encrypted JSONB values. The encrypt() helper returns a
 * JSON string of this shape - we wrap it in the JSONB column verbatim.
 */
interface EncryptedEnvelope {
  iv: string;
  tag: string;
  data: string;
}

function isEncryptedEnvelope(value: unknown): value is EncryptedEnvelope {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof (value as { iv?: unknown }).iv === 'string' &&
    typeof (value as { tag?: unknown }).tag === 'string' &&
    typeof (value as { data?: unknown }).data === 'string'
  );
}

/**
 * Validator inferred from the entry type when no explicit `validator` is set.
 * Keeps the registry concise - only override when standard rules don't fit.
 */
function defaultValidator(entry: SettingEntry): z.ZodTypeAny {
  if (entry.validator) return entry.validator;
  switch (entry.type) {
    case 'string':
    case 'password':
    case 'textarea':
    case 'user-select':
      return z.string();
    case 'url':
      return z.string().url();
    case 'email':
      return z.string().email();
    case 'number':
      return z.coerce.number();
    case 'boolean':
      return z.coerce.boolean();
    case 'select':
    case 'radio':
      if (entry.options) {
        return z.enum(entry.options.map((o) => o.value) as [string, ...string[]]);
      }
      return z.string();
    default:
      return z.unknown();
  }
}

function coerceForType(entry: SettingEntry, raw: unknown): unknown {
  if (raw == null) return null;
  if (entry.transform) return entry.transform(raw);
  if (entry.type === 'number') {
    const n = typeof raw === 'number' ? raw : Number(raw);
    return Number.isFinite(n) ? n : null;
  }
  if (entry.type === 'boolean') {
    if (typeof raw === 'boolean') return raw;
    if (raw === 'true' || raw === '1') return true;
    if (raw === 'false' || raw === '0') return false;
    return Boolean(raw);
  }
  return raw;
}

function readEnvValue(entry: SettingEntry): unknown | null {
  if (!entry.envFallback) return null;
  const v = process.env[entry.envFallback];
  if (v == null || v === '') return null;
  return coerceForType(entry, v);
}

function unwrapStoredValue(entry: SettingEntry, stored: unknown): unknown {
  if (stored == null) return null;
  if (entry.encrypted && isEncryptedEnvelope(stored)) {
    return decrypt(JSON.stringify(stored));
  }
  // Settings written via the legacy upsertSetting helper wrap the value in
  // `{ value: ... }`. Unwrap that shape transparently for backward compat.
  if (
    typeof stored === 'object' &&
    stored !== null &&
    'value' in stored &&
    Object.keys(stored as object).length === 1
  ) {
    return (stored as { value: unknown }).value;
  }
  return stored;
}

interface ResolvedRaw {
  source: SettingSource;
  rawValue: unknown;
}

/**
 * Lower-level lookup that returns both the resolved value AND the source it
 * came from (port row, global row, env, or registry default). The admin API
 * uses this directly to drive the "Using env fallback" badge; service code
 * usually calls `getSetting()` which discards the source.
 */
export async function resolveSettingWithSource(
  key: string,
  portId: string | null,
): Promise<ResolvedRaw> {
  const entry = registryFor(key);
  if (!entry) throw new Error(`Unknown setting key: ${key}`);

  // 1. Port-specific row (only meaningful for port-scoped entries).
  if (portId && entry.scope === 'port') {
    const row = await db.query.systemSettings.findFirst({
      where: and(eq(systemSettings.key, key), eq(systemSettings.portId, portId)),
    });
    if (row?.value != null) {
      return { source: 'port', rawValue: unwrapStoredValue(entry, row.value) };
    }
  }

  // 2. Global row (port_id IS NULL).
  const globalRow = await db.query.systemSettings.findFirst({
    where: and(eq(systemSettings.key, key), isNull(systemSettings.portId)),
  });
  if (globalRow?.value != null) {
    return { source: 'global', rawValue: unwrapStoredValue(entry, globalRow.value) };
  }

  // 3. Env fallback.
  const envValue = readEnvValue(entry);
  if (envValue != null) {
    return { source: 'env', rawValue: envValue };
  }

  // 4. Registry default.
  return { source: 'default', rawValue: entry.defaultValue ?? null };
}

/**
 * Resolves a setting value through the precedence chain: port → global → env
 * → registry default. Encrypted values are decrypted on the way out.
 *
 * Use this from service code that needs the concrete cleartext value
 * (e.g. building an outbound Documenso request).
 */
export async function getSetting<T = unknown>(
  key: string,
  portId: string | null,
): Promise<T | null> {
  const { rawValue } = await resolveSettingWithSource(key, portId);
  return rawValue as T | null;
}

/**
 * Batch resolver - efficient for the admin form which needs every field in a
 * section. Returns a map keyed by setting key.
 */
export async function resolveSettings(
  keys: string[],
  portId: string | null,
): Promise<Map<string, ResolvedRaw>> {
  const out = new Map<string, ResolvedRaw>();
  await Promise.all(
    keys.map(async (k) => {
      try {
        out.set(k, await resolveSettingWithSource(k, portId));
      } catch {
        // Unknown registry key — common when a feature stores settings via
        // its own dedicated route (e.g. branding) and a batch caller asks
        // for them by key. Skipping keeps the rest of the batch usable;
        // single-key callers via getSetting() still fail loud.
      }
    }),
  );
  return out;
}

/**
 * Shape returned to the admin API. Sensitive fields surface `isSet` only.
 */
export async function resolveForAdminAPI(
  keys: string[],
  portId: string | null,
): Promise<Map<string, ResolvedSetting>> {
  const resolved = await resolveSettings(keys, portId);
  const out = new Map<string, ResolvedSetting>();
  for (const key of keys) {
    const entry = registryFor(key);
    if (!entry) continue;
    const r = resolved.get(key);
    if (!r) continue;
    const isSet = r.source !== 'default' && r.rawValue != null && r.rawValue !== '';
    const surfaceSensitive = entry.sensitive || entry.encrypted;
    out.set(key, {
      key,
      source: r.source,
      isSet,
      value: surfaceSensitive ? undefined : (r.rawValue ?? undefined),
    });
  }
  return out;
}

/**
 * Validate and persist a setting. Encrypts if registered as encrypted. Always
 * writes to the row scope appropriate to the entry: port-scoped entries with
 * a non-null portId write the port row; global-scoped entries (or when called
 * with portId=null) write the global row.
 */
export async function writeSetting(
  key: string,
  rawValue: unknown,
  portId: string | null,
  meta: AuditMeta,
): Promise<void> {
  const entry = registryFor(key);
  if (!entry) throw new ValidationError(`Unknown setting: ${key}`);

  // Empty value on a settable field == delete the row (revert to fallback).
  // Sensitive/encrypted: empty input means "don't change" rather than
  // "revert" - UI shows ••• placeholder so an unchanged save shouldn't
  // wipe the stored ciphertext. The dedicated DELETE endpoint exists for
  // explicit reverts.
  if (rawValue === '' || rawValue == null) {
    if (entry.encrypted || entry.sensitive) {
      // No-op: leaving the existing row untouched.
      return;
    }
    await deleteSetting(key, portId, meta);
    return;
  }

  const validator = defaultValidator(entry);
  const parsed = validator.safeParse(rawValue);
  if (!parsed.success) {
    throw new ValidationError(
      `Invalid value for "${key}": ${parsed.error.issues
        .map((i) => `${i.path.join('.')}: ${i.message}`)
        .join('; ')}`,
    );
  }
  const value = parsed.data;

  const writePortId = entry.scope === 'global' ? null : portId;
  const storedValue = entry.encrypted
    ? (JSON.parse(encrypt(String(value))) as EncryptedEnvelope)
    : value;

  // Read existing for audit diff.
  const existing = writePortId
    ? await db.query.systemSettings.findFirst({
        where: and(eq(systemSettings.key, key), eq(systemSettings.portId, writePortId)),
      })
    : await db.query.systemSettings.findFirst({
        where: and(eq(systemSettings.key, key), isNull(systemSettings.portId)),
      });

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

  // Audit-log with redaction for sensitive / encrypted fields - fixes AU-02
  // (encrypted ciphertext stored in audit_logs.new_value).
  const isSecret = entry.encrypted || entry.sensitive;
  void createAuditLog({
    userId: meta.userId,
    portId: meta.portId,
    action: existing ? 'update' : 'create',
    entityType: 'setting',
    entityId: key,
    oldValue: existing ? { value: isSecret ? '[redacted]' : existing.value } : undefined,
    newValue: { value: isSecret ? '[redacted]' : value },
    metadata: { settingKey: key, scope: entry.scope },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });
}

/**
 * Delete a setting row, reverting the resolver to global → env → default.
 * No-op (with NotFoundError) if no row exists at the target scope.
 */
export async function deleteSetting(
  key: string,
  portId: string | null,
  meta: AuditMeta,
): Promise<void> {
  const entry = registryFor(key);
  if (!entry) throw new ValidationError(`Unknown setting: ${key}`);

  const writePortId = entry.scope === 'global' ? null : portId;

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

  if (!existing) throw new NotFoundError('Setting');

  await db
    .delete(systemSettings)
    .where(
      writePortId
        ? and(eq(systemSettings.key, key), eq(systemSettings.portId, writePortId))
        : and(eq(systemSettings.key, key), isNull(systemSettings.portId)),
    );

  const isSecret = entry.encrypted || entry.sensitive;
  void createAuditLog({
    userId: meta.userId,
    portId: meta.portId,
    action: 'delete',
    entityType: 'setting',
    entityId: key,
    oldValue: { value: isSecret ? '[redacted]' : existing.value },
    metadata: { settingKey: key, scope: entry.scope },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });
}

/**
 * One-click migration: read the env var named in `entry.envFallback`, write
 * it as the current scope's row. Used by the admin UI "Copy from env" button.
 */
export async function copyFromEnv(
  key: string,
  portId: string | null,
  meta: AuditMeta,
): Promise<{ copied: boolean; envValue?: string }> {
  const entry = registryFor(key);
  if (!entry) throw new ValidationError(`Unknown setting: ${key}`);
  if (!entry.envFallback) {
    throw new ValidationError(`Setting "${key}" has no env fallback configured`);
  }
  const envValue = process.env[entry.envFallback];
  if (envValue == null || envValue === '') {
    return { copied: false };
  }
  await writeSetting(key, envValue, portId, meta);
  return { copied: true, envValue: entry.encrypted || entry.sensitive ? undefined : envValue };
}
