import { normalizePathTrailingSlash } from 'next/dist/client/normalize-trailing-slash'
import type { Key } from 'path-to-regexp'
import { compile as compilePath, parse as parsePath } from 'path-to-regexp'
import { parse as parseQuery, stringify as stringifyQuery, ParsedUrlQuery } from 'querystring'
import { format as formatUrl, parse as parseUrl, UrlObject } from 'url'

import { getDynamicPathPartKey, getSpreadFilepathPartKey, ignoreSegmentPathRegex } from '../shared/regex'
import type { TRouteBranch, Url } from '../types'
import { getNtrData } from './ntrData'
import { removeLangPrefix } from './removeLangPrefix'

type Options<F extends 'string' | 'object' = 'string' | 'object'> = {
  format?: F
  withoutLangPrefix?: boolean
}

/** Get children + (grand)children of children whose path must be ignord (path === '.' or path === `.(${pathRegex})`) */
const getAllCandidates = (lang: string, children?: TRouteBranch[]): TRouteBranch[] =>
  children
    ? children.reduce((acc, child) => {
        const path = child.paths[lang] || child.paths.default
        return [...acc, ...(path === '' ? getAllCandidates(lang, child.children) : [child])]
      }, [] as TRouteBranch[])
    : []

/**
 * Recursively translate paths from file path, and extract parameters
 */
const translatePathParts = ({
  locale,
  pathParts,
  routeBranch,
  query,
}: {
  locale: string
  /** Can be a bare name or a dynamic value */
  pathParts: string[]
  routeBranch: TRouteBranch
  query?: ParsedUrlQuery
}): { augmentedQuery?: ParsedUrlQuery; translatedPathParts: string[] } => {
  const { children } = routeBranch

  if (!Array.isArray(pathParts)) {
    throw new Error('Wrong pathParts argument in translatePathParts')
  }

  if (pathParts.length === 0) {
    return { translatedPathParts: [], augmentedQuery: query }
  }

  const pathPart = pathParts[0]
  const nextPathParts = pathParts.slice(1)

  if (!pathPart) {
    return translatePathParts({ locale, pathParts: nextPathParts, routeBranch, query })
  }

  const candidates = getAllCandidates(locale, children).filter((child) =>
    pathParts.length === 1 // Last path part
      ? !child.children ||
        child.children.some((grandChild) => grandChild.name === 'index' || /\[\[\.{0,3}\w+\]\]/.exec(grandChild.name))
      : !!child.children || /\[\[?\.{3}\w+\]?\]/.exec(child.name),
  )

  let currentQuery = query

  /**
   * If defined: pathPart is a route segment name that should be translated.
   * If dynamic, the value should already be contained in the query.
   */
  let childRouteBranch = candidates.find(({ name }) => pathPart === name)

  if (!childRouteBranch) {
    // If not defined: pathPart is either a dynamic value either a wrong path.

    childRouteBranch = candidates.find((candidate) => getDynamicPathPartKey(candidate.name))
    if (childRouteBranch) {
      // Single dynamic route segment value => store it in the query
      currentQuery = {
        ...currentQuery,
        [childRouteBranch.name.replace(/\[|\]|\./g, '')]: pathPart,
      }
    } else {
      childRouteBranch = candidates.find((candidate) => getSpreadFilepathPartKey(candidate.name))
      if (childRouteBranch) {
        // Catch all route => store it in the query, then return the current data.
        currentQuery = {
          ...currentQuery,
          [childRouteBranch.name.replace(/\[|\]|\./g, '')]: pathParts,
        }
        return {
          translatedPathParts: [childRouteBranch.paths[locale] || childRouteBranch.paths.default],
          augmentedQuery: currentQuery,
        }
      }
      // No route match => return the remaining path as is
      return { translatedPathParts: pathParts, augmentedQuery: query }
    }
  }

  // Get the descendants translated path parts and query values
  const { augmentedQuery, translatedPathParts: nextTranslatedPathsParts } = childRouteBranch?.children
    ? translatePathParts({ locale, pathParts: nextPathParts, routeBranch: childRouteBranch, query: currentQuery })
    : { augmentedQuery: currentQuery, translatedPathParts: [] }

  const translatedPathPart = childRouteBranch.paths[locale] || childRouteBranch.paths.default

  return {
    translatedPathParts: [
      ...(ignoreSegmentPathRegex.test(translatedPathPart) ? [] : [translatedPathPart]),
      ...(nextTranslatedPathsParts || []),
    ],
    augmentedQuery,
  }
}

/**
 * Translate path into locale
 *
 * @param url string url or UrlObject
 * @param locale string
 * @param options (optional)
 * @param options.format `'string'` or `'object'`
 * @return string if `options.format === 'string'`,
 * UrlObject if `options.format === 'object'`,
 * same type as url if options.format is not defined
 */
export function translatePath<U extends string | UrlObject, F extends 'string' | 'object'>(
  url: U,
  locale: string,
  options?: Options<F>,
): 'string' | 'object' extends F
  ? U extends string
    ? string
    : U extends UrlObject
    ? UrlObject
    : Url
  : F extends 'string'
  ? string
  : UrlObject

export function translatePath(url: Url, locale: string, { format }: Options = {}): Url {
  const { routesTree } = getNtrData()
  const returnFormat = format || typeof url
  const urlObject = typeof url === 'object' ? (url as UrlObject) : parseUrl(url, true)
  const { pathname, query, hash } = urlObject

  if (!pathname || !locale) {
    return returnFormat === 'object' ? url : formatUrl(url)
  }

  const pathParts = removeLangPrefix(pathname, true)

  const { translatedPathParts, augmentedQuery = {} } = translatePathParts({
    locale,
    pathParts,
    query: parseQuery(typeof query === 'string' ? query : stringifyQuery(query || {})),
    routeBranch: routesTree,
  })
  const path = translatedPathParts.join('/')
  const compiledPath = compilePath(path, { validate: false })(augmentedQuery)
  const paramNames = (parsePath(path).filter((token) => typeof token === 'object') as Key[]).map((token) => token.name)
  const remainingQuery = Object.keys(augmentedQuery).reduce(
    (acc, key) => ({
      ...acc,
      ...(paramNames.includes(key)
        ? {}
        : { [key]: (typeof query === 'object' && query?.[key]) || augmentedQuery[key] }),
    }),
    {},
  )

  const translatedPathname = `${routesTree.paths[locale] ? `/${routesTree.paths[locale]}` : ''}/${compiledPath}`

  const translatedUrlObject = {
    ...urlObject,
    hash,
    pathname: translatedPathname,
    query: remainingQuery,
  }

  return returnFormat === 'object' ? translatedUrlObject : formatUrl(translatedUrlObject)
}

export type TTranslateUrl = typeof translatePath

/**
 * Translate url into locale
 *
 * Will soon be deprecated in favor of fileUrlToUrl and urlToFileUrl
 *
 * @param url string url or UrlObject
 * @param locale string
 * @param options (optional)
 * @param options.format `'string'` or `'object'`
 * @return string if `options.format === 'string'`,
 * UrlObject if `options.format === 'object'`,
 * same type as url if options.format is not defined
 */
export const translateUrl: TTranslateUrl = ((url, locale, options) => {
  const { defaultLocale } = getNtrData()

  // Handle external urls
  const parsedUrl: UrlObject = typeof url === 'string' ? parseUrl(url) : url
  if (parsedUrl.host) {
    if (typeof window === 'undefined' || parsedUrl.host !== parseUrl(window.location.href).host) {
      return url
    }
  }

  const translatedPath = translatePath(url, locale, options)

  if (typeof translatedPath === 'object') {
    return translatedPath
  }

  const prefix = locale === defaultLocale || options?.withoutLangPrefix ? '' : `/${locale}`

  return normalizePathTrailingSlash(prefix + translatedPath)
}) as typeof translatePath
