const allowedTags = new Set([
  'A',
  'B',
  'BR',
  'DIV',
  'EM',
  'FONT',
  'H2',
  'H3',
  'H4',
  'I',
  'LI',
  'OL',
  'P',
  'SPAN',
  'STRONG',
  'U',
  'UL',
])

const allowedStyleProperties = new Set([
  'font-family',
  'font-size',
  'font-style',
  'font-weight',
  'text-align',
  'text-decoration',
])

const dangerousTags = new Set(['IFRAME', 'OBJECT', 'SCRIPT', 'STYLE', 'TEMPLATE'])

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

function hasHtmlMarkup(value: string) {
  return /<\/?[a-z][\s\S]*>/i.test(value)
}

function normalizePlainTextLines(value: string) {
  return value
    .replace(/\r\n?/g, '\n')
    .split('\n')
    .map((line) => line.trim())
    .join('\n')
    .replace(/\n{3,}/g, '\n\n')
    .trim()
}

function decodeBasicHtmlEntities(value: string) {
  return value
    .replace(/&nbsp;/gi, ' ')
    .replace(/&amp;/gi, '&')
    .replace(/&lt;/gi, '<')
    .replace(/&gt;/gi, '>')
    .replace(/&quot;/gi, '"')
    .replace(/&#39;|&#039;/gi, "'")
}

function htmlToPlainText(value: string) {
  const withBreakHints = value
    .replace(/<\s*br\s*\/?>/gi, '\n')
    .replace(/<\s*li\b[^>]*>/gi, '- ')
    .replace(/<\/\s*(p|div|li|ul|ol|h1|h2|h3|h4|h5|h6)\s*>/gi, '\n')

  if (typeof document === 'undefined') {
    return normalizePlainTextLines(
      decodeBasicHtmlEntities(withBreakHints.replace(/<[^>]*>/g, ' ')),
    )
  }

  const template = document.createElement('template')
  template.innerHTML = withBreakHints
  return normalizePlainTextLines(template.textContent ?? '')
}

function normalizePlainText(value: string) {
  return value
    .split(/\n{2,}/)
    .map((block) => block.trim())
    .filter(Boolean)
    .map((block) => `<p>${escapeHtml(block).replace(/\n/g, '<br>')}</p>`)
    .join('')
}

function isSafeStyleValue(property: string, value: string) {
  const normalizedValue = value.trim()
  if (!normalizedValue || /url\s*\(|expression\s*\(|javascript:/i.test(normalizedValue)) {
    return false
  }

  if (property === 'text-align') {
    return /^(left|center|right|justify)$/i.test(normalizedValue)
  }

  if (property === 'font-size') {
    return /^([1-4](\.\d)?rem|[1-3]?\dpx|[1-2]?\dpt|small|medium|large|x-large)$/i.test(normalizedValue)
  }

  if (property === 'font-family') {
    return /^[a-z0-9\s"',.-]+$/i.test(normalizedValue)
  }

  if (property === 'font-weight') {
    return /^(normal|bold|[1-9]00)$/i.test(normalizedValue)
  }

  if (property === 'font-style') {
    return /^(normal|italic)$/i.test(normalizedValue)
  }

  return /^(none|underline|line-through)$/i.test(normalizedValue)
}

function sanitizeStyle(style: string) {
  return style
    .split(';')
    .map((declaration) => declaration.trim())
    .filter(Boolean)
    .map((declaration) => {
      const [property, ...valueParts] = declaration.split(':')
      const normalizedProperty = property?.trim().toLowerCase()
      const value = valueParts.join(':').trim()

      if (!allowedStyleProperties.has(normalizedProperty) || !isSafeStyleValue(normalizedProperty, value)) {
        return ''
      }

      return `${normalizedProperty}: ${value}`
    })
    .filter(Boolean)
    .join('; ')
}

function unwrapElement(element: Element) {
  const parent = element.parentNode
  if (!parent) {
    return
  }

  while (element.firstChild) {
    parent.insertBefore(element.firstChild, element)
  }
  parent.removeChild(element)
}

function sanitizeElement(element: Element) {
  if (dangerousTags.has(element.tagName)) {
    element.remove()
    return
  }

  if (!allowedTags.has(element.tagName)) {
    unwrapElement(element)
    return
  }

  for (const attribute of Array.from(element.attributes)) {
    const name = attribute.name.toLowerCase()
    const value = attribute.value

    if (name === 'style') {
      const safeStyle = sanitizeStyle(value)
      if (safeStyle) {
        element.setAttribute('style', safeStyle)
      } else {
        element.removeAttribute(attribute.name)
      }
      continue
    }

    if (element.tagName === 'A' && name === 'href' && /^(https?:|mailto:)/i.test(value)) {
      element.setAttribute('href', value)
      element.setAttribute('rel', 'noopener noreferrer')
      continue
    }

    if (element.tagName === 'FONT' && ['face', 'size'].includes(name) && /^[a-z0-9\s"',.-]+$/i.test(value)) {
      continue
    }

    element.removeAttribute(attribute.name)
  }
}

export function sanitizePrivacyHtml(html: string) {
  if (typeof document === 'undefined') {
    return hasHtmlMarkup(html) ? escapeHtml(stripPrivacyHtml(html)) : normalizePlainText(html)
  }

  const template = document.createElement('template')
  template.innerHTML = html

  const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT)
  const nodes: Node[] = []
  while (walker.nextNode()) {
    nodes.push(walker.currentNode)
  }

  for (const node of nodes) {
    if (node.nodeType === Node.COMMENT_NODE) {
      node.parentNode?.removeChild(node)
      continue
    }

    sanitizeElement(node as Element)
  }

  return template.innerHTML.trim()
}

export function stripPrivacyHtml(value: string) {
  if (typeof document === 'undefined') {
    return value.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
  }

  const template = document.createElement('template')
  template.innerHTML = value
  return (template.textContent ?? '').replace(/\s+/g, ' ').trim()
}

export function privacyContentToHtml(value: string) {
  const trimmed = value.trim()
  if (!trimmed) {
    return ''
  }

  return hasHtmlMarkup(trimmed) ? sanitizePrivacyHtml(trimmed) : normalizePlainText(trimmed)
}

export function privacyContentForStorage(value: string) {
  return privacyContentToHtml(value)
}

export function plainTextContentFromStorage(value: string) {
  const trimmed = value.trim()
  if (!trimmed) {
    return ''
  }

  return hasHtmlMarkup(trimmed) ? htmlToPlainText(trimmed) : normalizePlainTextLines(trimmed)
}

export function plainTextContentForStorage(value: string) {
  return plainTextContentFromStorage(value)
}
