'use client'

import {
  createContext,
  isValidElement,
  useContext,
  useMemo,
  useRef,
  useState,
  type ComponentProps,
  type ComponentType,
  type ElementType,
  type KeyboardEvent,
  type MouseEvent,
  type ReactElement,
  type ReactNode,
} from 'react'
import { useMergedRefs } from '@base-ui/utils/useMergedRefs'
import { mergeProps } from '@react-aria/utils'
import { useButton } from 'react-aria'
import type { Merge } from 'type-fest'

import { cn } from '../../utils/index.ts'
import { Render, type RenderFn, type RenderFnProps } from './Render.tsx'

interface LinkComponentObject {
  component: ElementType
  /** @default false */
  useForDisabledLinks?: boolean | undefined
  props?: (props: ComponentProps<'a'>) => Record<string, unknown>
}

type LinkComponent =
  | ComponentType<ComponentProps<'a'>>
  | ComponentType<{ href: string }>
  | ReactElement
  | LinkComponentObject
  | null

export type ButtonOrLinkState = {
  category: 'button' | 'link'
  role: NonNullable<InternalButtonOrLinkProps['role']>
  disabled: NonNullable<InternalButtonOrLinkProps['disabled']>
  type?: InternalButtonOrLinkProps['type']
  checked?: InternalButtonOrLinkProps['checked']
  href?: InternalButtonOrLinkProps['href']
  target?: InternalButtonOrLinkProps['target']
}

export type ButtonOrLinkRenderState = ButtonOrLinkState & {
  Element: ElementType<ButtonOrLinkRenderElementProps>
  elementProps: ButtonOrLinkRenderElementProps
}

interface ButtonOrLinkRenderElementProps {
  buttonOrLinkState: ButtonOrLinkState
  [key: string]: unknown
}

export declare namespace InternalButtonOrLinkProps {
  interface DisableableProps {
    disabled?: boolean | 'focusable' | undefined
  }
  interface CommonProps {
    /**
     * Whether to render the element inline instead of block-level. For buttons, renders a `span`
     * that behaves like an accessible button. For links, this prop has no effect as anchor elements
     * are already inline. This may not be necessary when `button` elements are allowed to be truly
     * inline (see https://github.com/w3c/csswg-drafts/issues/3226).
     *
     * @default false
     */
    inline?: boolean | undefined
    /** Custom render function to control the rendered element. */
    render?: RenderFn<ButtonOrLinkRenderState> | undefined
    /** Custom component to use for rendering links (e.g. Next.js Link) */
    linkComponent?: LinkComponent | undefined
  }
  interface ButtonProps
    extends
      CommonProps,
      DisableableProps,
      Omit<ComponentProps<'button'>, 'defaultValue' | 'disabled'> {
    href?: undefined
    target?: undefined
    checked?: undefined
    defaultChecked?: undefined
  }
  interface ToggleButtonProps extends Omit<ButtonProps, 'checked' | 'defaultChecked'> {
    /** Whether the button is in a pressed/selected state. */
    checked?: boolean | undefined
    defaultChecked?: boolean | undefined
  }
  interface LinkProps
    extends CommonProps, DisableableProps, Omit<ComponentProps<'a'>, 'defaultValue'> {
    href: string
    type?: undefined
    checked?: undefined
    defaultChecked?: undefined
  }
}

export type InternalButtonOrLinkProps =
  | InternalButtonOrLinkProps.ButtonProps
  | InternalButtonOrLinkProps.ToggleButtonProps
  | InternalButtonOrLinkProps.LinkProps

export type OmitInternalButtonOrLinkProps<T> = Omit<T, 'disabled' | 'inline' | 'render'>

export declare namespace ButtonOrLinkProps {
  export type DisableableProps = InternalButtonOrLinkProps.DisableableProps
  export type ButtonProps = OmitInternalButtonOrLinkProps<InternalButtonOrLinkProps.ButtonProps>
  export type ToggleButtonProps =
    OmitInternalButtonOrLinkProps<InternalButtonOrLinkProps.ToggleButtonProps>
  export type LinkProps = OmitInternalButtonOrLinkProps<InternalButtonOrLinkProps.LinkProps>
}

export type ButtonOrLinkProps =
  | ButtonOrLinkProps.ButtonProps
  | ButtonOrLinkProps.ToggleButtonProps
  | ButtonOrLinkProps.LinkProps

const ButtonOrLinkContext = createContext<ButtonOrLinkState | null>(null)

export function useAncestorButtonOrLink() {
  return useContext(ButtonOrLinkContext)
}

/**
 * Creating "with context" components as opposed to wrapping `ButtonOrLink`'s returned element in a
 * context provider to avoid a "Trying to render a nested `ButtonOrLink`" error when rendering
 * `ButtonOrLink` siblings in the `render` function (like `Card` does with `interactiveContent`).
 */

type WithContextComponent<T extends ElementType> = (
  props: Merge<ComponentProps<T>, ButtonOrLinkRenderElementProps>,
) => ReactElement

const memoizedWithContextComponents = new Map<ElementType, unknown>()

function withContext<T extends ElementType>(Element: T): WithContextComponent<T> {
  const cached = memoizedWithContextComponents.get(Element)
  if (cached) return cached as WithContextComponent<T>
  const newWithContextComponent: WithContextComponent<T> = ({ buttonOrLinkState, ...props }) => (
    <ButtonOrLinkContext.Provider value={buttonOrLinkState}>
      <Element {...(props as ComponentProps<T>)} />
    </ButtonOrLinkContext.Provider>
  )
  memoizedWithContextComponents.set(Element, newWithContextComponent)
  return newWithContextComponent
}

const ButtonWithContext = withContext('button')
const SpanButtonWithContext = withContext(SpanButton)
const AnchorWithContext = withContext('a')
const RenderWithContext = withContext(Render)

interface ButtonOrLinkConfigContextValue {
  onClick:
    | ((
        event: MouseEvent<HTMLButtonElement & HTMLAnchorElement>,
        props: InternalButtonOrLinkProps,
      ) => void)
    | undefined
  transformProps: ((props: InternalButtonOrLinkProps) => InternalButtonOrLinkProps) | undefined
}

const ButtonOrLinkConfigContext = createContext<ButtonOrLinkConfigContextValue | null>(null)

/**
 * Renders a link (`a` or custom `linkComponent`) if `href` is passed. Otherwise, renders a `button`
 * (or a `span` that behaves like a button if `inline` is true).
 */
export const ButtonOrLinkRoot = (passedProps: InternalButtonOrLinkProps) => {
  const ancestorButtonOrLink = useAncestorButtonOrLink()

  if (ancestorButtonOrLink) {
    throw new Error('[ButtonOrLink] Trying to render a nested `ButtonOrLink`')
  }

  const config = useContext(ButtonOrLinkConfigContext)
  const transformProps = config?.transformProps ?? ((props) => props)

  const props = transformProps({
    ...passedProps,
    onClick:
      passedProps.onClick || config?.onClick
        ? (event: MouseEvent<HTMLButtonElement & HTMLAnchorElement>) => {
            passedProps.onClick?.(event)
            if (event.isPropagationStopped()) return
            config?.onClick?.(event, passedProps)
          }
        : undefined,
  })

  const {
    disabled = false,
    inline = false,
    linkComponent: passedLinkComponent,
    render: passedRender,
    className: passedClassName,
    children,
    ...specificProps
  } = props
  const manuallyDisabledEventProps = {
    onClick: undefined,
    onMouseDown: undefined,
    onMouseUp: undefined,
    onPointerDown: undefined,
    onPointerUp: undefined,
    onTouchStart: undefined,
    onTouchEnd: undefined,
    onKeyDown: undefined,
    onKeyUp: undefined,
  }

  const render = (
    Element: ElementType<ButtonOrLinkRenderElementProps>,
    renderProps: RenderFnProps,
    buttonOrLinkState: ButtonOrLinkState,
    extraElementProps?: Record<string, unknown>,
  ) => {
    const elementProps: ButtonOrLinkRenderElementProps = { buttonOrLinkState, ...extraElementProps }
    const renderState: ButtonOrLinkRenderState = {
      ...buttonOrLinkState,
      Element,
      elementProps,
    }
    return passedRender ? (
      passedRender(renderProps, renderState)
    ) : (
      <Element {...renderProps} {...elementProps} />
    )
  }

  const className = cn('gds-button-or-link', passedClassName)

  if (props.href === undefined) {
    const {
      role = 'button',
      type = 'button',
      checked,
      defaultChecked,
      href: _href,
      target: _target,
      ...remainingProps
    } = specificProps

    if (defaultChecked !== undefined) {
      // oxlint-disable-next-line no-console
      console.warn(
        '[ButtonOrLink] `defaultChecked` is not supported; use a controlled `checked` prop instead, or wrap `ButtonOrLink` in a component that manages the uncontrolled state',
      )
    }

    const checkedAttribute =
      checked === undefined
        ? {}
        : ((checked: unknown) => {
            if (
              (role === 'option' || role === 'tab' || role === 'row' || role === 'gridcell') &&
              checked !== 'indeterminate'
            ) {
              return { 'aria-selected': Boolean(checked) }
            }
            const checkedOrMixed =
              checked === 'indeterminate' ? ('mixed' as const) : Boolean(checked)
            if (role === 'button') {
              return { 'aria-pressed': checkedOrMixed }
            }
            return { 'aria-checked': checkedOrMixed }
          })(checked)

    const buttonProps: ComponentProps<'button'> = {
      role,
      type: disabled === 'focusable' ? 'button' : type,
      disabled: disabled === true,
      'aria-disabled': disabled === 'focusable' || undefined,
      ...checkedAttribute,
      className,
      children,
      ...(remainingProps as ComponentProps<'button'>),
      ...((disabled === 'focusable' || (disabled && inline)) && manuallyDisabledEventProps),
    }

    const buttonOrLinkState: ButtonOrLinkState = {
      category: 'button',
      role,
      disabled,
      type,
      checked,
    }

    return render(
      inline ? SpanButtonWithContext : ButtonWithContext,
      buttonProps,
      buttonOrLinkState,
    )
  }

  const {
    href,
    target,
    role = 'link',
    rel = target === '_blank' ? 'noopener noreferrer' : undefined,
    type: _type,
    checked: _checked,
    defaultChecked: _defaultChecked,
    ...remainingProps
  } = specificProps

  let linkProps: ComponentProps<'a'> = {
    role: disabled || role !== 'link' ? role : undefined,
    href: !disabled ? href : undefined,
    target: !disabled ? target : undefined,
    rel: !disabled ? rel : undefined,
    tabIndex: disabled === 'focusable' ? 0 : undefined,
    'aria-disabled': Boolean(disabled) || undefined,
    className,
    children,
    ...(remainingProps as ComponentProps<'a'>),
    ...(disabled && manuallyDisabledEventProps),
    // Prevent scrolling the page when Space is pressed while a link is focused
    onKeyDown: (event: KeyboardEvent<HTMLAnchorElement>) => {
      if (event.key === ' ') event.preventDefault()
      if (!disabled) (remainingProps as ComponentProps<'a'>).onKeyDown?.(event)
    },
  }

  let linkComponent: ElementType | ReactElement | null = null
  if (passedLinkComponent) {
    if (typeof passedLinkComponent === 'object' && 'component' in passedLinkComponent) {
      const useForDisabledLinks = passedLinkComponent.useForDisabledLinks ?? false
      if (!disabled || useForDisabledLinks) {
        linkComponent = passedLinkComponent.component
        if (passedLinkComponent.props) {
          linkProps = passedLinkComponent.props(linkProps)
        }
      }
    } else if (!disabled) {
      linkComponent = passedLinkComponent
    }
  }

  const buttonOrLinkState: ButtonOrLinkState = {
    category: 'link',
    role,
    disabled,
    href,
    target,
  }

  if (isValidElement(linkComponent)) {
    return render(
      RenderWithContext as ButtonOrLinkRenderState['Element'],
      linkProps,
      buttonOrLinkState,
      { render: linkComponent },
    )
  }

  return render(
    linkComponent && linkComponent !== 'a' ? withContext(linkComponent) : AnchorWithContext,
    linkProps,
    buttonOrLinkState,
  )
}
ButtonOrLinkRoot.displayName = 'ButtonOrLink'

export interface ButtonOrLinkConfigProps {
  onClick?: ButtonOrLinkConfigContextValue['onClick']
  transformProps?: ButtonOrLinkConfigContextValue['transformProps']
  children: ReactNode
}

export const ButtonOrLinkConfig = ({
  onClick: passedOnClick,
  transformProps: passedTransformProps,
  children,
}: ButtonOrLinkConfigProps) => {
  const ancestorConfig = useContext(ButtonOrLinkConfigContext)

  const onClick =
    passedOnClick && ancestorConfig?.onClick
      ? (() => {
          const ancestorOnClick = ancestorConfig.onClick
          return (
            event: MouseEvent<HTMLButtonElement & HTMLAnchorElement>,
            props: InternalButtonOrLinkProps,
          ) => {
            passedOnClick(event, props)
            if (event.isPropagationStopped()) return
            ancestorOnClick(event, props)
          }
        })()
      : (passedOnClick ?? ancestorConfig?.onClick)
  const transformProps =
    passedTransformProps && ancestorConfig?.transformProps
      ? (() => {
          const ancestorTransformProps = ancestorConfig.transformProps
          return (props: InternalButtonOrLinkProps) => {
            return ancestorTransformProps(passedTransformProps(props))
          }
        })()
      : (passedTransformProps ?? ancestorConfig?.transformProps)

  const buttonOrLinkConfigContextValue = useMemo(() => {
    if (!onClick && !transformProps) return null
    return { onClick, transformProps }
  }, [onClick, transformProps])

  return (
    <ButtonOrLinkConfigContext.Provider value={buttonOrLinkConfigContextValue}>
      {children}
    </ButtonOrLinkConfigContext.Provider>
  )
}
ButtonOrLinkConfig.displayName = 'ButtonOrLink.Config'

export interface ButtonOrLinkConsumerProps {
  render: (state: ButtonOrLinkState | null) => ReactNode
}

export function ButtonOrLinkConsumer({ render }: ButtonOrLinkConsumerProps) {
  const state = useAncestorButtonOrLink()
  return render(state)
}
ButtonOrLinkConsumer.displayName = 'ButtonOrLink.Consumer'

interface SpanButtonProps
  extends
    Omit<ComponentProps<'span'>, 'ref'>,
    Pick<ComponentProps<'button'>, 'ref' | 'type' | 'disabled'> {}

function SpanButton({
  ref: passedRef,
  type = 'button',
  disabled = false,
  onClick,
  children,
  ...props
}: SpanButtonProps) {
  const spanRef = useRef<HTMLSpanElement>(null)
  const spanPassedRef = useMergedRefs(spanRef, passedRef)
  const [active, setActive] = useState(false)
  const { buttonProps } = useButton(
    {
      type,
      isDisabled: disabled,
      elementType: 'span',
      onPress: (event) => {
        event.target.dispatchEvent(
          new MouseEvent('click', {
            altKey: event.altKey,
            ctrlKey: event.ctrlKey,
            metaKey: event.metaKey,
            shiftKey: event.shiftKey,
            relatedTarget: event.target,
            bubbles: true,
            cancelable: true,
          }),
        )
      },
      onPressStart: (event) => {
        if (event.pointerType === 'keyboard') setActive(true)
      },
      onPressEnd: () => {
        if (active) setActive(false)
      },
    },
    spanRef,
  )

  return (
    <span
      ref={spanPassedRef}
      data-gds-exposed-pointer={active ? 'active' : undefined}
      {...mergeProps(props, {
        ...buttonProps,
        onClick: (event: MouseEvent<HTMLSpanElement>) => {
          onClick?.(event)
          /**
           * Prevent toggling a `<input type="checkbox">` or `<input type="radio">` when clicking on
           * a button inside the input's `<label>`.
           */
          event.preventDefault()
        },
      })}
    >
      {children}
    </span>
  )
}
