const ESC = '\u001B'

function isC0Control(cp: number): boolean {
  // Allow tab; callers generally split on newline already.
  return cp <= 0x1f && cp !== 0x09 && cp !== 0x0a
}

function isC1Control(cp: number): boolean {
  return cp >= 0x80 && cp <= 0x9f
}

/**
 * Removes control characters that could be interpreted by a terminal emulator.
 *
 * This is used for rendering tool output (bash/read/etc.) where the content may be untrusted
 * (e.g. reading a file containing escape sequences).
 */
export function sanitizeTerminalText(input: string): string {
  const s = String(input ?? '')
  if (!s) return ''

  // Fast path: return the original string if it contains no dangerous control chars.
  for (let i = 0; i < s.length; i++) {
    const c = s.charCodeAt(i)
    if (c === 0x1b || c === 0x7f || isC1Control(c) || isC0Control(c)) {
      // Slow path: remove disallowed chars by slicing spans.
      const chunks: string[] = []
      let last = 0
      for (let j = i; j < s.length; j++) {
        const cj = s.charCodeAt(j)
        if (cj === 0x1b || cj === 0x7f || isC1Control(cj) || isC0Control(cj)) {
          if (last < j) chunks.push(s.slice(last, j))
          last = j + 1
        }
      }
      if (last < s.length) chunks.push(s.slice(last))
      return chunks.join('')
    }
  }

  return s
}

function appendSanitizedSpan(
  chunks: string[],
  s: string,
  start: number,
  end: number,
): void {
  if (start >= end) return

  // Fast path: span contains no disallowed control chars.
  for (let i = start; i < end; i++) {
    const c = s.charCodeAt(i)
    if (c === 0x7f || isC1Control(c) || isC0Control(c)) {
      let last = start
      for (let j = i; j < end; j++) {
        const cj = s.charCodeAt(j)
        if (cj === 0x7f || isC1Control(cj) || isC0Control(cj)) {
          if (last < j) chunks.push(s.slice(last, j))
          last = j + 1
        }
      }
      if (last < end) chunks.push(s.slice(last, end))
      return
    }
  }

  chunks.push(s.slice(start, end))
}

/**
 * Strips all terminal control sequences except SGR (`ESC[ ... m`), which are later interpreted
 * via `parseAnsiSgr` into safe style spans.
 */
export function sanitizeAnsiSgrOnly(input: string): string {
  const s = String(input ?? '')
  if (!s) return ''

  const firstEsc = s.indexOf(ESC)
  if (firstEsc === -1) {
    return sanitizeTerminalText(s)
  }

  const chunks: string[] = []

  let i = 0
  while (i < s.length) {
    const escIdx = s.indexOf(ESC, i)
    if (escIdx === -1) {
      appendSanitizedSpan(chunks, s, i, s.length)
      break
    }
    appendSanitizedSpan(chunks, s, i, escIdx)

    const next = s[escIdx + 1]
    if (next === '[') {
      // Keep only CSI ... m (SGR). Drop other CSI sequences.
      let j = escIdx + 2
      while (j < s.length) {
        const c = s.charCodeAt(j)
        if ((c >= 48 && c <= 57) || c === 59) {
          // 0-9 or ';'
          j++
          continue
        }
        break
      }
      if (j < s.length && s[j] === 'm') {
        chunks.push(s.slice(escIdx, j + 1))
        i = j + 1
        continue
      }

      // Skip until CSI final byte (0x40-0x7E), then skip it too.
      j = escIdx + 2
      while (j < s.length) {
        const c = s.charCodeAt(j)
        j++
        if (c >= 0x40 && c <= 0x7e) break
      }
      i = j
      continue
    }

    if (next === ']') {
      // OSC: skip until BEL or ST (ESC \).
      let j = escIdx + 2
      while (j < s.length) {
        const c = s.charCodeAt(j)
        if (c === 0x07) {
          // BEL
          j++
          break
        }
        if (s[j] === ESC && s[j + 1] === '\\') {
          j += 2
          break
        }
        j++
      }
      i = j
      continue
    }

    // Other ESC sequences: drop ESC and (best-effort) their introducer.
    i = escIdx + 1
    if (i < s.length) i += 1
  }

  return chunks.join('')
}
