import { join } from 'path'
import React from 'react'
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import send from 'send'
import generateETag from 'etag'
import fresh from 'fresh'
import {normalizePagePath, pageNotFoundError} from './require'
import { Router } from '../lib/router'
import { loadGetInitialProps, isResSent } from '../lib/utils'
import Head, { defaultHead } from '../lib/head'
import ErrorDebug from '../lib/error-debug'
import Loadable from '../lib/loadable'
import LoadableCapture from '../lib/loadable-capture'
import {promisify} from 'util'
import fs from 'fs'
import { SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH } from 'next-server/constants'

const access = promisify(fs.access)

// Based on https://github.com/jamiebuilds/react-loadable/pull/132
function getDynamicImportBundles (manifest, moduleIds) {
  return moduleIds.reduce((bundles, moduleId) => {
    if (typeof manifest[moduleId] === 'undefined') {
      return bundles
    }

    return bundles.concat(manifest[moduleId])
  }, [])
}

// since send doesn't support wasm yet
send.mime.define({ 'application/wasm': ['wasm'] })

export async function render (req, res, pathname, query, opts) {
  const html = await renderToHTML(req, res, pathname, query, opts)
  sendHTML(req, res, html, req.method, opts)
}

export function renderToHTML (req, res, pathname, query, opts) {
  return doRender(req, res, pathname, query, opts)
}

export async function renderError (err, req, res, pathname, query, opts) {
  const html = await renderErrorToHTML(err, req, res, query, opts)
  sendHTML(req, res, html, req.method, opts)
}

export function renderErrorToHTML (err, req, res, pathname, query, opts = {}) {
  return doRender(req, res, pathname, query, { ...opts, err, page: '/_error' })
}

async function doRender (req, res, pathname, query, {
  err,
  page,
  buildId,
  hotReloader,
  assetPrefix,
  runtimeConfig,
  distDir,
  dir,
  dev = false,
  staticMarkup = false,
  nextExport
} = {}) {
  page = page || pathname
  let normalizedPagePath
  try {
    normalizedPagePath = normalizePagePath(page)
  } catch (err) {
    console.error(err)
    throw pageNotFoundError(page)
  }

  // In dev mode we use on demand entries to compile the page before rendering
  if (hotReloader) {
    await hotReloader.ensurePage(page)
  }

  const pagePath = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, buildId, 'pages', normalizedPagePath)

  try {
    await access(`${pagePath}.js`, (fs.constants || fs).R_OK)
  } catch (err) {
    throw pageNotFoundError(page)
  }

  const documentPath = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, buildId, 'pages', '_document')
  const appPath = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, buildId, 'pages', '_app')

  let Document = require(documentPath)
  let App = require(appPath)
  let Component = require(pagePath)

  Document = Document.default || Document
  App = App.default || App
  Component = Component.default || Component

  const reactLoadableManifest = require(pagePath + '-loadable.json')

  if (typeof Component !== 'function') {
    throw new Error(`The default export is not a React Component in page: "${page}"`)
  }

  const asPath = req.url
  const ctx = { err, req, res, pathname, query, asPath }
  const router = new Router(pathname, query, asPath)
  const props = await loadGetInitialProps(App, {Component, router, ctx})
  const files = [`static/${buildId}/main.js`]

  // the response might be finshed on the getinitialprops call
  if (isResSent(res)) return

  let reactLoadableModules = []
  const renderPage = (options = Page => Page) => {
    let EnhancedApp = App
    let EnhancedComponent = Component

    // For backwards compatibility
    if (typeof options === 'function') {
      EnhancedComponent = options(Component)
    } else if (typeof options === 'object') {
      if (options.enhanceApp) {
        EnhancedApp = options.enhanceApp(App)
      }
      if (options.enhanceComponent) {
        EnhancedComponent = options.enhanceComponent(Component)
      }
    }

    const app = <LoadableCapture report={moduleName => reactLoadableModules.push(moduleName)}>
      <EnhancedApp {...{
        Component: EnhancedComponent,
        router,
        ...props
      }} />
    </LoadableCapture>

    const render = staticMarkup ? renderToStaticMarkup : renderToString

    let html
    let head

    try {
      if (err && dev) {
        html = render(<ErrorDebug error={err} />)
      } else if (err) {
        html = render(app)
      } else {
        html = render(app)
      }
    } finally {
      head = Head.rewind() || defaultHead()
    }

    return { html, head }
  }

  await Loadable.preloadAll() // Make sure all dynamic imports are loaded

  const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })
  // const dynamicImports = [...(new Set(getDynamicImportBundles(reactLoadableManifest, reactLoadableModules)))]
  const dynamicImports = []
  const dynamicImportsIds = reactLoadableModules

  if (isResSent(res)) return

  if (!Document.prototype || !Document.prototype.isReactComponent) throw new Error('_document.js is not exporting a React component')
  const doc = <Document {...{
    __NEXT_DATA__: {
      props, // The result of getInitialProps
      page, // The rendered page
      pathname, // The requested path
      query, // querystring parsed / passed by the user
      buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
      assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
      runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
      nextExport, // If this is a page exported by `next export`
      dynamicIds: dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
      err: (err) ? serializeError(dev, err) : undefined // Error if one happened, otherwise don't sent in the resulting HTML
    },
    dev,
    dir,
    staticMarkup,
    // buildManifest,
    files,
    dynamicImports,
    assetPrefix, // We always pass assetPrefix as a top level property since _document needs it to render, even though the client side might not need it
    ...docProps
  }} />

  return '<!DOCTYPE html>' + renderToStaticMarkup(doc)
}

export async function renderScriptError (req, res, page, error) {
  // Asks CDNs and others to not to cache the errored page
  res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate')

  if (error.code === 'ENOENT' || error.message === 'INVALID_BUILD_ID') {
    res.statusCode = 404
    res.end('404 - Not Found')
    return
  }

  console.error(error.stack)
  res.statusCode = 500
  res.end('500 - Internal Error')
}

export function sendHTML (req, res, html, method, { dev, generateEtags }) {
  if (isResSent(res)) return
  const etag = generateEtags && generateETag(html)

  if (fresh(req.headers, { etag })) {
    res.statusCode = 304
    res.end()
    return
  }

  if (dev) {
    // In dev, we should not cache pages for any reason.
    // That's why we do this.
    res.setHeader('Cache-Control', 'no-store, must-revalidate')
  }

  if (etag) {
    res.setHeader('ETag', etag)
  }

  if (!res.getHeader('Content-Type')) {
    res.setHeader('Content-Type', 'text/html; charset=utf-8')
  }
  res.setHeader('Content-Length', Buffer.byteLength(html))
  res.end(method === 'HEAD' ? null : html)
}

function errorToJSON (err) {
  const { name, message, stack } = err
  const json = { name, message, stack }

  if (err.module) {
    // rawRequest contains the filename of the module which has the error.
    const { rawRequest } = err.module
    json.module = { rawRequest }
  }

  return json
}

function serializeError (dev, err) {
  if (dev) {
    return errorToJSON(err)
  }

  return { message: '500 - Internal Server Error.' }
}

export function serveStatic (req, res, path) {
  return new Promise((resolve, reject) => {
    send(req, path)
      .on('directory', () => {
      // We don't allow directories to be read.
        const err = new Error('No directory access')
        err.code = 'ENOENT'
        reject(err)
      })
      .on('error', reject)
      .pipe(res)
      .on('finish', resolve)
  })
}
