const React = require(`react`)
const path = require(`path`)
const {
  renderToString,
  renderToStaticMarkup,
  pipeToNodeWritable,
} = require(`react-dom/server`)
const { ServerLocation, Router, isRedirect } = require(`@gatsbyjs/reach-router`)
const merge = require(`deepmerge`)
const { StaticQueryContext } = require(`gatsby`)
const fs = require(`fs`)
const { WritableAsPromise } = require(`./server-utils/writable-as-promise`)

const { RouteAnnouncerProps } = require(`./route-announcer-props`)
const { apiRunner, apiRunnerAsync } = require(`./api-runner-ssr`)
const syncRequires = require(`$virtual/sync-requires`)
const { version: gatsbyVersion } = require(`gatsby/package.json`)
const { grabMatchParams } = require(`./find-path`)

const chunkMapping = require(`../public/chunk-map.json`)

// we want to force posix-style joins, so Windows doesn't produce backslashes for urls
const { join } = path.posix

// const testRequireError = require("./test-require-error")
// For some extremely mysterious reason, webpack adds the above module *after*
// this module so that when this code runs, testRequireError is undefined.
// So in the meantime, we'll just inline it.
const testRequireError = (moduleName, err) => {
  const regex = new RegExp(`Error: Cannot find module\\s.${moduleName}`)
  const firstLine = err.toString().split(`\n`)[0]
  return regex.test(firstLine)
}

let Html
try {
  Html = require(`../src/html`)
} catch (err) {
  if (testRequireError(`../src/html`, err)) {
    Html = require(`./default-html`)
  } else {
    throw err
  }
}

Html = Html && Html.__esModule ? Html.default : Html

const getPageDataPath = path => {
  const fixedPagePath = path === `/` ? `index` : path
  return join(`page-data`, fixedPagePath, `page-data.json`)
}

const getPageDataUrl = pagePath => {
  const pageDataPath = getPageDataPath(pagePath)
  return `${__PATH_PREFIX__}/${pageDataPath}`
}

const getStaticQueryPath = hash => join(`page-data`, `sq`, `d`, `${hash}.json`)

const getStaticQueryUrl = hash =>
  `${__PATH_PREFIX__}/${getStaticQueryPath(hash)}`

const getAppDataUrl = () =>
  `${__PATH_PREFIX__}/${join(`page-data`, `app-data.json`)}`

const createElement = React.createElement

export const sanitizeComponents = components => {
  const componentsArray = [].concat(components).flat(Infinity).filter(Boolean)

  return componentsArray.map(component => {
    // Ensure manifest is always loaded from content server
    // And not asset server when an assetPrefix is used
    if (__ASSET_PREFIX__ && component.props.rel === `manifest`) {
      return React.cloneElement(component, {
        href: component.props.href.replace(__ASSET_PREFIX__, ``),
      })
    }
    return component
  })
}

function deepMerge(a, b) {
  const combineMerge = (target, source, options) => {
    const destination = target.slice()

    source.forEach((item, index) => {
      if (typeof destination[index] === `undefined`) {
        destination[index] = options.cloneUnlessOtherwiseSpecified(
          item,
          options
        )
      } else if (options.isMergeableObject(item)) {
        destination[index] = merge(target[index], item, options)
      } else if (target.indexOf(item) === -1) {
        destination.push(item)
      }
    })
    return destination
  }

  return merge(a, b, { arrayMerge: combineMerge })
}

export default async function staticPage({
  pagePath,
  pageData,
  staticQueryContext,
  styles,
  scripts,
  reversedStyles,
  reversedScripts,
  inlinePageData = false,
}) {
  // for this to work we need this function to be sync or at least ensure there is single execution of it at a time
  global.unsafeBuiltinUsage = []

  try {
    let bodyHtml = ``
    let headComponents = [
      <meta
        name="generator"
        content={`Gatsby ${gatsbyVersion}`}
        key={`generator-${gatsbyVersion}`}
      />,
    ]
    let htmlAttributes = {}
    let bodyAttributes = {}
    let preBodyComponents = []
    let postBodyComponents = []
    let bodyProps = {}

    function loadPageDataSync(_pagePath) {
      if (_pagePath === pagePath) {
        // no need to use fs if we are asking for pageData of current page
        return pageData
      }

      const pageDataPath = getPageDataPath(_pagePath)
      const pageDataFile = join(process.cwd(), `public`, pageDataPath)
      try {
        // deprecation notice
        const myErrorHolder = {
          name: `Usage of loadPageDataSync for page other than currently generated page disables incremental html generation in future builds`,
        }
        Error.captureStackTrace(myErrorHolder, loadPageDataSync)
        global.unsafeBuiltinUsage.push(myErrorHolder.stack)
        const pageDataJson = fs.readFileSync(pageDataFile)
        return JSON.parse(pageDataJson)
      } catch (error) {
        // not an error if file is not found. There's just no page data
        return null
      }
    }

    const replaceBodyHTMLString = body => {
      bodyHtml = body
    }

    const setHeadComponents = components => {
      headComponents = headComponents.concat(sanitizeComponents(components))
    }

    const setHtmlAttributes = attributes => {
      // TODO - we should remove deep merges
      htmlAttributes = deepMerge(htmlAttributes, attributes)
    }

    const setBodyAttributes = attributes => {
      // TODO - we should remove deep merges
      bodyAttributes = deepMerge(bodyAttributes, attributes)
    }

    const setPreBodyComponents = components => {
      preBodyComponents = preBodyComponents.concat(
        sanitizeComponents(components)
      )
    }

    const setPostBodyComponents = components => {
      postBodyComponents = postBodyComponents.concat(
        sanitizeComponents(components)
      )
    }

    const setBodyProps = props => {
      // TODO - we should remove deep merges
      bodyProps = deepMerge({}, bodyProps, props)
    }

    const getHeadComponents = () => headComponents

    const replaceHeadComponents = components => {
      headComponents = sanitizeComponents(components)
    }

    const getPreBodyComponents = () => preBodyComponents

    const replacePreBodyComponents = components => {
      preBodyComponents = sanitizeComponents(components)
    }

    const getPostBodyComponents = () => postBodyComponents

    const replacePostBodyComponents = components => {
      postBodyComponents = sanitizeComponents(components)
    }

    const pageDataUrl = getPageDataUrl(pagePath)

    const { componentChunkName, staticQueryHashes = [] } = pageData

    const staticQueryUrls = staticQueryHashes.map(getStaticQueryUrl)

    class RouteHandler extends React.Component {
      render() {
        const props = {
          ...this.props,
          ...pageData.result,
          params: {
            ...grabMatchParams(this.props.location.pathname),
            ...(pageData.result?.pageContext?.__params || {}),
          },
        }

        const pageElement = createElement(
          syncRequires.components[componentChunkName],
          props
        )

        const wrappedPage = apiRunner(
          `wrapPageElement`,
          { element: pageElement, props },
          pageElement,
          ({ result }) => {
            return { element: result, props }
          }
        ).pop()

        return wrappedPage
      }
    }

    const routerElement = (
      <ServerLocation url={`${__BASE_PATH__}${pagePath}`}>
        <Router id="gatsby-focus-wrapper" baseuri={__BASE_PATH__}>
          <RouteHandler path="/*" />
        </Router>
        <div {...RouteAnnouncerProps} />
      </ServerLocation>
    )

    const bodyComponent = (
      <StaticQueryContext.Provider value={staticQueryContext}>
        {apiRunner(
          `wrapRootElement`,
          { element: routerElement, pathname: pagePath },
          routerElement,
          ({ result }) => {
            return { element: result, pathname: pagePath }
          }
        ).pop()}
      </StaticQueryContext.Provider>
    )

    // Let the site or plugin render the page component.
    await apiRunnerAsync(`replaceRenderer`, {
      bodyComponent,
      replaceBodyHTMLString,
      setHeadComponents,
      setHtmlAttributes,
      setBodyAttributes,
      setPreBodyComponents,
      setPostBodyComponents,
      setBodyProps,
      pathname: pagePath,
      pathPrefix: __PATH_PREFIX__,
    })

    // If no one stepped up, we'll handle it.
    if (!bodyHtml) {
      try {
        // react 18 enabled
        if (pipeToNodeWritable) {
          const writableStream = new WritableAsPromise()
          const { startWriting } = pipeToNodeWritable(
            bodyComponent,
            writableStream,
            {
              onCompleteAll() {
                startWriting()
              },
              onError() {},
            }
          )

          bodyHtml = await writableStream
        } else {
          bodyHtml = renderToString(bodyComponent)
        }
      } catch (e) {
        // ignore @reach/router redirect errors
        if (!isRedirect(e)) throw e
      }
    }

    apiRunner(`onRenderBody`, {
      setHeadComponents,
      setHtmlAttributes,
      setBodyAttributes,
      setPreBodyComponents,
      setPostBodyComponents,
      setBodyProps,
      pathname: pagePath,
      loadPageDataSync,
      bodyHtml,
      scripts,
      styles,
      pathPrefix: __PATH_PREFIX__,
    })

    reversedScripts.forEach(script => {
      // Add preload/prefetch <link>s for scripts.
      headComponents.push(
        <link
          as="script"
          rel={script.rel}
          key={script.name}
          href={`${__PATH_PREFIX__}/${script.name}`}
        />
      )
    })

    if (pageData && !inlinePageData) {
      headComponents.push(
        <link
          as="fetch"
          rel="preload"
          key={pageDataUrl}
          href={pageDataUrl}
          crossOrigin="anonymous"
        />
      )
    }
    staticQueryUrls.forEach(staticQueryUrl =>
      headComponents.push(
        <link
          as="fetch"
          rel="preload"
          key={staticQueryUrl}
          href={staticQueryUrl}
          crossOrigin="anonymous"
        />
      )
    )

    const appDataUrl = getAppDataUrl()
    if (appDataUrl) {
      headComponents.push(
        <link
          as="fetch"
          rel="preload"
          key={appDataUrl}
          href={appDataUrl}
          crossOrigin="anonymous"
        />
      )
    }

    reversedStyles.forEach(style => {
      // Add <link>s for styles that should be prefetched
      // otherwise, inline as a <style> tag

      if (style.rel === `prefetch`) {
        headComponents.push(
          <link
            as="style"
            rel={style.rel}
            key={style.name}
            href={`${__PATH_PREFIX__}/${style.name}`}
          />
        )
      } else {
        headComponents.unshift(
          <style
            data-href={`${__PATH_PREFIX__}/${style.name}`}
            data-identity={`gatsby-global-css`}
            dangerouslySetInnerHTML={{
              __html: style.content,
            }}
          />
        )
      }
    })

    // Add page metadata for the current page
    const windowPageData = `/*<![CDATA[*/window.pagePath="${pagePath}";${
      inlinePageData ? `window.pageData=${JSON.stringify(pageData)};` : ``
    }/*]]>*/`

    postBodyComponents.push(
      <script
        key={`script-loader`}
        id={`gatsby-script-loader`}
        dangerouslySetInnerHTML={{
          __html: windowPageData,
        }}
      />
    )

    // Add chunk mapping metadata
    const scriptChunkMapping = `/*<![CDATA[*/window.___chunkMapping=${JSON.stringify(
      chunkMapping
    )};/*]]>*/`

    postBodyComponents.push(
      <script
        key={`chunk-mapping`}
        id={`gatsby-chunk-mapping`}
        dangerouslySetInnerHTML={{
          __html: scriptChunkMapping,
        }}
      />
    )

    let bodyScripts = []
    if (chunkMapping[`polyfill`]) {
      chunkMapping[`polyfill`].forEach(script => {
        const scriptPath = `${__PATH_PREFIX__}${script}`
        bodyScripts.push(
          <script key={scriptPath} src={scriptPath} noModule={true} />
        )
      })
    }

    // Filter out prefetched bundles as adding them as a script tag
    // would force high priority fetching.
    bodyScripts = bodyScripts.concat(
      scripts
        .filter(s => s.rel !== `prefetch`)
        .map(s => {
          const scriptPath = `${__PATH_PREFIX__}/${JSON.stringify(s.name).slice(
            1,
            -1
          )}`
          return <script key={scriptPath} src={scriptPath} async />
        })
    )

    postBodyComponents.push(...bodyScripts)

    apiRunner(`onPreRenderHTML`, {
      getHeadComponents,
      replaceHeadComponents,
      getPreBodyComponents,
      replacePreBodyComponents,
      getPostBodyComponents,
      replacePostBodyComponents,
      pathname: pagePath,
      pathPrefix: __PATH_PREFIX__,
    })

    const html = `<!DOCTYPE html>${renderToStaticMarkup(
      <Html
        {...bodyProps}
        headComponents={headComponents}
        htmlAttributes={htmlAttributes}
        bodyAttributes={bodyAttributes}
        preBodyComponents={preBodyComponents}
        postBodyComponents={postBodyComponents}
        body={bodyHtml}
        path={pagePath}
      />
    )}`

    return { html, unsafeBuiltinsUsage: global.unsafeBuiltinUsage }
  } catch (e) {
    e.unsafeBuiltinsUsage = global.unsafeBuiltinUsage
    throw e
  }
}
