import "server-only";

import sanitizeHtml from "sanitize-html";

function escapeHtml(value: string) {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

const mailboxLinkPattern =
  /(^|[^@a-z0-9_.-])((?:https?:\/\/[^\s<>"']+|(?:www\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}(?::\d{2,5})?(?:\/[^\s<>"']*)?))/gi;
const mailboxLinkTrailingPunctuationPattern = /[.,!?;:)}\]]+$/;
const mailboxLinkBlockedTags = new Set(["a", "code", "pre", "style", "textarea"]);

function decodeMailboxLinkEntities(value: string) {
  return value
    .replace(/&amp;/gi, "&")
    .replace(/&#38;/gi, "&")
    .replace(/&#x26;/gi, "&");
}

function linkifyMailboxText(value: string) {
  return value.replace(
    mailboxLinkPattern,
    (match, prefix: string, candidate: string) => {
      const trailing = candidate.match(mailboxLinkTrailingPunctuationPattern)?.[0] ?? "";
      const visibleValue = trailing ? candidate.slice(0, -trailing.length) : candidate;

      if (!visibleValue) {
        return match;
      }

      const decodedValue = decodeMailboxLinkEntities(visibleValue);
      const href = /^https?:\/\//i.test(decodedValue)
        ? decodedValue
        : `https://${decodedValue}`;

      return `${prefix}<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer nofollow">${visibleValue}</a>${trailing}`;
    },
  );
}

export function linkifyMailboxHtml(value: string) {
  const tagPattern = /<\/?([a-z0-9]+)\b[^>]*>/gi;
  let blockedDepth = 0;
  let cursor = 0;
  let result = "";

  for (const match of value.matchAll(tagPattern)) {
    const index = match.index ?? 0;
    const tag = match[0];
    const tagName = (match[1] ?? "").toLowerCase();
    const isClosingTag = /^<\//.test(tag);
    const isSelfClosingTag = /\/>$/.test(tag);

    result +=
      blockedDepth === 0
        ? linkifyMailboxText(value.slice(cursor, index))
        : value.slice(cursor, index);

    if (mailboxLinkBlockedTags.has(tagName) && isClosingTag) {
      blockedDepth = Math.max(0, blockedDepth - 1);
    }

    result += tag;

    if (
      mailboxLinkBlockedTags.has(tagName) &&
      !isClosingTag &&
      !isSelfClosingTag
    ) {
      blockedDepth += 1;
    }

    cursor = index + tag.length;
  }

  result +=
    blockedDepth === 0
      ? linkifyMailboxText(value.slice(cursor))
      : value.slice(cursor);

  return result;
}

const spacingPattern =
  /^-?\d+(?:\.\d+)?(?:px|em|rem|pt|%|ex)?(?:\s+-?\d+(?:\.\d+)?(?:px|em|rem|pt|%|ex)?){0,3}$/i;
const sizePattern = /^(auto|\d+(?:\.\d+)?(?:px|em|rem|pt|%|vw|vh))$/i;
const sizeWithImportantPattern =
  /^(auto|\d+(?:\.\d+)?(?:px|em|rem|pt|%|vw|vh))(?:\s*!important)?$/i;
const colorWithImportantPattern =
  /^(#[0-9a-f]{3,8}|rgb(a)?\([^)]*\)|[a-z]+)(?:\s*!important)?$/i;
const spacingWithImportantPattern =
  /^-?\d+(?:\.\d+)?(?:px|em|rem|pt|%|ex)?(?:\s+-?\d+(?:\.\d+)?(?:px|em|rem|pt|%|ex)?){0,3}(?:\s*!important)?$/i;
const borderWithImportantPattern =
  /^(?:0|none|\d+(?:\.\d+)?px\s+(?:solid|dashed|dotted)\s+(?:#[0-9a-f]{3,8}|rgb(a)?\([^)]*\)|[a-z]+))(?:\s*!important)?$/i;
const borderRadiusPattern =
  /^\d+(?:\.\d+)?(?:px|em|rem|pt|%)?(?:\s+\d+(?:\.\d+)?(?:px|em|rem|pt|%)?){0,3}(?:\s*!important)?$/i;
const zeroOrSizedPattern =
  /^(?:0|auto|\d+(?:\.\d+)?(?:px|em|rem|pt|%|vw|vh))(?:\s*!important)?$/i;
const fontFamilyPattern = /^[\p{L}\p{N}\s"',._-]+(?:\s*!important)?$/iu;
const fontSizeWithImportantPattern =
  /^(?:0|\d+(?:\.\d+)?(?:px|em|rem|pt|%))(?:\s*!important)?$/i;
const letterSpacingPattern =
  /^(?:normal|-?\d+(?:\.\d+)?(?:px|em|rem|pt))(?:\s*!important)?$/i;

function preserveSafeEmailStyleTags(value: string) {
  return value.replace(/<style\b[^>]*>([\s\S]*?)<\/style>/gi, (_match, css: string) => {
    if (/@import|url\s*\(|expression\s*\(|javascript\s*:|behavior\s*:|-moz-binding/i.test(css)) {
      return "";
    }

    return `<style>${css.replace(/<\/?style\b[^>]*>/gi, "")}</style>`;
  });
}

function stripNonContentEmailMarkup(value: string) {
  return value
    .replace(/<(script|title)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, "")
    .replace(/<(?:script|title)\b[^>]*\/?\s*>/gi, "");
}

function transformLegacyFontTag(
  _tagName: string,
  attribs: sanitizeHtml.Attributes,
): sanitizeHtml.Tag {
  const color = attribs.color?.trim();
  const face = attribs.face?.trim();
  const size = attribs.size?.trim();
  const styles = attribs.style?.trim() ? [attribs.style.trim()] : [];
  const nextAttribs: sanitizeHtml.Attributes = {};

  if (color && colorWithImportantPattern.test(color)) {
    styles.push(`color:${color}`);
  }

  if (face && fontFamilyPattern.test(face)) {
    styles.push(`font-family:${face}`);
  }

  if (size) {
    const numericSize = /^([+-]?)([1-7])$/.exec(size);

    if (numericSize) {
      const baseSize = numericSize[1]
        ? Math.min(7, Math.max(1, 3 + Number(`${numericSize[1]}${numericSize[2]}`)))
        : Number(numericSize[2]);
      const legacySizeMap = ["", "10px", "13px", "16px", "18px", "24px", "32px", "48px"];
      styles.push(`font-size:${legacySizeMap[baseSize]}`);
    }
  }

  if (styles.length > 0) {
    nextAttribs.style = styles.join(";");
  }

  if (["auto", "ltr", "rtl"].includes(attribs.dir?.toLowerCase())) {
    nextAttribs.dir = attribs.dir.toLowerCase();
  }

  if (attribs.class) {
    nextAttribs.class = attribs.class;
  }

  if (attribs.id) {
    nextAttribs.id = attribs.id;
  }

  if (attribs.lang) {
    nextAttribs.lang = attribs.lang;
  }

  return {
    tagName: "span",
    attribs: nextAttribs,
  };
}

const MAIL_HTML_SANITIZE_OPTIONS: sanitizeHtml.IOptions = {
  allowVulnerableTags: true,
  allowedAttributes: {
    "*": ["class", "data-compose-layout", "data-official-mail-signature", "dir", "id", "lang", "style"],
    a: ["href", "name", "target", "rel"],
    img: ["src", "alt", "title", "width", "height", "border"],
    table: ["align", "border", "cellpadding", "cellspacing", "width"],
    td: ["align", "colspan", "rowspan", "valign", "width", "height"],
    th: ["align", "colspan", "rowspan", "valign", "width", "height"],
    tr: ["align", "valign", "width", "height"],
  },
  allowedClasses: {
    "*": [/^[a-z][a-z0-9_-]{0,127}$/i],
  },
  allowedSchemes: ["http", "https", "mailto"],
  allowedSchemesByTag: {
    img: ["cid", "data", "http", "https"],
  },
  allowedStyles: {
    "*": {
      background: [colorWithImportantPattern],
      "background-color": [colorWithImportantPattern],
      border: [borderWithImportantPattern],
      "border-bottom": [borderWithImportantPattern],
      "border-left": [borderWithImportantPattern],
      "border-right": [borderWithImportantPattern],
      "border-top": [borderWithImportantPattern],
      "border-collapse": [/^(collapse|separate)(?:\s*!important)?$/i],
      "border-radius": [borderRadiusPattern],
      "border-spacing": [spacingWithImportantPattern],
      color: [colorWithImportantPattern],
      display: [/^(none|block|inline|inline-block|flex|grid|table|table-row|table-cell)(?:\s*!important)?$/i],
      float: [/^(left|right|none)(?:\s*!important)?$/i],
      "font-family": [fontFamilyPattern],
      "font-size": [fontSizeWithImportantPattern],
      "font-style": [/^(normal|italic|oblique)(?:\s*!important)?$/i],
      "font-weight": [/^(normal|bold|[1-9]00)(?:\s*!important)?$/i],
      height: [sizeWithImportantPattern, sizePattern],
      "line-height": [/^(normal|\d+(?:\.\d+)?(?:px|em|rem|pt|%)?)(?:\s*!important)?$/i],
      "letter-spacing": [letterSpacingPattern],
      margin: [spacingWithImportantPattern, spacingPattern],
      "margin-bottom": [spacingWithImportantPattern, spacingPattern],
      "margin-left": [spacingWithImportantPattern, spacingPattern],
      "margin-right": [spacingWithImportantPattern, spacingPattern],
      "margin-top": [spacingWithImportantPattern, spacingPattern],
      "max-height": [zeroOrSizedPattern, sizePattern],
      "max-width": [zeroOrSizedPattern, sizePattern],
      "min-height": [zeroOrSizedPattern, sizePattern],
      "min-width": [zeroOrSizedPattern, sizePattern],
      "mso-hide": [/^(all|none)(?:\s*!important)?$/i],
      opacity: [/^(0|0?\.\d+|1)(?:\s*!important)?$/i],
      outline: [/^(?:none|0)(?:\s*!important)?$/i, borderWithImportantPattern],
      overflow: [/^(visible|hidden|scroll|auto)(?:\s*!important)?$/i],
      padding: [spacingWithImportantPattern, spacingPattern],
      "padding-bottom": [spacingWithImportantPattern, spacingPattern],
      "padding-left": [spacingWithImportantPattern, spacingPattern],
      "padding-right": [spacingWithImportantPattern, spacingPattern],
      "padding-top": [spacingWithImportantPattern, spacingPattern],
      "text-align": [/^(left|right|center|justify)(?:\s*!important)?$/i],
      "text-decoration": [/^[a-z\s-]+$/i],
      "vertical-align": [/^(top|middle|bottom|baseline)(?:\s*!important)?$/i],
      visibility: [/^(visible|hidden|collapse)(?:\s*!important)?$/i],
      "white-space": [/^(normal|nowrap|pre|pre-line|pre-wrap)$/i],
      width: [sizeWithImportantPattern, sizePattern],
    },
  },
  allowedTags: [
    ...sanitizeHtml.defaults.allowedTags,
    "blockquote",
    "br",
    "div",
    "h1",
    "h2",
    "h3",
    "h4",
    "h5",
    "h6",
    "hr",
    "img",
    "span",
    "style",
    "table",
    "tbody",
    "td",
    "tfoot",
    "th",
    "thead",
    "tr",
  ],
  disallowedTagsMode: "discard",
  transformTags: {
    a: (_tagName, attribs) => ({
      attribs: {
        ...attribs,
        rel: "noopener noreferrer nofollow",
        target: "_blank",
      },
      tagName: "a",
    }),
    font: transformLegacyFontTag,
  },
};

export function normalizeStoredMailboxHtml(value: string | null | undefined) {
  if (typeof value !== "string") {
    return null;
  }

  const normalized = value.trim();
  return normalized ? normalized : null;
}

export function plainTextToHtml(value: string) {
  const normalized = value.replace(/\r/g, "").trim();

  if (!normalized) {
    return "";
  }

  return normalized
    .split(/\n{2,}/)
    .map((block) => {
      const html = block
        .split("\n")
        .map((line) => {
          const escaped = escapeHtml(line).replace(/ {2}/g, " &nbsp;");
          return escaped || "<br>";
        })
        .join("<br>");

      return `<p>${html}</p>`;
    })
    .join("");
}

export function sanitizeMailboxHtml(
  value: string | null | undefined,
  fallbackText?: string | null,
) {
  const normalized = normalizeStoredMailboxHtml(value);

  if (!normalized) {
    return fallbackText?.trim() ? plainTextToHtml(fallbackText) : null;
  }

  const sanitized = sanitizeHtml(
    preserveSafeEmailStyleTags(stripNonContentEmailMarkup(normalized)),
    MAIL_HTML_SANITIZE_OPTIONS,
  ).trim();

  if (sanitized) {
    return sanitized;
  }

  return fallbackText?.trim() ? plainTextToHtml(fallbackText) : null;
}

function normalizeContentId(value: string) {
  const trimmed = value.trim().replace(/^<|>$/g, "");

  try {
    return decodeURIComponent(trimmed).toLowerCase();
  } catch {
    return trimmed.toLowerCase();
  }
}

export function rewriteMailboxCidSources(
  value: string,
  messageId: number,
  attachments: Array<{ contentId: string | null; index: number }>,
) {
  const attachmentIndexByContentId = new Map(
    attachments
      .filter(
        (attachment): attachment is { contentId: string; index: number } =>
          Boolean(attachment.contentId?.trim()),
      )
      .map(
        (attachment) =>
          [normalizeContentId(attachment.contentId), attachment.index] as const,
      ),
  );

  if (attachmentIndexByContentId.size === 0) {
    return value;
  }

  return value.replace(
    /(\bsrc\s*=\s*(["']))cid:([^"']+)\2/gi,
    (match, prefix: string, _quote: string, contentId: string) => {
      const attachmentIndex = attachmentIndexByContentId.get(
        normalizeContentId(contentId),
      );

      if (attachmentIndex === undefined) {
        return match;
      }

      return `${prefix}/api/mailbox/attachment?message=${messageId}&amp;index=${attachmentIndex}&amp;mode=inline${_quote}`;
    },
  );
}

export function buildMailboxHtmlDisplay(input: {
  attachments?: Array<{ contentId: string | null; index: number }>;
  bodyHtml: string | null | undefined;
  bodyText: string | null | undefined;
  messageId: number;
}) {
  const sanitized = sanitizeMailboxHtml(input.bodyHtml, input.bodyText);

  if (!sanitized) {
    return null;
  }

  return rewriteMailboxCidSources(
    linkifyMailboxHtml(sanitized),
    input.messageId,
    input.attachments ?? [],
  );
}
