/* global BROWSER_ESM_ONLY */
import React from "react"
import fs from "fs-extra"
import { renderToString, renderToStaticMarkup } from "react-dom/server"
import { get, merge, isObject, flatten, uniqBy, concat } from "lodash"
import nodePath from "path"
import { apiRunner, apiRunnerAsync } from "./api-runner-ssr"
import { grabMatchParams } from "./find-path"
import syncRequires from "$virtual/ssr-sync-requires"

import { RouteAnnouncerProps } from "./route-announcer-props"
import { ServerLocation, Router, isRedirect } from "@reach/router"
import { headHandlerForSSR } from "./head/head-export-handler-for-ssr"
import { getStaticQueryResults } from "./loader"

// prefer default export if available
const preferDefault = m => (m && m.default) || m

// import testRequireError from "./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 cachedStats
const getStats = publicDir => {
  if (cachedStats) {
    return cachedStats
  } else {
    cachedStats = JSON.parse(
      fs.readFileSync(nodePath.join(publicDir, `webpack.stats.json`), `utf-8`)
    )

    return cachedStats
  }
}

let Html
try {
  Html = require(`../src/html`)
} catch (err) {
  if (testRequireError(`../src/html`, err)) {
    Html = require(`./default-html`)
  } else {
    console.log(`There was an error requiring "src/html.js"\n\n`, err, `\n\n`)
    process.exit()
  }
}

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

export default async function staticPage({
  pagePath,
  isClientOnlyPage,
  publicDir,
  error,
  serverData,
}) {
  let bodyHtml = ``
  let headComponents = [
    <meta key="environment" name="note" content="environment=development" />,
  ]
  let htmlAttributes = {}
  let bodyAttributes = {}
  let preBodyComponents = []
  let postBodyComponents = []
  let bodyProps = {}

  if (error) {
    postBodyComponents.push([
      <script
        key="dev-ssr-error"
        dangerouslySetInnerHTML={{
          __html: `window._gatsbyEvents = window._gatsbyEvents || []; window._gatsbyEvents.push(["FAST_REFRESH", { action: "SHOW_DEV_SSR_ERROR", payload: ${JSON.stringify(
            error
          )} }])`,
        }}
      />,
      <noscript key="dev-ssr-error-noscript">
        <h1>Failed to Server Render (SSR)</h1>
        <h2>Error message:</h2>
        <p>{error.sourceMessage}</p>
        <h2>File:</h2>
        <p>
          {error.source}:{error.line}:{error.column}
        </p>
        <h2>Stack:</h2>
        <pre>
          <code>{error.stack}</code>
        </pre>
      </noscript>,
    ])
  }

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

    const setHtmlAttributes = attributes => {
      htmlAttributes = merge(htmlAttributes, attributes)
    }

    const setBodyAttributes = attributes => {
      bodyAttributes = merge(bodyAttributes, attributes)
    }

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

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

    const setBodyProps = props => {
      bodyProps = merge({}, bodyProps, props)
    }

    const getHeadComponents = () => headComponents

    const replaceHeadComponents = components => {
      headComponents = components
    }

    const replaceBodyHTMLString = body => {
      bodyHtml = body
    }

    const getPreBodyComponents = () => preBodyComponents

    const replacePreBodyComponents = components => {
      preBodyComponents = components
    }

    const getPostBodyComponents = () => postBodyComponents

    const replacePostBodyComponents = components => {
      postBodyComponents = components
    }

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

    const getPageData = pagePath => {
      const pageDataPath = getPageDataPath(pagePath)
      const absolutePageDataPath = nodePath.join(publicDir, pageDataPath)
      const pageDataJson = fs.readFileSync(absolutePageDataPath, `utf8`)

      try {
        return JSON.parse(pageDataJson)
      } catch (err) {
        return null
      }
    }

    const pageData = getPageData(pagePath)

    const { componentChunkName } = pageData

    const pageComponent = await syncRequires.ssrComponents[componentChunkName]

    headHandlerForSSR({
      pageComponent,
      setHeadComponents,
      staticQueryContext: getStaticQueryResults(),
      pageData,
      pagePath,
    })

    let scriptsAndStyles = flatten(
      [`commons`].map(chunkKey => {
        const fetchKey = `assetsByChunkName[${chunkKey}]`

        const stats = getStats(publicDir)
        let chunks = get(stats, fetchKey)
        const namedChunkGroups = get(stats, `namedChunkGroups`)

        if (!chunks) {
          return null
        }

        chunks = chunks.map(chunk => {
          if (chunk === `/`) {
            return null
          }
          return { rel: `preload`, name: chunk }
        })

        namedChunkGroups[chunkKey].assets.forEach(asset =>
          chunks.push({ rel: `preload`, name: asset.name })
        )

        const childAssets = namedChunkGroups[chunkKey].childAssets
        for (const rel in childAssets) {
          if (childAssets.hasownProperty(rel)) {
            chunks = concat(
              chunks,
              childAssets[rel].map(chunk => {
                return { rel, name: chunk }
              })
            )
          }
        }

        return chunks
      })
    )
      .filter(s => isObject(s))
      .sort((s1, _s2) => (s1.rel == `preload` ? -1 : 1)) // given priority to preload

    scriptsAndStyles = uniqBy(scriptsAndStyles, item => item.name)

    const styles = scriptsAndStyles.filter(
      style => style.name && style.name.endsWith(`.css`)
    )

    styles
      .slice(0)
      .reverse()
      .forEach(style => {
        headComponents.unshift(
          <link
            data-identity={`gatsby-dev-css`}
            key={style.name}
            rel="stylesheet"
            type="text/css"
            href={`${__PATH_PREFIX__}/${style.name}`}
          />
        )
      })
    class RouteHandler extends React.Component {
      render() {
        const props = {
          ...this.props,
          ...pageData.result,
          serverData,
          params: {
            ...grabMatchParams(this.props.location.pathname),
            ...(pageData.result?.pageContext?.__params || {}),
          },
        }

        const pageElement = React.createElement(
          preferDefault(syncRequires.ssrComponents[componentChunkName]),
          props
        )

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

        return wrappedPage
      }
    }

    const routerElement =
      syncRequires.ssrComponents[componentChunkName] && !isClientOnlyPage ? (
        <ServerLocation url={`${__BASE_PATH__}${pagePath}`}>
          <Router id="gatsby-focus-wrapper" baseuri={__BASE_PATH__}>
            <RouteHandler path="/*" />
          </Router>
          <div {...RouteAnnouncerProps} />
        </ServerLocation>
      ) : null

    const bodyComponent = apiRunner(
      `wrapRootElement`,
      { element: routerElement, pathname: pagePath },
      routerElement,
      ({ result }) => {
        return { element: result, pathname: pagePath }
      }
    ).pop()

    // 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 {
        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,
    })

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

    return bodyHtml
  }

  const bodyStr = await generateBodyHTML()

  const htmlElement = React.createElement(Html, {
    ...bodyProps,
    body: bodyStr,
    headComponents: headComponents.concat([
      <script key={`io`} src="/socket.io/socket.io.js" />,
    ]),
    htmlAttributes,
    bodyAttributes,
    preBodyComponents,
    postBodyComponents: postBodyComponents.concat(
      [
        !BROWSER_ESM_ONLY && (
          <script key={`polyfill`} src="/polyfill.js" noModule={true} />
        ),
        <script key={`framework`} src="/framework.js" />,
        <script key={`commons`} src="/commons.js" />,
      ].filter(Boolean)
    ),
  })
  let htmlStr = renderToStaticMarkup(htmlElement)
  htmlStr = `<!DOCTYPE html>${htmlStr}`

  return htmlStr
}

export function getPageChunk({ componentChunkName }) {
  return syncRequires.ssrComponents[componentChunkName]
}
