/**
 * Server-side rendering of notification text for outbound delivery (Slack).
 *
 * The in-app bell renders notification keys client-side via next-intl; the Slack
 * sink has no i18n runtime, so this module mirrors the small set of strings it
 * needs and interpolates the simple `{name}` placeholders the catalog uses.
 *
 * `NOTIFICATIONS_I18N` MIRRORS the `notifications` namespace of
 * `services/platform/messages/{en,de,fr}.json`. It is kept honest by
 * `notification_messages.test.ts`, which deep-equals it against those files —
 * editing a notification string in messages/ without updating it here fails CI.
 * `WORKFLOW_I18N` holds Slack-only workflow-outcome strings that never appear in
 * the bell, so they live solely here.
 *
 * Rendering escapes interpolated values for Slack mrkdwn (so a user-controlled
 * email/error can't inject markup) while leaving the template's own `*bold*`
 * markup intact.
 */

export const SUPPORTED_NOTIFICATION_LOCALES = ['en', 'de', 'fr'] as const;
export type NotificationLocale =
  (typeof SUPPORTED_NOTIFICATION_LOCALES)[number];

function isSupportedLocale(value: string): value is NotificationLocale {
  return (SUPPORTED_NOTIFICATION_LOCALES as readonly string[]).includes(value);
}

type LocaleStrings = Record<string, string>;

export const NOTIFICATIONS_I18N: Record<NotificationLocale, LocaleStrings> = {
  en: {
    title: 'Notifications',
    ariaUnread: 'Unread',
    emptyCaughtUpTitle: "You're all caught up",
    emptyCaughtUpDescription: "We'll let you know when anything new comes in.",
    emptyAllTitle: 'No notifications yet',
    emptyAllDescription:
      'Alerts and system updates will appear here when they arrive.',
    markAsRead: 'Mark as read',
    markAllAsRead: 'Mark all as read',
    filterUnread: 'Unread',
    filterAll: 'All',
    loadMore: 'Load more',
    loading: 'Loading…',
    accountLocked: 'Account temporarily locked: {email}',
    lockoutDetails: '{consecutiveFailures} failed sign-in attempts from {ip}.',
    auditIntegrityFailed: 'Audit log integrity check failed',
    auditIntegrityFailedDetails:
      "We couldn't verify the integrity of this organization's audit log: {reason}. This can indicate tampering — please investigate as soon as possible.",
    dsarScheduled: 'Erasure request scheduled',
    dsarScheduledBody:
      'An erasure request was filed for a member of this organization. It runs in {coolingOffHours} hours — open the receipt to review or cancel it.',
    dsarApprovalNeeded: 'Erasure request awaiting your approval',
    dsarApprovalNeededBody:
      "An erasure request needs a second admin's approval before it can run.",
    dsarCancelled: 'Erasure request cancelled',
    dsarCancelledBody:
      "A pending erasure request was cancelled during its cooling-off window, so it won't run.",
    dsarScheduledByBody:
      '{actor} filed an erasure request for {subject}. It runs in {coolingOffHours} hours — open the receipt to review or cancel it.',
    dsarApprovalNeededByBody:
      "{actor} filed an erasure request for {subject}. It needs a second admin's approval before it can run.",
    dsarCancelledByBody:
      "{actor} cancelled the pending erasure request for {subject}, so it won't run.",
    dsarPolicyTightened: 'Data-erasure policy made stricter',
    dsarPolicyTightenedBody:
      "This organization's data-erasure (DSAR) policy was made stricter. It takes effect immediately.",
    dsarPolicyLoosenProposed: 'Data-erasure policy weakening proposed',
    dsarPolicyLoosenProposedBody:
      'A weaker data-erasure (DSAR) policy was proposed. Any admin can cancel it within the 24-hour grace window before it takes effect.',
    dsarPolicyLoosenApplied: 'Data-erasure policy weakened',
    dsarPolicyLoosenAppliedBody:
      'A weaker data-erasure (DSAR) policy took effect after the 24-hour grace window.',
    dsarPolicyLoosenCancelled: 'Pending data-erasure policy change cancelled',
    dsarPolicyLoosenCancelledBody:
      'A pending data-erasure (DSAR) policy change was cancelled before it took effect.',
  },
  de: {
    title: 'Benachrichtigungen',
    ariaUnread: 'Ungelesen',
    emptyCaughtUpTitle: 'Alles erledigt',
    emptyCaughtUpDescription: 'Wir melden uns, sobald etwas Neues eintrifft.',
    emptyAllTitle: 'Noch keine Benachrichtigungen',
    emptyAllDescription:
      'Hinweise und System-Updates erscheinen hier, sobald sie eintreffen.',
    markAsRead: 'Als gelesen markieren',
    markAllAsRead: 'Alle als gelesen markieren',
    filterUnread: 'Ungelesen',
    filterAll: 'Alle',
    loadMore: 'Mehr laden',
    loading: 'Lädt…',
    accountLocked: 'Konto vorübergehend gesperrt: {email}',
    lockoutDetails:
      '{consecutiveFailures} fehlgeschlagene Anmeldeversuche von {ip}.',
    auditIntegrityFailed: 'Integritätsprüfung des Audit-Logs fehlgeschlagen',
    auditIntegrityFailedDetails:
      'Wir konnten die Integrität des Audit-Logs dieser Organisation nicht bestätigen: {reason}. Das kann auf Manipulation hindeuten — untersuche das so schnell wie möglich.',
    dsarScheduled: 'Löschungsanfrage geplant',
    dsarScheduledBody:
      'Eine Löschungsanfrage wurde für ein Mitglied dieser Organisation eingereicht. Sie wird in {coolingOffHours} Stunden ausgeführt — öffne den Beleg, um sie zu prüfen oder abzubrechen.',
    dsarApprovalNeeded: 'Löschungsanfrage wartet auf deine Freigabe',
    dsarApprovalNeededBody:
      'Eine Löschungsanfrage benötigt die Freigabe eines zweiten Admins, bevor sie ausgeführt werden kann.',
    dsarCancelled: 'Löschungsanfrage abgebrochen',
    dsarCancelledBody:
      'Eine offene Löschungsanfrage wurde während der Karenzzeit abgebrochen und läuft daher nicht.',
    dsarScheduledByBody:
      '{actor} hat eine Löschungsanfrage für {subject} eingereicht. Sie wird in {coolingOffHours} Stunden ausgeführt — öffne den Beleg, um sie zu prüfen oder abzubrechen.',
    dsarApprovalNeededByBody:
      '{actor} hat eine Löschungsanfrage für {subject} eingereicht. Sie benötigt die Freigabe eines zweiten Admins, bevor sie ausgeführt werden kann.',
    dsarCancelledByBody:
      '{actor} hat die offene Löschungsanfrage für {subject} abgebrochen; sie läuft daher nicht.',
    dsarPolicyTightened: 'Löschrichtlinie verschärft',
    dsarPolicyTightenedBody:
      'Die Löschrichtlinie (DSAR) dieser Organisation wurde verschärft. Sie wird sofort wirksam.',
    dsarPolicyLoosenProposed: 'Lockerung der Löschrichtlinie vorgeschlagen',
    dsarPolicyLoosenProposedBody:
      'Eine schwächere Löschrichtlinie (DSAR) wurde vorgeschlagen. Jeder Admin kann sie innerhalb des 24-Stunden-Karenzfensters abbrechen, bevor sie wirksam wird.',
    dsarPolicyLoosenApplied: 'Löschrichtlinie gelockert',
    dsarPolicyLoosenAppliedBody:
      'Eine schwächere Löschrichtlinie (DSAR) wurde nach dem 24-Stunden-Karenzfenster wirksam.',
    dsarPolicyLoosenCancelled:
      'Vorgemerkte Änderung der Löschrichtlinie abgebrochen',
    dsarPolicyLoosenCancelledBody:
      'Eine vorgemerkte Änderung der Löschrichtlinie (DSAR) wurde abgebrochen, bevor sie wirksam wurde.',
  },
  fr: {
    title: 'Notifications',
    ariaUnread: 'Non lu',
    emptyCaughtUpTitle: 'Tout est à jour',
    emptyCaughtUpDescription:
      "Nous vous préviendrons dès qu'il y aura du nouveau.",
    emptyAllTitle: "Aucune notification pour l'instant",
    emptyAllDescription:
      "Les alertes et mises à jour système apparaîtront ici à mesure qu'elles arriveront.",
    markAsRead: 'Marquer comme lu',
    markAllAsRead: 'Tout marquer comme lu',
    filterUnread: 'Non lus',
    filterAll: 'Tous',
    loadMore: 'Charger plus',
    loading: 'Chargement…',
    accountLocked: 'Compte temporairement verrouillé : {email}',
    lockoutDetails:
      '{consecutiveFailures} tentatives de connexion échouées depuis {ip}.',
    auditIntegrityFailed: "Échec du contrôle d'intégrité du journal d'audit",
    auditIntegrityFailedDetails:
      "Nous n'avons pas pu vérifier l'intégrité du journal d'audit de cette organisation : {reason}. Cela peut indiquer une altération — examine la situation dès que possible.",
    dsarScheduled: "Demande d'effacement planifiée",
    dsarScheduledBody:
      "Une demande d'effacement a été déposée concernant un membre de cette organisation. Elle s'exécute dans {coolingOffHours} heures — ouvre le reçu pour la consulter ou l'annuler.",
    dsarApprovalNeeded: "Demande d'effacement en attente de ton approbation",
    dsarApprovalNeededBody:
      "Une demande d'effacement nécessite l'approbation d'un second admin avant de pouvoir s'exécuter.",
    dsarCancelled: "Demande d'effacement annulée",
    dsarCancelledBody:
      "Une demande d'effacement en attente a été annulée pendant son délai de réflexion ; elle ne s'exécutera pas.",
    dsarScheduledByBody:
      "{actor} a déposé une demande d'effacement concernant {subject}. Elle s'exécute dans {coolingOffHours} heures — ouvre le reçu pour la consulter ou l'annuler.",
    dsarApprovalNeededByBody:
      "{actor} a déposé une demande d'effacement concernant {subject}. Elle nécessite l'approbation d'un second admin avant de pouvoir s'exécuter.",
    dsarCancelledByBody:
      "{actor} a annulé la demande d'effacement en attente concernant {subject} ; elle ne s'exécutera pas.",
    dsarPolicyTightened: "Politique d'effacement renforcée",
    dsarPolicyTightenedBody:
      "La politique d'effacement (DSAR) de cette organisation a été renforcée. Elle prend effet immédiatement.",
    dsarPolicyLoosenProposed:
      "Assouplissement proposé de la politique d'effacement",
    dsarPolicyLoosenProposedBody:
      "Un assouplissement de la politique d'effacement (DSAR) a été proposé. Tout admin peut l'annuler dans le délai de grâce de 24 heures avant qu'il ne prenne effet.",
    dsarPolicyLoosenApplied: "Politique d'effacement assouplie",
    dsarPolicyLoosenAppliedBody:
      "Une politique d'effacement (DSAR) assouplie a pris effet après le délai de grâce de 24 heures.",
    dsarPolicyLoosenCancelled:
      "Modification en attente de la politique d'effacement annulée",
    dsarPolicyLoosenCancelledBody:
      "Une modification en attente de la politique d'effacement (DSAR) a été annulée avant de prendre effet.",
  },
};

/** Slack-only workflow-outcome strings (never shown in the in-app bell). */
const WORKFLOW_I18N: Record<NotificationLocale, LocaleStrings> = {
  en: {
    workflowFailed: 'Workflow *{slug}* failed',
    workflowCompleted: 'Workflow *{slug}* completed',
  },
  de: {
    workflowFailed: 'Workflow *{slug}* fehlgeschlagen',
    workflowCompleted: 'Workflow *{slug}* abgeschlossen',
  },
  fr: {
    workflowFailed: 'Le workflow *{slug}* a échoué',
    workflowCompleted: 'Workflow *{slug}* terminé',
  },
};

/**
 * Escape the three characters Slack reserves in mrkdwn text, so an interpolated
 * user-controlled value cannot inject markup or break rendering. `&` first.
 */
export function escapeSlackText(value: string): string {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}

/**
 * Render a notification string in `locale` (falling back to English for an
 * unknown locale or a missing key), interpolating `{name}` placeholders from
 * `params`. Interpolated values are Slack-escaped; the template's own markup is
 * preserved. A missing key returns the key itself and logs — so a gap surfaces
 * in logs instead of silently shipping a raw key to Slack.
 */
export function renderNotificationMessage(
  locale: string,
  key: string,
  params?: Record<string, unknown>,
): string {
  const loc: NotificationLocale = isSupportedLocale(locale) ? locale : 'en';
  const template =
    NOTIFICATIONS_I18N[loc][key] ??
    WORKFLOW_I18N[loc][key] ??
    NOTIFICATIONS_I18N.en[key] ??
    WORKFLOW_I18N.en[key];
  if (template === undefined) {
    console.warn(`[notification_messages] no string for key "${key}"`);
    return key;
  }
  return template.replace(/\{(\w+)\}/g, (_, name: string) => {
    const value = params?.[name];
    if (value === undefined) return `{${name}}`;
    // Interpolation values are primitives in practice; stringify safely so an
    // unexpected object never renders as '[object Object]'.
    const str =
      typeof value === 'string'
        ? value
        : typeof value === 'number' || typeof value === 'boolean'
          ? String(value)
          : JSON.stringify(value);
    return escapeSlackText(str);
  });
}
