import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
import { Box, Link, Typography } from '@mui/material'
import type { Components } from 'react-markdown'
import { MarkdownContent } from '../markdown/markdown-content'
import { observeResize } from '../echart/shared-resize-observer'
import { DEFAULT_NOTE_LABELS, type NoteLabels } from './labels'
import { styles } from './style'

export interface NoteProps {
  /**
   * Maximum visible lines before clamping. Defaults to 3. Set to `0` to
   * disable clamping entirely — full content renders and the Show More/Less
   * toggle is never shown.
   */
  lineClamp?: number
  labels?: Partial<NoteLabels>
  /**
   * Renders verbatim inside the caption shell. For markdown source (a
   * string with `**bold**`, lists, links, etc.) reach for the
   * `<Note.Markdown>` subcomponent instead — it pipes the string through
   * the shared {@link MarkdownContent} engine with caption-safe element
   * mapping.
   */
  children: ReactNode
}

export interface NoteMarkdownProps {
  /** Markdown source. Rendered with caption typography via {@link MarkdownContent}. */
  content: string
  /** Maximum visible lines before clamping. Defaults to 3. */
  lineClamp?: number
  labels?: Partial<NoteLabels>
}

/** Block element used for every Markdown paragraph and demoted heading so
 *  Notes share a single caption look and never inject heading levels into
 *  the document outline. */
function CaptionBlock({ children }: { children?: ReactNode }) {
  return (
    <Typography variant='caption' component='p' color='inherit' sx={{ m: 0 }}>
      {children}
    </Typography>
  )
}

const NOTE_COMPONENTS: Components = {
  // Headings demoted to caption typography — a Note can't introduce a
  // heading into the page outline.
  h1: CaptionBlock,
  h2: CaptionBlock,
  h3: CaptionBlock,
  h4: CaptionBlock,
  h5: CaptionBlock,
  h6: CaptionBlock,
  p: CaptionBlock,
  // External-safe link defaults; inherit colour from the muted caption.
  a: ({ children, href }) => (
    <Link
      href={href}
      target='_blank'
      rel='noopener noreferrer'
      color='inherit'
      underline='always'
    >
      {children}
    </Link>
  ),
}

/**
 * Auxiliary slot for an optional explanatory note below a widget. Clamps to
 * `lineClamp` lines (default 3) and reveals a Show More/Less toggle when the
 * content overflows. Overflow detection re-runs on container resize via the
 * shared singleton ResizeObserver.
 *
 * Renders `children` verbatim through caption typography. For markdown
 * source, use the {@link NoteMarkdown} (`Note.Markdown`) subcomponent.
 */
function NoteBase({ lineClamp = 3, labels, children }: NoteProps) {
  return (
    <NoteShell lineClamp={lineClamp} labels={labels} dependency={children}>
      <Typography variant='caption' component='div' color='inherit'>
        {children}
      </Typography>
    </NoteShell>
  )
}

/**
 * Markdown variant of {@link Note}. Pipes the `content` string through the
 * shared {@link MarkdownContent} engine with caption-safe element mapping
 * (headings demoted, links open in new tabs, images/HTML stripped).
 */
function NoteMarkdown({ content, lineClamp = 3, labels }: NoteMarkdownProps) {
  return (
    <NoteShell lineClamp={lineClamp} labels={labels} dependency={content}>
      <MarkdownContent content={content} components={NOTE_COMPONENTS} />
    </NoteShell>
  )
}

// Public namespace export. Consumers reach the markdown variant via
// `<Widget.Note.Markdown>{content}</Widget.Note.Markdown>`.
export const Note = Object.assign(NoteBase, { Markdown: NoteMarkdown })

interface NoteShellProps {
  lineClamp: number
  labels: Partial<NoteLabels> | undefined
  /** Used to re-measure overflow when the underlying content changes. */
  dependency: unknown
  children: ReactNode
}

/**
 * Shared shell: caption box + clamping + Show More/Less toggle. Both the
 * verbatim `Note` and the markdown `Note.Markdown` reach for this so the
 * overflow + measurement behaviour stays identical across variants.
 */
function NoteShell({
  lineClamp,
  labels,
  dependency,
  children,
}: NoteShellProps) {
  const _labels = { ...DEFAULT_NOTE_LABELS, ...labels }
  const textRef = useRef<HTMLDivElement | null>(null)
  const [isOverflowing, setIsOverflowing] = useState(false)
  const [expanded, setExpanded] = useState(false)
  const clampingDisabled = lineClamp <= 0

  const measure = useCallback(() => {
    const node = textRef.current
    if (!node) return
    setIsOverflowing(node.scrollHeight > node.clientHeight + 1)
  }, [])

  useEffect(() => {
    if (clampingDisabled) return undefined
    const node = textRef.current
    if (!node) return undefined
    measure()
    return observeResize(node, measure)
  }, [measure, dependency, clampingDisabled])

  const toggle = useCallback(() => setExpanded((v) => !v), [])
  const clamped = !expanded && !clampingDisabled
  const lineClampSx = clamped
    ? { ...styles.clamped, WebkitLineClamp: lineClamp }
    : null

  return (
    <Box sx={styles.root}>
      <Box ref={textRef} sx={{ ...styles.text, ...(lineClampSx ?? {}) }}>
        {children}
      </Box>
      {/* Stay visible while the user is in the expanded state so they always
          have a way back. Without `|| expanded` the toggle would vanish the
          moment content unclamps (scrollHeight === clientHeight). Matches v1. */}
      {!clampingDisabled && (isOverflowing || expanded) && (
        <Link
          component='button'
          variant='caption'
          underline='none'
          onClick={toggle}
          sx={styles.toggle}
        >
          {expanded ? _labels.showLess : _labels.showMore}
        </Link>
      )}
    </Box>
  )
}
