import { useMemo, type ReactNode } from 'react'

/**
 * Лёгкий JSON viewer с token-based syntax highlight + diff-подсветкой
 * top-level ключей. Никаких внешних зависимостей: regex-проход по
 * `JSON.stringify(..., null, 2)`.
 *
 * <p>Syntax: ключи (aurora), строки (pink), числа (warn), true/false/null
 * (accent), пунктуация (mute).
 *
 * <p>Diff (опционально): передай diff — массив { kind, keys }. Строки
 * JSON где key в added/removed/changed получают bg-цвет (зелёный /
 * красный / жёлтый). Подсветка — top-level only (для глубокого diff'а
 * есть DraftDiffSummary).
 */
export type JsonDiff = {
  added?: Set<string> | readonly string[]
  removed?: Set<string> | readonly string[]
  changed?: Set<string> | readonly string[]
}

type JsonViewProps = {
  value: unknown
  diff?: JsonDiff
  className?: string
}

const TOKEN_RE =
  /("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(true|false|null)|([{}[\],])/g

const toSet = (v: Set<string> | readonly string[] | undefined): Set<string> =>
  v instanceof Set ? v : new Set(v ?? [])

const tokenize = (line: string): ReactNode[] => {
  const out: ReactNode[] = []
  let last = 0
  let i = 0
  for (const m of line.matchAll(TOKEN_RE)) {
    const idx = m.index ?? 0
    if (idx > last) out.push(line.slice(last, idx))
    const [whole, str, colon, num, lit, punct] = m
    if (str && colon) {
      out.push(
        <span key={`k${i}`} className="text-aurora">
          {str}
        </span>,
      )
      out.push(
        <span key={`c${i}`} className="text-mute">
          {colon}
        </span>,
      )
    } else if (str) {
      out.push(
        <span key={`s${i}`} className="text-pink">
          {str}
        </span>,
      )
    } else if (num) {
      out.push(
        <span key={`n${i}`} className="text-warn">
          {num}
        </span>,
      )
    } else if (lit) {
      out.push(
        <span key={`l${i}`} className="text-accent">
          {lit}
        </span>,
      )
    } else if (punct) {
      out.push(
        <span key={`p${i}`} className="text-mute">
          {punct}
        </span>,
      )
    } else {
      out.push(whole)
    }
    last = idx + whole.length
    i++
  }
  if (last < line.length) out.push(line.slice(last))
  return out
}

const LINE_KEY_RE = /^ {2}"((?:\\.|[^"\\])*)":/

const lineDiffClass = (
  line: string,
  added: Set<string>,
  removed: Set<string>,
  changed: Set<string>,
): string => {
  const m = LINE_KEY_RE.exec(line)
  if (!m) return ''
  const k = m[1]
  if (added.has(k)) return 'bg-aurora/15 -mx-2 px-2 border-l-2 border-aurora'
  if (removed.has(k)) return 'bg-danger/15 -mx-2 px-2 border-l-2 border-danger'
  if (changed.has(k)) return 'bg-warn/15 -mx-2 px-2 border-l-2 border-warn'
  return ''
}

export const JsonView = ({ value, diff, className }: JsonViewProps) => {
  const added = useMemo(() => toSet(diff?.added), [diff?.added])
  const removed = useMemo(() => toSet(diff?.removed), [diff?.removed])
  const changed = useMemo(() => toSet(diff?.changed), [diff?.changed])

  const lines = useMemo(() => {
    try {
      return JSON.stringify(value, null, 2).split('\n')
    } catch {
      return [String(value)]
    }
  }, [value])

  return (
    <pre
      className={
        'text-mono text-cell bg-line/20 rounded p-2 overflow-x-auto whitespace-pre ' +
        (className ?? '')
      }
    >
      {lines.map((line, i) => {
        const dc = lineDiffClass(line, added, removed, changed)
        return (
          <div key={i} className={dc}>
            {tokenize(line)}
          </div>
        )
      })}
    </pre>
  )
}
