const ESC = /\x1b\[([0-9;]*)m/g

const STYLES: Record<number, string> = {
  1: 'font-weight:bold',
  2: 'opacity:0.7',
  30: 'color:#666',
  31: 'color:#f87171',
  32: 'color:#4ade80',
  33: 'color:#facc15',
  34: 'color:#60a5fa',
  35: 'color:#c084fc',
  36: 'color:#22d3ee',
  37: 'color:#e5e5e5',
  90: 'color:#999',
  91: 'color:#fca5a5',
  92: 'color:#86efac',
  93: 'color:#fde047',
  94: 'color:#93c5fd',
  95: 'color:#d8b4fe',
  96: 'color:#67e8f9',
  97: 'color:#fff',
}

const escape = (s: string) =>
  s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')

export const stripAnsi = (s: string) =>
  s.replace(/\x1b\[[0-9;]*m/g, '')

export function ansiToHtml(text: string): string {
  if (!text.includes('\x1b')) return escape(text)

  ESC.lastIndex = 0
  let result = ''
  let last = 0
  let open = false
  const styles: string[] = []
  let match: RegExpExecArray | null

  while ((match = ESC.exec(text)) !== null) {
    result += escape(text.slice(last, match.index))
    last = match.index + match[0].length

    const codes = match[1] ? match[1].split(';').map(Number) : [0]

    for (const code of codes) {
      if (code === 0) {
        styles.length = 0
        if (open) { result += '</span>'; open = false }
      } else if (code === 39) {
        const filtered = styles.filter(s => !s.startsWith('color:'))
        styles.length = 0
        styles.push(...filtered)
        if (open) { result += '</span>'; open = false }
      } else if (STYLES[code]) {
        styles.push(STYLES[code])
      }
    }

    if (styles.length) {
      if (open) result += '</span>'
      result += `<span style="${styles.join(';')}">`
      open = true
    }
  }

  result += escape(text.slice(last))
  if (open) result += '</span>'

  return result
}
