/* global location */
import { useEffect, useState } from 'preact/hooks'
import getMeta from '../shared/get-meta.js'
import h from '../shared/h.js'
import { i18n } from '../shared/t.js'
import { Component } from './component.js'

const CLIENT_ERROR_CODE_LEVEL = 400
const FORWARDED_PARAMS = new Set(['q', 'tag', 'limit', 'offset', 'resource'])

// A very simplistic cache.
export const dataset = {}

async function doFetch (url, handleRedirections = false) {
  if (dataset[url]) {
    return dataset[url]
  }

  let res
  try {
    res = await fetch(url)
  } catch (error) {
    // Network error
    console.error(`Error when fetching: ${url}`)
    console.error(error)
    // Reload the page to follow e.g. potential external redirection blocked by CORS
    globalThis.location.reload()
    return {}
  }

  try {
    if (res?.ok && res.status < CLIENT_ERROR_CODE_LEVEL) {
      if (handleRedirections && res.redirected && res.headers.get('content-type')?.toLowerCase?.()?.startsWith?.('text/html')) {
        // Redirect to an HTML page, use backend handling
        globalThis.location.reload()
        return {}
      } else {
        // Regular model fetch: add response to the cache and return
        const obj = await res.json()
        dataset[url] = obj
        return obj
      }
    } else {
      // Response status error
      return null
    }
  } catch {
    // Other response error (e.g. malformed JSON)
    return null
  }
}

async function fetchModelJson () {
  // Fetch from .model.json.
  let data = await doFetch(getModelHref(), true)
  if (!data) {
    // Fetch from error's page model.json if no data.
    data = await doFetch(getModelHref(getMeta('error-url')))
  }

  return data
}

function getLocale (data) {
  const links = (data ?? {})?.model?.links || []
  return links.find?.(link => link?.name === 'locale')?.value
}


function fetchAcingUiUxI18nJson (pageData) {
  // Fetch only if a cached copy does not exist or if the locale of the site does not match
  // the locale of the dictionary (multilingual sites on the same domain).
  const locale = getLocale(pageData)
  const homeUrl = pageData?.model?.links?.find?.(link => link?.name === 'homeUrl')?.value
  if (locale && (Object.keys(i18n).length === 0 || i18n.locale !== locale)) {
    return doFetch(getI18nHref(homeUrl)).then(data => {
      if (data) {
        Object.assign(i18n, data)
        i18n.locale = locale
      }
    }).catch(() => null)
  }

  return null
}

function resolvePartialModels (componentModel) {
  if (!componentModel) {
    return null
  }

  if (componentModel?.ref) {
    return doFetch(componentModel.ref).then(partialData => {
      if (partialData) {
        Object.assign(componentModel, partialData)
        delete componentModel.ref
        const resolvedChildren = (componentModel?.children || []).map(child => resolvePartialModels(child))
        return Promise.allSettled(resolvedChildren)
      }

      // Skip resolving children if the partial model could not be fetched
      return null
    }).catch(() => null)
  } else {
    const resolvedChildren = (componentModel?.children || []).map(child => resolvePartialModels(child))
    return Promise.allSettled(resolvedChildren)
  }
}

function getModelHref (href = location) {
  const url = new URL(href, location)

  // Filter model params.
  for (const param of url.searchParams.keys()) {
    if (!FORWARDED_PARAMS.has(param)) {
      url.searchParams.delete(param)
    }
  }

  // Only use the "params" selector if there are search parameters to pass.
  const base = url.pathname.replace(/\.html(\/.*)?$/, '')
  url.pathname = url.searchParams.size > 0 ? `${base}.model.params.json` : `${base}.model.json`

  // Model requests are resolved via fetch, so hash is useless as it is not passed to the server.
  url.hash = ''
  return url.href
}

function getI18nHref (href = location) {
  const url = new URL(href, location)
  const base = url.origin + url.pathname.replace(/\.html(\/.*)?$/, '')
  return `${base}.acinguiuxi18n.json`
}

async function route () {
  // The i18n request needs to happen after model request for all the metadata
  // to be available (= Page model's links).
  const data = await fetchModelJson()

  await Promise.allSettled([resolvePartialModels(data), fetchAcingUiUxI18nJson(data)])
  return data
}

// Preact router component.
let lastUrl = new URL(location)
export function Router () {
  // Ability to skip page rendering entirely.
  if (new URLSearchParams(location.search).has('do-not-render')) {
    return null
  }

  const [data, setData] = useState(null)

  useEffect(() => {
    (async () => {
      // Route for the first time.
      const initialData = await route()
      initialData && setData(initialData)

      // Route for each popstate.
      globalThis.addEventListener('popstate', async () => {
        const newUrl = new URL(location)

        // Do not route if suffix or hash change.
        if (isHashOnly(lastUrl, newUrl) || isSuffixOnly(lastUrl, newUrl)) {
          return
        }

        const newData = await route()
        newData && setData(newData)

        // Reset scroll and focus only if the path is different.
        if (newUrl.pathname !== lastUrl.pathname) {
          document.activeElement.blur()
          document.body.scrollTo(0, 0)
        }

        lastUrl = newUrl
      })
    })()
  }, [])

  useEffect(() => {
    globalThis.dispatchEvent(new CustomEvent('route'))
  }, [lastUrl])

  return data && h(Component, { _componentModel: data, _model: data.model })
}

function isSuffixOnly (prevUrl, curUrl) {
  // Suffixes are applied only after the html extension.
  if (!curUrl.pathname.includes('.html')) {
    return false
  }

  const prevSuffix = prevUrl.pathname.split('.html').pop()
  const curSuffix = curUrl.pathname.split('.html').pop()
  return prevSuffix && prevUrl.href.replace(prevSuffix, '') === curUrl.href.replace(curSuffix, '')
}

function isHashOnly (prevUrl, curUrl) {
  return prevUrl.href.replace(prevUrl.hash, '') === curUrl.href.replace(curUrl.hash, '')
}
