import {
  useCallback,
  useLayoutEffect,
  useState,
  type ComponentType,
  type ReactNode,
} from 'react'
import { createPortal } from 'react-dom'
import {
  Box,
  Dialog,
  DialogContent,
  DialogTitle,
  IconButton,
  Typography,
  type SvgIconProps,
} from '@mui/material'
import { Fullscreen as FullscreenIcon } from '@mui/icons-material'
import { FullscreenExit as FullscreenExitIcon } from '@mui/icons-material'
import { Close as CloseIcon } from '@mui/icons-material'
import { Tooltip } from '../../../components'
import { getWidgetStore, useWidget, useWidgetId } from '../../stores'
import { DEFAULT_FULLSCREEN_LABELS, type FullScreenLabels } from './labels'
import type { FullScreenWidgetState } from './types'
import { styles } from './style'

export interface FullScreenTriggerProps {
  labels?: Partial<FullScreenLabels>
  enterIcon?: ComponentType<SvgIconProps>
  exitIcon?: ComponentType<SvgIconProps>
  iconProps?: SvgIconProps
}

export interface FullScreenSlotProps {
  /**
   * Render-prop body. Receives the live `isFullScreen` flag so the consumer
   * can branch per-mode props (e.g., `Widget.Toolbox visibleCount`) without
   * duplicating the JSX.
   */
  children: (isFullScreen: boolean) => ReactNode
  /** Header text rendered above the body when the modal is open. */
  title?: ReactNode
  labels?: Partial<FullScreenLabels>
  closeIcon?: ComponentType<SvgIconProps>
  iconProps?: SvgIconProps
}

/**
 * Toggle button placed inside `<Widget.Actions>`. Reads / writes
 * {@link FullScreenWidgetState.isFullScreen} on the per-widget store. The
 * matching modal is rendered by {@link FullScreenSlot} (typically inside
 * `<Widget.Content>`).
 */
function FullScreenTrigger({
  labels,
  enterIcon: EnterIcon = FullscreenIcon,
  exitIcon: ExitIcon = FullscreenExitIcon,
  iconProps,
}: FullScreenTriggerProps) {
  const id = useWidgetId()
  const _labels = { ...DEFAULT_FULLSCREEN_LABELS, ...labels }
  const open = useWidget<boolean, FullScreenWidgetState>(
    id,
    (s) => s.isFullScreen ?? false,
  )

  const handleToggle = useCallback(() => {
    getWidgetStore(id).setState({
      isFullScreen: !open,
    } as Partial<FullScreenWidgetState>)
  }, [id, open])

  const triggerLabel = open ? _labels.close : _labels.open
  const TriggerIcon = open ? ExitIcon : EnterIcon

  return (
    <Tooltip title={triggerLabel}>
      <IconButton
        size='small'
        aria-label={triggerLabel}
        aria-pressed={open}
        onClick={handleToggle}
        className={open ? 'active' : undefined}
        sx={{ ...styles.trigger, ...(open && styles.triggerActive) }}
      >
        <TriggerIcon fontSize='small' {...iconProps} />
      </IconButton>
    </Tooltip>
  )
}

/**
 * Body wrapper. Renders its render-prop children once, via a single
 * `createPortal` call whose container is a stable `<div>` we hold as a ref.
 * That div is moved imperatively between an inline placeholder and the
 * `<DialogContent>` of an MUI `<Dialog keepMounted>`. Because both the
 * React parent (the portal call site) AND the portal container are
 * identical across the toggle, the subtree keeps its fiber identity and
 * ECharts (or any other heavy child) stays mounted while only its DOM
 * parent changes.
 */
function FullScreenSlot({
  children,
  title,
  labels,
  closeIcon: CloseSvg = CloseIcon,
  iconProps,
}: FullScreenSlotProps) {
  const id = useWidgetId()
  const _labels = { ...DEFAULT_FULLSCREEN_LABELS, ...labels }
  const open = useWidget<boolean, FullScreenWidgetState>(
    id,
    (s) => s.isFullScreen ?? false,
  )
  const [inlineEl, setInlineEl] = useState<HTMLElement | null>(null)
  const [dialogEl, setDialogEl] = useState<HTMLElement | null>(null)
  // Stable, never-changing portal container created once per slot. Held in
  // state (not a ref) so the React Compiler is happy reading it during
  // render — it's immutable from React's perspective, only its DOM parent
  // changes via `appendChild`. Switching `createPortal`'s `container`
  // argument makes React unmount and remount the children, defeating the
  // single-mount goal; keeping the container identical and moving the div
  // imperatively avoids that.
  const [portalDiv] = useState<HTMLDivElement | null>(() => {
    if (typeof document === 'undefined') return null
    const div = document.createElement('div')
    // `display: contents` keeps the wrapper transparent in flow so it
    // doesn't introduce a stray block when nested inside the inline slot.
    div.style.display = 'contents'
    return div
  })

  const handleClose = useCallback(() => {
    getWidgetStore(id).setState({
      isFullScreen: false,
    } as Partial<FullScreenWidgetState>)
  }, [id])

  // Reparent the portal div into the active host whenever `open` (or either
  // host) changes. `useLayoutEffect` runs synchronously after commit (after
  // refs have been written), so the dialog ref is already live when we read
  // it.
  //
  // Anti-blink: pin a `min-height` on the inline shell that's always live —
  // not just while fullscreen is open. We re-measure whenever the body is
  // inline (open=false) so the lock tracks data-driven height changes
  // (Formula adding rows, Table growing, ECharts re-init…) and survives
  // every fullscreen toggle. Briefly clearing minHeight before the measure
  // lets the lock shrink as well as grow; both operations are synchronous
  // within useLayoutEffect, so the browser never paints the intermediate
  // "no lock" frame.
  useLayoutEffect(() => {
    if (!portalDiv || !inlineEl) return
    const host = open ? dialogEl : inlineEl
    if (host && portalDiv.parentNode !== host) {
      host.appendChild(portalDiv)
    }
    if (!open) refreshInlineHeightLock(inlineEl)
  }, [open, inlineEl, dialogEl, portalDiv])

  const titleId = `widget-fullscreen-title-${id}`
  const rendered = children(open)

  return (
    <>
      <Box ref={setInlineEl} sx={styles.inlineSlot} />
      {portalDiv ? createPortal(rendered, portalDiv) : null}
      <Dialog
        open={open}
        onClose={handleClose}
        PaperProps={{ sx: styles.dialogPaper }}
        aria-labelledby={titleId}
        // Sits above the Toolbox overflow Paper (zIndex.tooltip = 1500),
        // so opening fullscreen never leaves a stray popover floating on top.
        sx={{ zIndex: (theme) => theme.zIndex.tooltip + 1 }}
      >
        <DialogTitle id={titleId} component='div' sx={styles.dialogTitle}>
          <Typography
            variant='subtitle1'
            component='span'
            sx={styles.titleText}
          >
            {title}
          </Typography>
          <Tooltip title={_labels.close}>
            <IconButton
              size='medium'
              aria-label={_labels.close}
              onClick={handleClose}
            >
              <CloseSvg fontSize='small' {...iconProps} />
            </IconButton>
          </Tooltip>
        </DialogTitle>
        <DialogContent ref={setDialogEl} sx={styles.dialogContent} />
      </Dialog>
    </>
  )
}

/**
 * Briefly clear the inline shell's `min-height`, measure its natural
 * height, then re-pin. Lives outside the component so the React Compiler
 * doesn't see the DOM mutation as an attempt to modify a `useState` value
 * — `el` here is just a parameter, not a tracked store value.
 */
function refreshInlineHeightLock(el: HTMLElement): void {
  el.style.minHeight = ''
  const h = el.getBoundingClientRect().height
  if (h > 0) el.style.minHeight = `${h}px`
}

/**
 * Namespace export. Consumers compose with:
 *
 * ```tsx
 * <Widget.Actions>
 *   <Widget.FullScreen.Trigger />
 * </Widget.Actions>
 * <Widget.Content>
 *   <Widget.FullScreen.Slot title={title}>
 *     {(isFullScreen) => <Body visibleCount={isFullScreen ? undefined : 2} />}
 *   </Widget.FullScreen.Slot>
 * </Widget.Content>
 * ```
 */
export const FullScreen = {
  Trigger: FullScreenTrigger,
  Slot: FullScreenSlot,
}
