import {
  MinimalComponentProps,
  getComponent,
  getMergedComponentProperties,
  isWebComponent,
  registeredDatasources,
  registered,
  toKebabCase
} from '@sitecore/byoc'
import React from 'react'
export * from '@sitecore/byoc'

var NextClientsideRenderer: any = null
var NextDynamicFunction: any = null

export interface ComponentProps extends MinimalComponentProps {
  fallbackWrapper?: boolean
  fallback?: any
  clientFallback?: any
}

/**
 * A special case of Component designed for Next.js.
 * It uses `next/dynamic` to control rendering on the client, server, or both.
 *
 * @param {ComponentProps} props  - The properties for the component.
 * @returns {JSX.Element} The JSX element representing the component.
 */
export function NextComponent(props: ComponentProps) {
  // See if component is registered in current context
  const { componentName, clientFallback, ...attributes } = props
  const Component = getComponent(componentName)?.component

  // Render component as is on the server
  const Regular = React.useMemo<any>(() => <RegularComponent {...attributes} componentName={componentName} />, [])

  // maintain identity of wrapped component
  const External = React.useMemo<any>(
    () =>
      NextDynamicFunction(() => Promise.resolve(NextClientsideRenderer), {
        ssr: false,
        // Show server-rendered component during page load
        loading: () => Regular
      }),
    []
  )

  // remove functions from props to avoid them to be passed from server to client which is illegal
  var sanitizedAttributes = typeof window == 'undefined' ? JSON.parse(JSON.stringify(attributes)) : attributes

  return (
    <External
      {...sanitizedAttributes}
      componentName={componentName}
      fallbackWrapper={!Component}
      fallback={
        Component
          ? // If there's no client component, keep the server-rendered component displayed
            Regular
          : // wrap clientFallback into a dynamic() call to make it render clientside hydration
          clientFallback
          ? React.createElement(NextDynamicFunction(() => Promise.resolve(() => clientFallback), { ssr: false }))
          : props.fallback
      }
    ></External>
  )
}

/**
 * Enables Next.js client/server rendering options for registered components.
 * The `component` argument should be a function exported from a file with a `use client` directive.
 *
 * @param {any} dynamic    - The Next dynamic function.
 * @param {any} component  - The component function.
 * @returns {any} Augmented bundle.
 */
export function enableNextClientsideComponents(dynamic: any, component: any) {
  NextDynamicFunction = dynamic
  NextClientsideRenderer = component
  return component
}

/**
 * Renders a registered external component in a React tree. If enableNextClientsideComponents was called previously,
 * the component is rendered using NextComponent codepath, which allows rendering separate component on client and
 * server.
 *
 * @param {ComponentProps} props  - The properties for the component.
 * @returns {JSX.Element} The JSX element representing the component.
 */
export function Component(props: ComponentProps) {
  if (Object.keys(props).length == 0) {
    return <></>
  }
  // _dynamic property ensures the component does not endlessly loop in pre-app-router setup
  if (NextDynamicFunction && !props._dynamic) {
    return NextComponent({ _dynamic: true, ...props })
  }
  return RegularComponent(props)
}

/**
 * Pass list of server side component registrations to clients, and embed clientside components to the page.
 */
export function Bundle() {
  return (
    <>
      <byoc-registration
        components={JSON.stringify(Object.values(registered))}
        datasources={JSON.stringify(Object.values(registeredDatasources))}
        suppressHydrationWarning
      />
      {/** Will not be rendered, but needs to be referenced */}
      {<NextClientsideRenderer />}
    </>
  )
}

/**
 * A wrapper over External component that accepts a fallback for rendering in case of a missing component.
 * It accepts properties in camelcase format and ensures no hydration errors on the client-side.
 *
 * @param {ComponentProps} props  - The properties for the component.
 * @returns {JSX.Element} The JSX element representing the component.
 */
export function RegularComponent(props: ComponentProps) {
  const { componentName, fallback, fallbackWrapper } = props
  const definition = getComponent(componentName)
  const Component = definition?.component as any
  const { attributes, properties, merged } = getMergedComponentProperties(props)
  if ((!Component && fallback) || !componentName) {
    if (fallbackWrapper === false) return <>{fallback}</>
    return (
      <feaas-external {...attributes} hydrate='false'>
        {fallback}
      </feaas-external>
    )
  }
  if (definition && Component && isWebComponent(Component)) {
    const webComponentName = 'byoc-' + toKebabCase(definition.id)
    return React.createElement(webComponentName, {
      ...attributes,
      ref: (el: { sitecoreContextCallback: (context: typeof properties) => {} }) => {
        if (el && typeof window != 'undefined') {
          // web components can define sitecoreContextCallback defined that recieves props directly
          window.customElements?.whenDefined(webComponentName).then(() => {
            el.sitecoreContextCallback?.({ ...merged })
          })
        }
      }
    })
  }
  return (
    <>
      <feaas-external {...attributes} hydrate='false'>
        {Component == null ? null : <Component {...merged} />}
      </feaas-external>
    </>
  )
}
declare global {
  namespace JSX {
    interface IntrinsicElements {
      'feaas-external': {
        'data-external-id': string
        children?: any
        dangerouslySetInnerHTML?: { __html: string }
        [key: string]: any
      }
    }
  }
}
