import React, {
  type ReactPortal,
  useCallback,
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
} from 'react'
import * as ReactDOM from 'react-dom'

import { cn } from '../../services/cn'
import { type ConfirmationPopoverContentProps } from '../ConfirmationPopover/ConfirmationPopoverContent'
import isClient from '../../services/isClient'
import PopoverHeader from '../PopoverHeader/PopoverHeader'
import Stepper from '../Stepper/Stepper'
import { useMediaQuery } from '../../hooks/responsiveHooks'
import { useWindowSize } from '../../hooks/useWindowSize/useWindowSize'
import { type StepperProps } from '../Stepper/Stepper'
import ScrollingContainer from '../ScrollingContainer/ScrollingContainer'
import BackgroundOverlay from '../BackgroundOverlay/BackgroundOverlay'
import { c } from '../../translations/LibraryTranslationService'
const { createPortal } = ReactDOM

type SideDrawerWithSize = SideDrawerBase & {
  /** Size of the drawer */
  size?: 'sm' | 'md' | 'lg'
  fullWidth?: never
}

type SideDrawerWithFullWidth = SideDrawerBase & {
  /** Optionally render `SideDrawer` in a full width view. */
  fullWidth?: boolean
  size?: never
}

type DetermineSideDrawerSize = SideDrawerWithSize | SideDrawerWithFullWidth

type SideDrawerWithText = DetermineSideDrawerSize & {
  /** The content to be rendered within the header. */
  headerContent: React.ReactNode
  logoUrl?: never
}

type SideDrawerWithLogo = DetermineSideDrawerSize & {
  headerContent?: never
  /** Optional logo to be passed in place of `headerContent` */
  logoUrl: string
}

export type SideDrawerBase = {
  /** The content to be rendered within the component. */
  children:
    | React.ReactNode
    | (({
        height,
        close,
      }: {
        height: number
        close: () => void
      }) => React.JSX.Element)
  /** The open state of `SideDrawer`. */
  isOpen: boolean
  /** Callout function to close the `SideDrawer`. */
  closeCallout: React.MouseEventHandler<HTMLButtonElement> &
    ((isOutsideClick?: boolean) => void)
  /** Optional className for the `SideDrawer` container */
  containerClassName?: string
  /** Optional className for the content */
  contentClassName?: string
  /** The content to be rendered within the footer. */
  footerContent?:
    | React.ReactNode
    | (({ close }: { close: () => void }) => React.JSX.Element)
  /** Optionally render `SideDrawer` in a mobile view only. */
  onlyMobile?: boolean
  /** Optionally add the `Stepper` component. */
  stepperProps?: StepperProps
  /** Optionally remove the gradient from the `SideDrawer`. This is useful when the background of
   *  the content is something other than white. */
  noGradient?: boolean
  /** Optionally remove content padding */
  noContentPadding?: boolean
  /** Optionally remove footer padding */
  noFooterPadding?: boolean
  /** Optionally add a layer position to the drawer to allow layered drawers. The number here will
   *  be the position in the stack of `SideDrawer` layers if there are multiple SideDrawers that
   *  need to be displayed. NOTE: Omit this prop if there is only 1 `SideDrawer` layer. This is
   *  important for styling. */
  layerPosition?: number
  /** Optionally use the confirmation header style. Note: this only applies to the header of the drawer and for styling the mobile version of the `Modal` component. */
  confirmation?: ConfirmationPopoverContentProps
  /** Optional prop to force the background overlay to be shown.
   *  By default, the background overlay is displayed. However, it is
   *  turned off for layered drawers. Setting this prop ensures that
   *  the overlay is always shown. */
  showBackgroundOverlay?: boolean
  /** Optional prop to add a test id for QA testing */
  qaTestId?: string
}

export type SideDrawerProps = SideDrawerWithText | SideDrawerWithLogo

const sizeVariants = {
  sm: 'max-w-[448px]', // 448px width defined in the Design System
  md: 'max-w-[600px]', // 600px width defined in the Design System
  lg: 'max-w-[768px]', // 768px width defined in the Design System
  fullWidth: 'max-w-screen',
} as const

export function SideDrawer({
  children,
  closeCallout,
  containerClassName = '',
  contentClassName = '',
  footerContent,
  headerContent,
  logoUrl,
  isOpen,
  onlyMobile,
  size = 'sm',
  fullWidth,
  stepperProps,
  noGradient,
  noContentPadding,
  noFooterPadding,
  layerPosition,
  confirmation,
  qaTestId = 'side-drawer',
  showBackgroundOverlay = false,
}: SideDrawerProps): React.JSX.Element {
  const zIndex = 1100 + (layerPosition ?? 0)

  if (typeof isOpen !== 'boolean') {
    throw new Error(c('errorSideDrawerIsOpen'))
  }
  if (!closeCallout || typeof closeCallout !== 'function') {
    throw new Error(
      !closeCallout
        ? c('errorSideDrawerCloseCalloutRequired')
        : c('errorSideDrawerCloseCalloutMustBeFunction'),
    )
  }

  const screenLargerThanMd = useMediaQuery({ type: 'min', breakpoint: 'md' })
  const hide = screenLargerThanMd && onlyMobile
  const sectionRef = useRef<HTMLDivElement>(null)
  const sectionContentRef = useRef<HTMLDivElement>(null)
  const [contentHeight, setContentHeight] = useState<number | string>('auto')
  const [footerRef, setFooterRef] = useState<HTMLDivElement | null>(null)
  const openDrawer = useRef(false)
  const windowHeight = useWindowSize(true)?.height
  const [isTransitioning, setIsTransitioning] = useState(false)
  const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined)

  useEffect(() => {
    setTimeout(() => (openDrawer.current = isOpen)) // wait for SideDrawer to be rendered on screen
  }, [isOpen])

  useLayoutEffect(() => {
    if (sectionRef.current) {
      const sectionEl = sectionRef.current

      const transitionStart = (evt: TransitionEvent) => {
        // we need to make sure we only pay attention to transition events that the section element fires and nothing from the children
        if (evt.target === sectionEl) {
          sectionEl.dataset.drawerState = 'transitioning'
        }
      }

      const transitionEnd = (evt?: TransitionEvent) => {
        // evt could be undefined, in the case below where we call it manually.
        // otherwise, on all events, we need to make sure we only pay attention to transition events that the section element fires, and nothing from the children
        if (!evt || evt.target === sectionEl) {
          if (sectionEl.dataset.drawerEndStatus === 'open') {
            sectionEl.dataset.drawerState = 'open'
          } else {
            sectionEl.dataset.drawerState = 'closed'
          }
        }
      }

      sectionEl.addEventListener('transitionstart', transitionStart)
      sectionEl.addEventListener('transitionend', transitionEnd)

      // execute one time to set the initial data-drawer-state
      transitionEnd()

      return () => {
        sectionEl.removeEventListener('transitionstart', transitionStart)
        sectionEl.removeEventListener('transitionend', transitionEnd)
      }
    }
  }, [])

  useEffect(() => {
    const headerHeight = 49 // 49px accounts for the height of `PopoverHeader`
    const footerHeight = footerRef ? footerRef.clientHeight + 1 : 0 // 1px accounts for the border of the footer
    const heightToSubtract =
      headerHeight +
      footerHeight +
      (stepperProps ? 56 /** Height of the Stepper */ : 0)
    const height = (windowHeight ?? 0) - heightToSubtract
    setContentHeight(height)
  }, [footerRef, stepperProps, windowHeight])

  const close = useCallback(
    (isOutsideClick?: boolean) => {
      setIsTransitioning(true)
      timeoutRef.current = setTimeout(() => {
        closeCallout(isOutsideClick)
        setIsTransitioning(false)
      }, 250)
    },
    [closeCallout],
  )

  useEffect(() => {
    return () => {
      clearTimeout(timeoutRef.current)
    }
  }, [])

  // when onlyMobile, we won't use this at all on > md devices
  if (hide) return <></>

  return isClient && isOpen ? (
    (createPortal(
      <>
        <BackgroundOverlay
          close={() => close(true)} // Passing in true to indicate that we are clicking outside of the drawer
          isTransitioning={isTransitioning}
          style={{
            zIndex: zIndex - 1,
            background:
              layerPosition && !showBackgroundOverlay
                ? 'rgba(0, 0, 0, 0)' /** Overriding the opacity for the background so that SideDrawers that have `layerPosition` defined will not have the background overlay visible. */
                : 'rgba(0, 0, 0, 0.25)' /** This is the style defined in _background-overlay.module.scss */,
          }}
        />
        <section
          className={cn(
            // Base container styles
            'fixed top-0 right-0 box-border h-full w-full bg-white',
            // Shadow styles (conditional based on layerPosition)
            !layerPosition && 'shadow-md',
            // Size variants
            fullWidth ? sizeVariants.fullWidth : sizeVariants[size],
            // Animation classes
            isTransitioning
              ? 'animate-slide-out-right'
              : 'animate-slide-in-right',
            // Custom container className
            containerClassName,
          )}
          style={{ zIndex }}
          ref={sectionRef}
          tabIndex={isOpen ? 0 : -1}
          data-drawer-end-status={isOpen ? 'open' : 'closed'}
          qa-test-id={qaTestId}
        >
          <PopoverHeader
            {...(logoUrl ? { logoUrl } : { headerText: headerContent ?? '' })}
            closeCallout={() => close(false)} // Explicitly passing in false to indicate that we are clicking inside of the drawer.
            noBorderRadius
            hasBackButton={!!layerPosition}
            {...(confirmation?.type ? { styleType: confirmation.type } : {})}
          />
          {stepperProps ? <Stepper {...stepperProps} /> : null}
          <ScrollingContainer
            height={contentHeight}
            hideGradients={noGradient}
            ref={sectionContentRef}
          >
            <div
              className={cn(
                // Content padding styles
                !noContentPadding && (noGradient ? 'px-4 pt-4' : 'px-4'),
                // Custom content className
                contentClassName,
              )}
            >
              {typeof children === 'function'
                ? children({
                    height:
                      typeof contentHeight === 'number' ? contentHeight : 0,
                    close,
                  })
                : children}
            </div>
          </ScrollingContainer>
          {footerContent ? (
            <div
              className={cn(
                // Base footer styles
                'absolute right-0 bottom-0 left-0 border-t border-medium-purple bg-white',
                // Footer padding styles
                !noFooterPadding && 'p-4',
              )}
              ref={setFooterRef}
            >
              {typeof footerContent === 'function'
                ? footerContent({
                    close,
                  })
                : footerContent}
            </div>
          ) : null}
        </section>
      </>,
      document.querySelector('body') as HTMLBodyElement,
    ) as ReactPortal)
  ) : (
    <></>
  )
}
