import React from 'react'
import { AppRegistry } from 'react-native'
import { flush, openRenderBracket } from './ssr-media-query/utils/inject'
/** @typedef {import('react').ComponentType} ReactComponent */
/** @typedef {import('react').ReactElement} ReactElement */

/**
 * Returns object with `renderApp` and `getStyles` functions.
 * Weave these into your app's server-side render process:
 *
 * - Call `renderApp` first to do the actual server-side render
 * - After the render is complete, call `getStyles`
 * - Include the style tags returned by `getStyles` in the SSR <head>
 *
 * Media query CSS is collected in one store shared by the process, and every rule records the
 * renders that created it, so renders may overlap: rendering two pages at the same time in one
 * process gives each response its own rules, plus any rule the other render created while both were
 * in flight. Those extra rules are inert, because each is scoped to the `data-media` id of the
 * elements that use it. Rendering several apps in one page is supported, whether one after another
 * or one inside another.
 *
 * Call `getStyles` for every `renderApp`. A render that never calls it stays open, and while it is
 * open no rule created anywhere in the process is released, so the store grows for the lifetime of
 * the process as it did before #7212.
 *
 * @param {string} [appName] - optional unique identifier if ssrStyles is called multiple times for multiple apps
 * @param {object} [options] -
 *  - `styleGetters`: optional array of additional style getter functions to call after render
 *  - `collectStyles`: optional function, takes the rendered app, returns either the same or a new app element
 * @param {boolean} [options.styleGetters]
 * @param {(ReactElement) => ReactElement} [options.collectStyles]
 */
export const ssrStyles = (appName = 'UDS app', { styleGetters = [], collectStyles } = {}) => {
  let hasAppRendered = false
  let getRNWStyle
  let renderBracket
  return {
    /**
     * Server-side-renders the provided app in a way that supports collecting
     * styles for Styled Components and React Native Web stylesheets.
     *
     * Opens this render's media query bracket, so every rule created from here until `getStyles` is
     * attributed to this render. Called more than once before `getStyles`, it keeps the bracket it
     * already opened, so a framework that renders the same app twice does not leave one open. Opens
     * nothing in the browser, where rules live for the lifetime of the page.
     *
     * @param {ReactComponent} App - the root component for the app
     * @param {object} [props] - props for this render e.g. page routing props
     * @param {object} [options] -
     *  - `renderedByRNW`: pass as true if the main render is by AppRegistry.runApplication from React Native
     *  - `WrapperComponent`: Component rendered with no props around app content and inside root tag
     * @param {boolean} [options.renderedByRNW]
     * @param {ReactComponent} [options.WrapperComponent]
     *
     * @returns {ReactElement} - the rendered app output
     */
    renderApp: (App, props, { renderedByRNW = false, WrapperComponent } = {}) => {
      if (renderBracket === undefined) {
        renderBracket = openRenderBracket()
      }

      AppRegistry.registerComponent(appName, () => App)
      // AppRegistry.getApplication renders the app in a container, and collects styles.
      const { element, getStyleElement } = AppRegistry.getApplication(appName, {
        WrapperComponent,
        initialProps: props
      })

      let renderedApp =
        // React Native Web's AppRegistry.getApplication assumes the app is rendered using
        // AppRegistry.runApplication and wraps it in <AppContainer>, which wraps the entire app
        // in two outer containers resembling <View style={{ flex: 1 }} pointerEvents="box-none">.
        // So, use that IF user says AppRegistry.runApplication will do the client-side render.
        (renderedByRNW && element) ||
        // If the live app is not rendered using AppRegistry.runApplication, we need to
        // re-render it without the <AppContainer> wrapper, to avoid SSR mismatch errors.
        // Default to this as many platforms (e.g. NextJS) will use their own renderers.
        WrapperComponent ? (
          <WrapperComponent>
            <App {...props} />
          </WrapperComponent>
        ) : (
          <App {...props} />
        )

      getRNWStyle = () => getStyleElement({ key: 'react-native-stylesheet' })

      if (typeof collectStyles === 'function') {
        renderedApp = collectStyles(renderedApp)
      }
      hasAppRendered = true
      return renderedApp
    },
    /**
     * Turns styles collected during renderApp into an array of React elements of
     * HTML <style> tags ready for insertion into the SSR HTML's <head>.
     *
     * Must be called after `renderApp` has completed.
     *
     * Closes this render's media query bracket: the returned tag carries the rules this render
     * created, and any rule no other open render created is released. Closing belongs here and not
     * in `renderApp`: an app embedded in another UDS app opens its own render bracket while the host
     * render is still collecting, so closing on `renderApp` would discard the host's rules (#7212).
     *
     * Flushes before the other style getters rather than inside the returned array, so a getter that
     * throws cannot leave this render's bracket open: an open bracket holds every rule in the process
     * for its lifetime (#7212).
     *
     * @param {...ReactElement} existingStyles - any existing style tag elements to merge in
     * @returns {ReactElement[]} - flat array of <style> React elements
     */
    getStyles: (...existingStyles) => {
      if (!hasAppRendered) throw new Error('Called getStyles before renderApp in ssrStyles')

      const mediaQueryStyles = flush(renderBracket)
      renderBracket = undefined

      return [
        ...existingStyles,
        ...styleGetters.flatMap((getter) => getter()),
        getRNWStyle(),
        mediaQueryStyles
      ]
    }
  }
}

/**
 * @deprecated - use ssrStyles instead
 *
 * Registers the app's root component with React Native Web and generates
 * the main <style> tag containing React Native Web stylesheet styles.
 *
 * @param {ReactComponent} AppRoot
 * @param {string} [appName]
 * @returns {ReactElement[]}
 */

export const getReactNativeWebSSRStyles = (AppRoot, appName = 'app') => {
  AppRegistry.registerComponent(appName, () => AppRoot)
  const { getStyleElement } = AppRegistry.getApplication(appName)
  return [getStyleElement()]
}

/**
 * @deprecated - use ssrStyles instead
 *
 * Gets style tags for each currently supported CSS-in-JS library and returns
 * them alongside any existing style tags.
 *
 * @param {ReactComponent} AppRoot
 * @param {string} [appName]
 * @param {ReactElement[]} [existingStyles]
 * @returns {ReactElement[]}
 */
export const getSSRStyles = (AppRoot, appName = 'app', existingStyles = []) => {
  return [
    ...existingStyles,
    ...getReactNativeWebSSRStyles(AppRoot, appName)
    // if any other CSS-in-JS is added e.g. styled-components generate and add its styles here
  ]
}
