/**
 * Typed accessors for port-level configuration with env-fallback.
 *
 * Settings are stored in the `system_settings` table keyed by (key, portId).
 * The functions in this module resolve a port's effective configuration for
 * a given domain (email, Documenso, branding, reminders) by reading the
 * port-scoped row first, falling back to the global row, and finally to the
 * env var when neither is set.
 */
import { env } from '@/lib/env';
import { normalizeBrandingUrl } from '@/lib/branding/url';
import { getSetting } from '@/lib/services/settings.service';
import { normalizeDocumensoApiUrl } from '@/lib/validators/settings';

// ─── Setting key constants ───────────────────────────────────────────────────

export const SETTING_KEYS = {
  // Email
  emailFromName: 'email_from_name',
  emailFromAddress: 'email_from_address',
  emailReplyTo: 'email_reply_to',
  // §1.4: optional per-port URL that the supplemental-info email links
  // to (typically the marketing site's hosted form). When blank, the
  // built-in CRM route `/public/supplemental-info/<token>` is used.
  supplementalFormUrl: 'supplemental_form_url',
  // email_signature_html / email_footer_html - removed; the email shell
  // reads branding_email_header_html / branding_email_footer_html from
  // /admin/branding, which is the source of truth.
  emailAllowPersonalAccountSends: 'email_allow_personal_account_sends',
  smtpHostOverride: 'smtp_host_override',
  smtpPortOverride: 'smtp_port_override',
  smtpUserOverride: 'smtp_user_override',
  smtpPassOverride: 'smtp_pass_override',

  // Documenso / EOI
  documensoApiUrlOverride: 'documenso_api_url_override',
  documensoApiKeyOverride: 'documenso_api_key_override',
  documensoApiVersionOverride: 'documenso_api_version_override',
  documensoEoiTemplateId: 'documenso_eoi_template_id',
  // Documenso template recipient slot IDs are per-Documenso-instance
  // numeric values, so they have to follow the per-port template config.
  // Falling back to env keeps single-tenant deploys working.
  documensoClientRecipientId: 'documenso_client_recipient_id',
  documensoDeveloperRecipientId: 'documenso_developer_recipient_id',
  documensoApprovalRecipientId: 'documenso_approval_recipient_id',
  // Per-port Documenso webhook secret - two ports pointed at different
  // Documenso instances cannot share the global env secret. The receiver
  // resolves the matching port by trying each enabled secret with a
  // timing-safe comparison.
  documensoWebhookSecret: 'documenso_webhook_secret',
  eoiDefaultPathway: 'eoi_default_pathway',
  // EOI body-text fill method: 'local' (CRM fills + flattens the PDF, clean
  // 12pt + multiline address wrap, Documenso signs only) vs 'documenso'
  // (legacy: Documenso fills the template AcroForm fields and auto-sizes /
  // clips them). Toggleable per-port in admin → Documenso.
  eoiFillMethod: 'eoi_fill_method',
  // Identity of the developer + approver that the template's static
  // recipient slots get filled with. Old system hardcoded these
  // (David Mizrahi, Abbie May @ portnimara.com) but multi-port deploys
  // need per-port values. Falls back to env or "" if neither set.
  documensoDeveloperName: 'documenso_developer_name',
  documensoDeveloperEmail: 'documenso_developer_email',
  documensoApproverName: 'documenso_approver_name',
  documensoApproverEmail: 'documenso_approver_email',
  // Optional CRM-user binding for the developer + approver slots.
  // When set, the per-port admin UI shows "Linked to <user>" and
  // the webhook handler can match the Documenso developer signer
  // against this user's email for in-CRM signing-status updates.
  // Plan Phase 7 (Project Director RBAC). Stored as the user.id.
  documensoDeveloperUserId: 'documenso_developer_user_id',
  documensoApproverUserId: 'documenso_approver_user_id',
  // Display labels for the developer + approver slots, used in
  // email subjects + signer-progress UI ("Your Project Director,
  // Marie, has signed…"). Defaults to "Developer" / "Approver".
  documensoDeveloperLabel: 'documenso_developer_label',
  documensoApproverLabel: 'documenso_approver_label',
  // Sending behavior for the initial "please sign" invitation email
  // after a document is generated. 'auto' = our branded email goes
  // out immediately; 'manual' = doc generated, signing URL shown in
  // UI, rep clicks a Send button to dispatch. Per-port so different
  // ports can default to different rep workflows.
  eoiSendMode: 'eoi_send_mode',
  // Public-facing host where embedded signing pages live. Used to
  // transform raw Documenso signing URLs into branded
  // {host}/sign/<type>/<token> URLs that go in our outbound emails.
  // Falls back to APP_URL when unset.
  embeddedSigningHost: 'embedded_signing_host',
  // Documenso template IDs for contract / reservation if the port
  // uses templates rather than per-deal uploads. Optional.
  documensoContractTemplateId: 'documenso_contract_template_id',
  documensoReservationTemplateId: 'documenso_reservation_template_id',
  // v2-only: PARALLEL (default) or SEQUENTIAL signing-order enforcement on
  // multi-recipient envelopes. When SEQUENTIAL is set + apiVersion=v2,
  // Documenso refuses to email recipient N+1 until recipient N has signed.
  // Ignored entirely on v1 instances.
  documensoSigningOrder: 'documenso_signing_order',
  // v2-only override of the post-signing redirect URL set on documentMeta.
  // Resolver chain: explicit override -> port's public_site_url -> null
  // (let Documenso use its own default). Lets signers land on the port's
  // marketing site by default without each admin having to configure two
  // settings.
  documensoRedirectUrl: 'documenso_redirect_url',
  // Per-port public marketing-site URL. Used by signing-redirect
  // fallback, email CTAs, and some templates.
  publicSiteUrl: 'public_site_url',

  // Branding
  brandingLogoUrl: 'branding_logo_url',
  brandingPrimaryColor: 'branding_primary_color',
  brandingAppName: 'branding_app_name',
  brandingEmailHeaderHtml: 'branding_email_header_html',
  brandingEmailFooterHtml: 'branding_email_footer_html',
  // Phase 5: per-port background image (the blurred overhead photo
  // shown behind the white card in every transactional email + the
  // branded auth shell). Defaults to the Port Nimara overhead photo
  // when blank.
  brandingEmailBackgroundUrl: 'branding_email_background_url',

  // Reminders (port-level defaults)
  reminderDefaultDays: 'reminder_default_days',
  reminderDefaultEnabled: 'reminder_default_enabled',
  reminderDigestEnabled: 'reminder_digest_enabled',
  reminderDigestTime: 'reminder_digest_time',
  reminderDigestTimezone: 'reminder_digest_timezone',

  // Berths
  berthsDefaultCurrency: 'berths_default_currency',

  // Pipeline auto-advance - per-trigger mode (auto | suggest | off).
  // Stored as a single JSON blob keyed by trigger name so the admin UI
  // edits/saves the full map atomically. Defaults applied in
  // `getStageAdvanceMode` - aggressive defaults match the conventional
  // CRM behaviour (EOI signed → reservation auto-advances).
  stageAdvanceRules: 'stage_advance_rules',

  // Residential partner-forwarding recipients - comma-separated emails
  // that receive a courtesy notification on every new residential
  // inquiry. Blank disables. See createResidentialInterest +
  // forwardResidentialInquiryToPartner for usage.
  residentialPartnerRecipients: 'residential_partner_recipients',
} as const;

// ─── Stage auto-advance ──────────────────────────────────────────────────────

export type StageAdvanceMode = 'auto' | 'suggest' | 'off';

/**
 * Stage transitions that callers can gate through the admin's
 * `stage_advance_rules` setting. Keys are the trigger names already
 * used by the rules engine (`berth-rules-engine.ts`) - keeping them in
 * sync lets a single admin toggle drive both side-effects (berth status)
 * and stage moves.
 */
export type StageAdvanceTrigger =
  | 'eoi_sent'
  | 'eoi_signed'
  | 'reservation_signed'
  | 'deposit_received'
  | 'contract_signed';

const STAGE_ADVANCE_DEFAULTS: Record<StageAdvanceTrigger, StageAdvanceMode> = {
  // Sending the EOI is the moment the deal formally enters the document-
  // signing pursuit phase - auto-advance so the kanban tracks reality
  // without a rep having to click.
  eoi_sent: 'auto',
  // EOI signed = formal commitment to proceed → advance to reservation.
  eoi_signed: 'auto',
  // Reservation signed = the deal is now under contract pursuit.
  reservation_signed: 'auto',
  // Deposit received = monies in, deal is committed; the next milestone
  // is contract sign-off.
  deposit_received: 'auto',
  contract_signed: 'auto',
};

/**
 * Resolve the auto-advance mode for a single trigger on a port.
 * Reads `stage_advance_rules` (a JSON object keyed by trigger name) and
 * falls back to the platform default when the port hasn't overridden.
 */
export async function getStageAdvanceMode(
  portId: string,
  trigger: StageAdvanceTrigger,
): Promise<StageAdvanceMode> {
  const raw = await readSetting<Record<string, StageAdvanceMode>>(
    SETTING_KEYS.stageAdvanceRules,
    portId,
  );
  const mode = raw?.[trigger];
  if (mode === 'auto' || mode === 'suggest' || mode === 'off') return mode;
  return STAGE_ADVANCE_DEFAULTS[trigger];
}

export function getStageAdvanceDefaults(): Record<StageAdvanceTrigger, StageAdvanceMode> {
  return { ...STAGE_ADVANCE_DEFAULTS };
}

// ─── Helper ──────────────────────────────────────────────────────────────────

/**
 * Resolves a port-scoped setting through the registry-aware resolver, which
 * applies the port → global → env → registry-default chain and decrypts
 * encrypted entries transparently. The legacy `{ value: <T> }` wrapper from
 * `settings.service.upsertSetting` is unwrapped inside the resolver, so this
 * helper still returns the bare `T | null` shape its callers expect.
 *
 * If the key isn't registered yet (legacy bespoke settings not in the
 * registry), we fall back to the older `settings.service.getSetting()` path
 * for backward compatibility.
 */
export async function readSetting<T>(key: string, portId: string): Promise<T | null> {
  const { registryFor } = await import('@/lib/settings/registry');
  if (registryFor(key)) {
    const { getSetting: getSettingFromRegistry } = await import('@/lib/settings/resolver');
    return (await getSettingFromRegistry<T>(key, portId)) ?? null;
  }
  const setting = await getSetting(key, portId);
  if (!setting) return null;
  // Legacy upsertSetting wrote the value as the JSONB column directly (not
  // wrapped). Newer paths wrap it as `{ value }`. Tolerate both.
  const raw = setting.value as unknown;
  if (
    raw &&
    typeof raw === 'object' &&
    'value' in (raw as Record<string, unknown>) &&
    Object.keys(raw as object).length === 1
  ) {
    return (raw as { value: T }).value;
  }
  return raw as T;
}

// ─── Email ──────────────────────────────────────────────────────────────────

export interface PortEmailConfig {
  fromName: string;
  fromAddress: string;
  replyTo: string | null;
  smtpHost: string;
  smtpPort: number;
  smtpUser: string | null;
  smtpPass: string | null;
  /**
   * When false, only the system (port-config) sender identity is allowed.
   * When true, admins/users may send via their connected personal email
   * account. Defaults to false for safety.
   */
  allowPersonalAccountSends: boolean;
  /**
   * §1.4: optional per-port URL for the supplemental-info email link.
   * When set, the email contains `${supplementalFormUrl}?token=<raw>`;
   * when null, the built-in CRM route is used.
   */
  supplementalFormUrl: string | null;
}

export async function getPortEmailConfig(portId: string): Promise<PortEmailConfig> {
  const [
    fromName,
    fromAddress,
    replyTo,
    smtpHost,
    smtpPort,
    smtpUser,
    smtpPass,
    allowPersonalAccountSends,
    supplementalFormUrl,
  ] = await Promise.all([
    readSetting<string>(SETTING_KEYS.emailFromName, portId),
    readSetting<string>(SETTING_KEYS.emailFromAddress, portId),
    readSetting<string>(SETTING_KEYS.emailReplyTo, portId),
    readSetting<string>(SETTING_KEYS.smtpHostOverride, portId),
    readSetting<number>(SETTING_KEYS.smtpPortOverride, portId),
    readSetting<string>(SETTING_KEYS.smtpUserOverride, portId),
    readSetting<string>(SETTING_KEYS.smtpPassOverride, portId),
    readSetting<boolean>(SETTING_KEYS.emailAllowPersonalAccountSends, portId),
    readSetting<string>(SETTING_KEYS.supplementalFormUrl, portId),
  ]);

  // Parse env.SMTP_FROM into name + address if no port override
  let envFromName = 'Port Nimara CRM';
  let envFromAddress = `noreply@${env.SMTP_HOST}`;
  if (env.SMTP_FROM) {
    const match = env.SMTP_FROM.match(/^(.+?)\s*<(.+)>$/);
    if (match) {
      envFromName = match[1]!.trim();
      envFromAddress = match[2]!.trim();
    } else {
      envFromAddress = env.SMTP_FROM;
    }
  }

  return {
    fromName: fromName ?? envFromName,
    fromAddress: fromAddress ?? envFromAddress,
    replyTo: replyTo ?? null,
    smtpHost: smtpHost ?? env.SMTP_HOST ?? '',
    smtpPort: smtpPort ?? env.SMTP_PORT ?? 587,
    smtpUser: smtpUser ?? env.SMTP_USER ?? null,
    smtpPass: smtpPass ?? env.SMTP_PASS ?? null,
    allowPersonalAccountSends: allowPersonalAccountSends ?? false,
    supplementalFormUrl: supplementalFormUrl ?? null,
  };
}

// ─── Documenso ──────────────────────────────────────────────────────────────

export type EoiPathway = 'documenso-template' | 'inapp';
export type DocumensoApiVersion = 'v1' | 'v2';
export type EoiSendMode = 'auto' | 'manual';

export interface PortDocumensoConfig {
  apiUrl: string;
  apiKey: string;
  apiVersion: DocumensoApiVersion;
  /** Resolution provenance - `port` / `global` / `env` / `default` /
   *  `none`. Surfaces in DOCUMENSO_AUTH_FAILURE messages so a 401 in
   *  prod tells the operator "this came from env fallback" vs "this
   *  came from a per-port admin entry" without checking logs. */
  apiKeySource: 'port' | 'global' | 'env' | 'default' | 'none';
  apiUrlSource: 'port' | 'global' | 'env' | 'default' | 'none';
  eoiTemplateId: number;
  defaultPathway: EoiPathway;
  /**
   * EOI body-text fill method:
   * - 'local'    : CRM fills + flattens the source PDF (pdf-lib, fixed 12pt +
   *                multiline address wrapping), then uploads the flattened PDF
   *                to Documenso for signature placement only. Renders cleanly.
   * - 'documenso': legacy — Documenso fills the template's AcroForm fields via
   *                the template-generate API (auto-sizes the text → clips it).
   * Toggleable per-port in admin → Documenso. Defaults to 'local'.
   */
  eoiFillMethod: 'local' | 'documenso';
  /** Documenso template recipient slot IDs (per-instance numeric). */
  clientRecipientId: number;
  developerRecipientId: number;
  approvalRecipientId: number;
  /** Static developer + approver identity per port (was hardcoded in old system). */
  developerName: string;
  developerEmail: string;
  approverName: string;
  approverEmail: string;
  /**
   * Auto = system sends our branded "please sign" email immediately
   * after generation. Manual = generates only; rep clicks a separate
   * Send button. Defaults to 'manual' to match the old system's
   * behavior (which also doesn't auto-send).
   */
  sendMode: EoiSendMode;
  /**
   * Host that wraps Documenso signing URLs into branded embed URLs.
   * Outbound emails point here for the actual sign UI. e.g.
   * `https://portnimara.com` makes sign URLs look like
   * `https://portnimara.com/sign/<type>/<token>`.
   */
  embeddedSigningHost: string | null;
  /** Optional template IDs for contract / reservation. null = use
   *  upload-and-place-fields per deal instead of templates. */
  contractTemplateId: number | null;
  reservationTemplateId: number | null;
  /** Per-port display labels for the developer + approver slots - drive
   *  email subjects and signer-progress UI copy. */
  developerLabel: string;
  approverLabel: string;
  /** Optional CRM-user binding for the developer / approver slots.
   *  When set, the per-port admin UI auto-fills name/email from the
   *  user's profile and the webhook handler matches against this
   *  user's email for in-CRM signing-status updates. */
  developerUserId: string | null;
  approverUserId: string | null;
  /**
   * v2-only: PARALLEL (default) or SEQUENTIAL signing-order enforcement.
   * `null` keeps the upstream default (PARALLEL); a non-null value gets
   * passed verbatim. v1 instances ignore this - see admin Documenso page.
   */
  signingOrder: 'PARALLEL' | 'SEQUENTIAL' | null;
  /**
   * v2-only: post-signing redirect URL set on documentMeta. When null,
   * the upstream Documenso default applies (Documenso's own thank-you
   * page). Typically set to `{embeddedSigningHost}/sign/success` so
   * signers land back on the branded marketing site.
   */
  redirectUrl: string | null;
}

function toIntOrNull(raw: unknown): number | null {
  if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
  if (typeof raw === 'string' && raw.trim()) {
    const n = Number(raw);
    return Number.isFinite(n) ? n : null;
  }
  return null;
}

export async function getPortDocumensoConfig(portId: string): Promise<PortDocumensoConfig> {
  const [
    apiUrl,
    apiKey,
    apiVersion,
    eoiTemplateId,
    clientRecipientId,
    developerRecipientId,
    approvalRecipientId,
    defaultPathway,
    eoiFillMethod,
    developerName,
    developerEmail,
    approverName,
    approverEmail,
    sendMode,
    embeddedSigningHost,
    contractTemplateId,
    reservationTemplateId,
    developerLabel,
    approverLabel,
    developerUserId,
    approverUserId,
    signingOrder,
    redirectUrlOverride,
    publicSiteUrl,
  ] = await Promise.all([
    readSetting<string>(SETTING_KEYS.documensoApiUrlOverride, portId),
    readSetting<string>(SETTING_KEYS.documensoApiKeyOverride, portId),
    readSetting<DocumensoApiVersion>(SETTING_KEYS.documensoApiVersionOverride, portId),
    readSetting<string | number>(SETTING_KEYS.documensoEoiTemplateId, portId),
    readSetting<string | number>(SETTING_KEYS.documensoClientRecipientId, portId),
    readSetting<string | number>(SETTING_KEYS.documensoDeveloperRecipientId, portId),
    readSetting<string | number>(SETTING_KEYS.documensoApprovalRecipientId, portId),
    readSetting<EoiPathway>(SETTING_KEYS.eoiDefaultPathway, portId),
    readSetting<'local' | 'documenso'>(SETTING_KEYS.eoiFillMethod, portId),
    readSetting<string>(SETTING_KEYS.documensoDeveloperName, portId),
    readSetting<string>(SETTING_KEYS.documensoDeveloperEmail, portId),
    readSetting<string>(SETTING_KEYS.documensoApproverName, portId),
    readSetting<string>(SETTING_KEYS.documensoApproverEmail, portId),
    readSetting<EoiSendMode>(SETTING_KEYS.eoiSendMode, portId),
    readSetting<string>(SETTING_KEYS.embeddedSigningHost, portId),
    readSetting<string | number>(SETTING_KEYS.documensoContractTemplateId, portId),
    readSetting<string | number>(SETTING_KEYS.documensoReservationTemplateId, portId),
    readSetting<string>(SETTING_KEYS.documensoDeveloperLabel, portId),
    readSetting<string>(SETTING_KEYS.documensoApproverLabel, portId),
    readSetting<string>(SETTING_KEYS.documensoDeveloperUserId, portId),
    readSetting<string>(SETTING_KEYS.documensoApproverUserId, portId),
    readSetting<'PARALLEL' | 'SEQUENTIAL' | 'TEMPLATE_DEFAULT'>(
      SETTING_KEYS.documensoSigningOrder,
      portId,
    ),
    readSetting<string>(SETTING_KEYS.documensoRedirectUrl, portId),
    readSetting<string>(SETTING_KEYS.publicSiteUrl, portId),
  ]);

  // Defense-in-depth: normalize any persisted Documenso API URL override at
  // read time. Writes are normalized + URL-validated by upsertSettingSchema,
  // but values stored before that validation landed (or seeded directly) may
  // still carry a `/api/v1`|`/api/v2` suffix that would double-path requests
  // (documensoFetch appends `/api/vN/...` to this base). Strip it here too.
  const normalizedApiUrl =
    typeof apiUrl === 'string' && apiUrl.length > 0
      ? (normalizeDocumensoApiUrl(apiUrl) as string)
      : apiUrl;

  // Determine the resolution source for the two credentials. Used by
  // the documenso client to enrich 401/403 error messages so operators
  // can tell at a glance whether the failing key is per-port or env.
  type Source = 'port' | 'global' | 'env' | 'default' | 'none';
  const apiUrlSource: Source = normalizedApiUrl ? 'port' : env.DOCUMENSO_API_URL ? 'env' : 'none';
  const apiKeySource: Source = apiKey ? 'port' : env.DOCUMENSO_API_KEY ? 'env' : 'none';

  return {
    // Env values are now optional (admin is canonical). Empty / zero
    // defaults let consumers proceed and fail at the actual API call with
    // a clearer "not configured" error rather than crashing at type-check.
    apiUrl: normalizedApiUrl ?? env.DOCUMENSO_API_URL ?? '',
    apiKey: apiKey ?? env.DOCUMENSO_API_KEY ?? '',
    apiVersion: apiVersion ?? env.DOCUMENSO_API_VERSION,
    apiKeySource,
    apiUrlSource,
    eoiTemplateId: toIntOrNull(eoiTemplateId) ?? env.DOCUMENSO_TEMPLATE_ID_EOI ?? 0,
    clientRecipientId: toIntOrNull(clientRecipientId) ?? env.DOCUMENSO_CLIENT_RECIPIENT_ID ?? 0,
    developerRecipientId:
      toIntOrNull(developerRecipientId) ?? env.DOCUMENSO_DEVELOPER_RECIPIENT_ID ?? 0,
    approvalRecipientId:
      toIntOrNull(approvalRecipientId) ?? env.DOCUMENSO_APPROVAL_RECIPIENT_ID ?? 0,
    defaultPathway: defaultPathway ?? 'documenso-template',
    // Default to the local-fill method (clean render + address wrapping). Set
    // to 'documenso' per-port to fall back to Documenso's template AcroForm fill.
    eoiFillMethod: eoiFillMethod === 'documenso' ? 'documenso' : 'local',
    developerName: developerName ?? '',
    developerEmail: developerEmail ?? '',
    approverName: approverName ?? '',
    approverEmail: approverEmail ?? '',
    sendMode: sendMode ?? 'manual',
    embeddedSigningHost: embeddedSigningHost ?? null,
    contractTemplateId: toIntOrNull(contractTemplateId),
    reservationTemplateId: toIntOrNull(reservationTemplateId),
    developerLabel: developerLabel ?? 'Developer',
    approverLabel: approverLabel ?? 'Approver',
    developerUserId: developerUserId ?? null,
    approverUserId: approverUserId ?? null,
    // TEMPLATE_DEFAULT is the new tri-state default - it normalises to
    // `null` here so downstream consumers (documenso-payload, signing-
    // automation) skip passing signingOrder entirely and let the
    // template's own setting win. PARALLEL / SEQUENTIAL still force
    // the override.
    signingOrder:
      signingOrder === 'PARALLEL' || signingOrder === 'SEQUENTIAL' ? signingOrder : null,
    // Resolution chain: explicit Documenso override → port's marketing
    // site URL → null (Documenso falls back to its own default, which is
    // typically the configured APP_URL = the CRM login - not what we want
    // for signers). The marketing-site fallback means operators who set
    // public_site_url (most do) automatically get sensible signer landing.
    redirectUrl: redirectUrlOverride ?? publicSiteUrl ?? null,
  };
}

/**
 * List every (portId, webhookSecret) pair configured across the platform,
 * plus a wildcard-port entry for the global env secret. The Documenso
 * webhook receiver iterates the list with `timingSafeEqual` until it
 * finds a match, then dispatches with the resolved portId.
 *
 * `null` portId in the returned array means "matches but no port was
 * resolved" - the caller falls back to the legacy global path.
 */
export interface DocumensoSecretEntry {
  portId: string | null;
  secret: string;
}

export async function listDocumensoWebhookSecrets(): Promise<DocumensoSecretEntry[]> {
  const { db } = await import('@/lib/db');
  const { systemSettings } = await import('@/lib/db/schema/system');
  const { decrypt } = await import('@/lib/utils/encryption');
  const { eq } = await import('drizzle-orm');
  const rows = await db
    .select({ portId: systemSettings.portId, value: systemSettings.value })
    .from(systemSettings)
    .where(eq(systemSettings.key, SETTING_KEYS.documensoWebhookSecret));

  const out: DocumensoSecretEntry[] = [];
  for (const row of rows) {
    if (!row.portId) continue;
    let secret: string | null = null;
    if (typeof row.value === 'string') {
      // Legacy plaintext rows.
      secret = row.value;
    } else if (
      typeof row.value === 'object' &&
      row.value !== null &&
      'iv' in row.value &&
      'tag' in row.value &&
      'data' in row.value
    ) {
      // Encrypted envelope written by the registry-aware resolver.
      try {
        secret = decrypt(JSON.stringify(row.value));
      } catch {
        // Decryption failure (corrupt envelope, key mismatch) - skip the
        // entry so a stale row doesn't crash the entire receiver loop.
        secret = null;
      }
    }
    if (!secret) continue;
    out.push({ portId: row.portId, secret });
  }

  // Append the global env secret as a fallback ONLY when it's a real,
  // non-empty value. An empty env secret would otherwise match an empty
  // X-Documenso-Secret header (verifyDocumensoSecret guards this too,
  // but skipping the entry here keeps the matched-secret loop honest).
  if (env.DOCUMENSO_WEBHOOK_SECRET) {
    out.push({ portId: null, secret: env.DOCUMENSO_WEBHOOK_SECRET });
  }
  return out;
}

// ─── Branding ───────────────────────────────────────────────────────────────

/**
 * NOT YET WIRED end-to-end. The `/admin/branding` page persists these
 * settings to system_settings, but the email templates in
 * `src/lib/email/templates/` and the `<BrandedAuthShell>` component in
 * `src/components/shared/branded-auth-shell.tsx` still hardcode the
 * `s3.portnimara.com` logo URL and the Port Nimara color palette. A
 * second port wired into this CRM will see Port Nimara branding in
 * every transactional email until those consumers call
 * `getPortBrandingConfig(portId)`. Tracked as audit finding R2-H15.
 *
 * To wire fully:
 *   1. Take `branding` config as a server-side prop into
 *      `<BrandedAuthShell>` (pass it from the page server component).
 *   2. Refactor the email shell helper in each `templates/*.ts` module
 *      to take `headerHtml` / `footerHtml` / `primaryColor` instead of
 *      the inline constants.
 *   3. In each sender, call `getPortBrandingConfig(portId)` and thread
 *      the branding values into the template call.
 */
export interface PortBrandingConfig {
  logoUrl: string | null;
  emailBackgroundUrl: string | null;
  primaryColor: string;
  appName: string;
  emailHeaderHtml: string | null;
  emailFooterHtml: string | null;
}

const DEFAULT_BRANDING: PortBrandingConfig = {
  logoUrl: null,
  emailBackgroundUrl: null,
  primaryColor: '#1e293b',
  appName: 'Port Nimara CRM',
  emailHeaderHtml: null,
  emailFooterHtml: null,
};

export async function getPortBrandingConfig(portId: string): Promise<PortBrandingConfig> {
  const [logoUrl, emailBackgroundUrl, primaryColor, appName, emailHeaderHtml, emailFooterHtml] =
    await Promise.all([
      readSetting<string>(SETTING_KEYS.brandingLogoUrl, portId),
      readSetting<string>(SETTING_KEYS.brandingEmailBackgroundUrl, portId),
      readSetting<string>(SETTING_KEYS.brandingPrimaryColor, portId),
      readSetting<string>(SETTING_KEYS.brandingAppName, portId),
      readSetting<string>(SETTING_KEYS.brandingEmailHeaderHtml, portId),
      readSetting<string>(SETTING_KEYS.brandingEmailFooterHtml, portId),
    ]);

  return {
    // Branding URLs that bake a localhost/LAN host (uploaded while running
    // on the dev's Mac) don't resolve from any other device. Normalize
    // here so in-app consumers get a path-only URL the browser resolves
    // against the current origin. Email surfaces re-absolutize via
    // `absolutizeBrandingUrl()` because mail clients have no app origin.
    logoUrl: normalizeBrandingUrl(logoUrl) ?? DEFAULT_BRANDING.logoUrl,
    emailBackgroundUrl:
      normalizeBrandingUrl(emailBackgroundUrl) ?? DEFAULT_BRANDING.emailBackgroundUrl,
    primaryColor: primaryColor ?? DEFAULT_BRANDING.primaryColor,
    appName: appName ?? DEFAULT_BRANDING.appName,
    emailHeaderHtml: emailHeaderHtml ?? DEFAULT_BRANDING.emailHeaderHtml,
    emailFooterHtml: emailFooterHtml ?? DEFAULT_BRANDING.emailFooterHtml,
  };
}

// ─── Reminders ──────────────────────────────────────────────────────────────

export interface PortReminderConfig {
  defaultDays: number;
  defaultEnabled: boolean;
  digestEnabled: boolean;
  digestTime: string; // 'HH:MM'
  digestTimezone: string;
}

const DEFAULT_REMINDER: PortReminderConfig = {
  defaultDays: 7,
  defaultEnabled: false,
  digestEnabled: false,
  digestTime: '09:00',
  digestTimezone: 'Europe/Warsaw',
};

/**
 * Port-level default currency for newly-created berths. Per-berth
 * `priceCurrency` overrides this when set. Defaults to USD because
 * 95% of marinas in the rollout target are USD-denominated.
 */
export async function getPortBerthsDefaultCurrency(portId: string): Promise<string> {
  const value = await readSetting<string>(SETTING_KEYS.berthsDefaultCurrency, portId);
  return (value ?? 'USD').trim().toUpperCase() || 'USD';
}

export async function getPortReminderConfig(portId: string): Promise<PortReminderConfig> {
  const [defaultDays, defaultEnabled, digestEnabled, digestTime, digestTimezone] =
    await Promise.all([
      readSetting<number>(SETTING_KEYS.reminderDefaultDays, portId),
      readSetting<boolean>(SETTING_KEYS.reminderDefaultEnabled, portId),
      readSetting<boolean>(SETTING_KEYS.reminderDigestEnabled, portId),
      readSetting<string>(SETTING_KEYS.reminderDigestTime, portId),
      readSetting<string>(SETTING_KEYS.reminderDigestTimezone, portId),
    ]);

  return {
    defaultDays: defaultDays ?? DEFAULT_REMINDER.defaultDays,
    defaultEnabled: defaultEnabled ?? DEFAULT_REMINDER.defaultEnabled,
    digestEnabled: digestEnabled ?? DEFAULT_REMINDER.digestEnabled,
    digestTime: digestTime ?? DEFAULT_REMINDER.digestTime,
    digestTimezone: digestTimezone ?? DEFAULT_REMINDER.digestTimezone,
  };
}
