export interface TableOfContentsItem {
  id: string
  level: number
  text: string
  children: TableOfContentsItem[]
}

type UnknownRecord = Record<string, unknown>

function isRecord(value: unknown): value is UnknownRecord {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function readString(record: UnknownRecord, key: string): string | null {
  const value = Reflect.get(record, key)
  return typeof value === 'string' ? value : null
}

function readChildren(record: UnknownRecord): unknown[] {
  const value = record.children
  return Array.isArray(value) ? value : []
}

function normalizeText(value: string): string {
  return value.replace(/\s+/g, ' ').trim()
}

function collectTextSegments(value: unknown, segments: string[]) {
  if (typeof value === 'string') {
    segments.push(value)
    return
  }

  if (Array.isArray(value)) {
    for (const item of value) collectTextSegments(item, segments)
    return
  }

  if (!isRecord(value)) return

  const text = readString(value, 'text')
  if (text !== null) segments.push(text)
  collectTextSegments(value.content, segments)
  collectTextSegments(value.children, segments)
}

function extractInlineText(content: unknown): string {
  const segments: string[] = []
  collectTextSegments(content, segments)
  return normalizeText(segments.join(' '))
}

function parseHeadingLevel(rawLevel: unknown): number | null {
  const level =
    typeof rawLevel === 'string'
      ? Number(rawLevel)
      : typeof rawLevel === 'number'
        ? rawLevel
        : Number.NaN
  if (!Number.isInteger(level)) return null
  if (level < 1 || level > 6) return null
  return level
}

function headingLevel(block: UnknownRecord): number | null {
  if (readString(block, 'type') !== 'heading') return null
  const props = isRecord(block.props) ? block.props : {}
  return parseHeadingLevel(props.level)
}

function buildHeadingItem(block: UnknownRecord): TableOfContentsItem | null {
  const id = readString(block, 'id')
  const level = headingLevel(block)
  if (!id || level === null) return null

  return {
    id,
    level,
    text: extractInlineText(block.content),
    children: [],
  }
}

function collectHeadingItems(blocks: unknown): TableOfContentsItem[] {
  if (!Array.isArray(blocks)) return []

  const items: TableOfContentsItem[] = []
  for (const block of blocks) {
    if (!isRecord(block)) continue

    const heading = buildHeadingItem(block)
    if (heading) items.push(heading)
    items.push(...collectHeadingItems(readChildren(block)))
  }
  return items
}

function appendHeadingItem(
  roots: TableOfContentsItem[],
  stack: TableOfContentsItem[],
  item: TableOfContentsItem,
) {
  while (stack.length > 0 && stack[stack.length - 1].level >= item.level) {
    stack.pop()
  }

  const parent = stack.at(-1)
  if (parent) {
    parent.children.push(item)
  } else {
    roots.push(item)
  }
  stack.push(item)
}

export function extractTableOfContents(blocks: unknown): TableOfContentsItem[] {
  const roots: TableOfContentsItem[] = []
  const stack: TableOfContentsItem[] = []

  for (const item of collectHeadingItems(blocks)) {
    appendHeadingItem(roots, stack, item)
  }
  return roots
}
