import { Component, useCallback, type ReactNode } from 'react'
import { Alert, AlertTitle, Box, Typography } from '@mui/material'
import { setCaptureEl, useWidgetId, useWidgetShallow } from '../stores'
import { styles } from './style'
import { DEFAULT_STATE_LABELS, type StateLabels } from './labels'

export interface StateProps {
  skeleton: ReactNode
  children: ReactNode
  /** Override the empty-state predicate. Receives `rawData` (pre-pipeline). */
  isEmpty?: (rawData: unknown) => boolean
  /** Render an alternative error UI. When omitted, the default ErrorView is used. */
  fallback?: (error: Error) => ReactNode
  labels?: Partial<StateLabels>
}

interface StateSlice {
  isLoading: boolean
  isFetching: boolean
  error: unknown
  rawData: unknown
}

const sliceSelector = (s: {
  isLoading: boolean
  isFetching: boolean
  error: unknown
  rawData: unknown
}): StateSlice => ({
  isLoading: s.isLoading,
  isFetching: s.isFetching,
  error: s.error,
  rawData: s.rawData,
})

/**
 * Priority chain: loading → error → empty → content. Both consumer-supplied
 * errors (set via Provider's `error` prop) and transform-throw errors (set by
 * the pipeline middleware) flow through the same `error` field. Render-time
 * errors thrown by `children` are caught by the inlined error boundary and
 * routed through the same path.
 */
export function State({
  skeleton,
  children,
  isEmpty = defaultIsEmpty,
  fallback,
  labels,
}: StateProps) {
  const id = useWidgetId()
  const slice = useWidgetShallow(id, sliceSelector)
  const _labels = { ...DEFAULT_STATE_LABELS, ...labels }

  // Loading wins over everything else (matches v1 — hides errors during fetch).
  if (slice.isLoading) return <>{skeleton}</>

  // Store-side error (consumer-supplied or transform-throw via middleware).
  if (slice.error !== undefined && slice.error !== null) {
    const err = toError(slice.error)
    return fallback ? (
      <>{fallback(err)}</>
    ) : (
      <ErrorView error={err} labels={_labels} />
    )
  }

  // Empty (uses pre-pipeline rawData so a transform that filters everything
  // doesn't get misclassified as "no data from upstream").
  if (isEmpty(slice.rawData)) {
    return <EmptyView labels={_labels} />
  }

  return (
    <RenderErrorBoundary fallback={fallback} labels={_labels}>
      <CaptureBox id={id}>{children}</CaptureBox>
    </RenderErrorBoundary>
  )
}

/**
 * Wraps the success-path children in a stable flex column whose DOM ref is
 * registered with the per-widget capture map. Per-widget download configs
 * feed the wrapper element to `html2canvas` for PNG export. The callback
 * ref auto-clears (React calls it with `null`) when Widget.State leaves
 * the success branch or unmounts, so loading / error / empty states
 * correctly report "no capture target" via `getCaptureEl`.
 *
 * Composition convention: render interactive controls that should *not*
 * appear in the PNG (e.g. `<Widget.Searcher />`) as siblings of
 * `<Widget.State>` rather than children, so they sit outside this box.
 */
function CaptureBox({ id, children }: { id: string; children: ReactNode }) {
  const setRef = useCallback(
    (el: HTMLDivElement | null) => setCaptureEl(id, el),
    [id],
  )
  return (
    <Box ref={setRef} sx={styles.captureBox}>
      {children}
    </Box>
  )
}

function ErrorView({ error, labels }: { error: Error; labels: StateLabels }) {
  return (
    <Alert severity='error'>
      <AlertTitle>{labels.errorTitle}</AlertTitle>
      {labels.errorMessage(error)}
    </Alert>
  )
}

function EmptyView({ labels }: { labels: StateLabels }) {
  return (
    <Box sx={styles.emptyRoot}>
      <Typography variant='body2' color='text.primary'>
        {labels.emptyTitle}
      </Typography>
      <Typography variant='caption' color='text.secondary'>
        {labels.emptyDescription}
      </Typography>
    </Box>
  )
}

interface BoundaryProps {
  fallback?: (error: Error) => ReactNode
  labels: StateLabels
  children: ReactNode
}

interface BoundaryState {
  error: Error | null
}

class RenderErrorBoundary extends Component<BoundaryProps, BoundaryState> {
  override state: BoundaryState = { error: null }

  static getDerivedStateFromError(error: Error): BoundaryState {
    return { error }
  }

  override componentDidCatch(error: Error): void {
    // eslint-disable-next-line no-console
    console.error('[widgets-v2] Render error in <Widget.State> child:', error)
  }

  override render(): ReactNode {
    const { error } = this.state
    if (error !== null) {
      const { fallback, labels } = this.props
      return fallback ? (
        fallback(error)
      ) : (
        <ErrorView error={error} labels={labels} />
      )
    }
    return this.props.children
  }
}

function defaultIsEmpty(rawData: unknown): boolean {
  if (rawData == null) return true
  if (Array.isArray(rawData)) {
    if (rawData.length === 0) return true
    if (rawData.every((item) => Array.isArray(item) && item.length === 0)) {
      return true
    }
    return false
  }
  if (typeof rawData === 'object' && Object.keys(rawData).length === 0) {
    return true
  }
  return false
}

function toError(value: unknown): Error {
  if (value instanceof Error) return value
  if (typeof value === 'string') return new Error(value)
  if (value && typeof value === 'object' && 'message' in value) {
    return new Error(String((value as { message: unknown }).message))
  }
  return new Error('Unknown error')
}
