import { isServer } from '@tanstack/router-core/isServer'
import { last } from './utils'
import {
  SEGMENT_TYPE_OPTIONAL_PARAM,
  SEGMENT_TYPE_PARAM,
  SEGMENT_TYPE_PATHNAME,
  SEGMENT_TYPE_WILDCARD,
  parseSegment,
} from './new-process-route-tree'
import type { LRUCache } from './lru-cache'

/** Join path segments, cleaning duplicate slashes between parts. */
export function joinPaths(paths: Array<string | undefined>) {
  return cleanPath(
    paths
      .filter((val) => {
        return val !== undefined
      })
      .join('/'),
  )
}

/** Remove repeated slashes from a path string. */
export function cleanPath(path: string) {
  // remove double slashes
  return path.replace(/\/{2,}/g, '/')
}

/** Trim leading slashes (except preserving root '/'). */
export function trimPathLeft(path: string) {
  return path === '/' ? path : path.replace(/^\/{1,}/, '')
}

/** Trim trailing slashes (except preserving root '/'). */
export function trimPathRight(path: string) {
  const len = path.length
  return len > 1 && path[len - 1] === '/' ? path.replace(/\/{1,}$/, '') : path
}

/** Trim both leading and trailing slashes. */
export function trimPath(path: string) {
  return trimPathRight(trimPathLeft(path))
}

/** Remove a trailing slash from value when appropriate for comparisons. */
export function removeTrailingSlash(value: string, basepath: string): string {
  if (value?.endsWith('/') && value !== '/' && value !== `${basepath}/`) {
    return value.slice(0, -1)
  }
  return value
}

// intended to only compare path name
// see the usage in the isActive under useLinkProps
// /sample/path1 = /sample/path1/
// /sample/path1/some <> /sample/path1
/**
 * Compare two pathnames for exact equality after normalizing trailing slashes
 * relative to the provided `basepath`.
 */
export function exactPathTest(
  pathName1: string,
  pathName2: string,
  basepath: string,
): boolean {
  return (
    removeTrailingSlash(pathName1, basepath) ===
    removeTrailingSlash(pathName2, basepath)
  )
}

// When resolving relative paths, we treat all paths as if they are trailing slash
// documents. All trailing slashes are removed after the path is resolved.
// Here are a few examples:
//
// /a/b/c + ./d = /a/b/c/d
// /a/b/c + ../d = /a/b/d
// /a/b/c + ./d/ = /a/b/c/d
// /a/b/c + ../d/ = /a/b/d
// /a/b/c + ./ = /a/b/c
//
// Absolute paths that start with `/` short circuit the resolution process to the root
// path.
//
// Here are some examples:
//
// /a/b/c + /d = /d
// /a/b/c + /d/ = /d
// /a/b/c + / = /
//
// Non-.-prefixed paths are still treated as relative paths, resolved like `./`
//
// Here are some examples:
//
// /a/b/c + d = /a/b/c/d
// /a/b/c + d/ = /a/b/c/d
// /a/b/c + d/e = /a/b/c/d/e
interface ResolvePathOptions {
  base: string
  to: string
  trailingSlash?: 'always' | 'never' | 'preserve'
  cache?: LRUCache<string, string>
}

/**
 * Resolve a destination path against a base, honoring trailing-slash policy
 * and supporting relative segments (`.`/`..`) and absolute `to` values.
 */
export function resolvePath({
  base,
  to,
  trailingSlash = 'never',
  cache,
}: ResolvePathOptions) {
  const isAbsolute = to.startsWith('/')
  const isBase = !isAbsolute && to === '.'

  let key
  if (cache) {
    // `trailingSlash` is static per router, so it doesn't need to be part of the cache key
    key = isAbsolute ? to : isBase ? base : base + '\0' + to
    const cached = cache.get(key)
    if (cached) return cached
  }

  let baseSegments: Array<string>
  if (isBase) {
    baseSegments = base.split('/')
  } else if (isAbsolute) {
    baseSegments = to.split('/')
  } else {
    baseSegments = base.split('/')
    while (baseSegments.length > 1 && last(baseSegments) === '') {
      baseSegments.pop()
    }

    const toSegments = to.split('/')
    for (let index = 0, length = toSegments.length; index < length; index++) {
      const value = toSegments[index]!
      if (value === '') {
        if (!index) {
          // Leading slash
          baseSegments = [value]
        } else if (index === length - 1) {
          // Trailing Slash
          baseSegments.push(value)
        } else {
          // ignore inter-slashes
        }
      } else if (value === '..') {
        baseSegments.pop()
      } else if (value === '.') {
        // ignore
      } else {
        baseSegments.push(value)
      }
    }
  }

  if (baseSegments.length > 1) {
    if (last(baseSegments) === '') {
      if (trailingSlash === 'never') {
        baseSegments.pop()
      }
    } else if (trailingSlash === 'always') {
      baseSegments.push('')
    }
  }

  let segment
  let joined = ''
  for (let i = 0; i < baseSegments.length; i++) {
    if (i > 0) joined += '/'
    const part = baseSegments[i]!
    if (!part) continue
    segment = parseSegment(part, 0, segment)
    const kind = segment[0]
    if (kind === SEGMENT_TYPE_PATHNAME) {
      joined += part
      continue
    }
    const end = segment[5]
    const prefix = part.substring(0, segment[1])
    const suffix = part.substring(segment[4], end)
    const value = part.substring(segment[2], segment[3])
    if (kind === SEGMENT_TYPE_PARAM) {
      joined += prefix || suffix ? `${prefix}{$${value}}${suffix}` : `$${value}`
    } else if (kind === SEGMENT_TYPE_WILDCARD) {
      joined += prefix || suffix ? `${prefix}{$}${suffix}` : '$'
    } else {
      // SEGMENT_TYPE_OPTIONAL_PARAM
      joined += `${prefix}{-$${value}}${suffix}`
    }
  }
  joined = cleanPath(joined)
  const result = joined || '/'
  if (key && cache) cache.set(key, result)
  return result
}

/**
 * Create a pre-compiled decode config from allowed characters.
 * This should be called once at router initialization.
 */
export function compileDecodeCharMap(
  pathParamsAllowedCharacters: ReadonlyArray<string>,
) {
  const charMap = new Map(
    pathParamsAllowedCharacters.map((char) => [encodeURIComponent(char), char]),
  )
  // Escape special regex characters and join with |
  const pattern = Array.from(charMap.keys())
    .map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
    .join('|')
  const regex = new RegExp(pattern, 'g')
  return (encoded: string) =>
    encoded.replace(regex, (match) => charMap.get(match) ?? match)
}

interface InterpolatePathOptions {
  path?: string
  params: Record<string, unknown>
  /**
   * A function that decodes a path parameter value.
   * Obtained from `compileDecodeCharMap(pathParamsAllowedCharacters)`.
   */
  decoder?: (encoded: string) => string
  /**
   * @internal
   * For testing only, in development mode we use the router.isServer value
   */
  server?: boolean
}

type InterPolatePathResult = {
  interpolatedPath: string
  usedParams: Record<string, unknown>
  isMissingParams: boolean // true if any params were not available when being looked up in the params object
}

function encodeParam(
  key: string,
  params: InterpolatePathOptions['params'],
  decoder: InterpolatePathOptions['decoder'],
): any {
  const value = params[key]
  if (typeof value !== 'string') return value

  if (key === '_splat') {
    // Early return if value only contains URL-safe characters (performance optimization)
    if (/^[a-zA-Z0-9\-._~!/]*$/.test(value)) return value
    // the splat/catch-all routes shouldn't have the '/' encoded out
    // Use encodeURIComponent for each segment to properly encode spaces,
    // plus signs, and other special characters that encodeURI leaves unencoded
    return value
      .split('/')
      .map((segment) => encodePathParam(segment, decoder))
      .join('/')
  } else {
    return encodePathParam(value, decoder)
  }
}

/**
 * Interpolate params and wildcards into a route path template.
 *
 * - Encodes params safely (configurable allowed characters)
 * - Supports `{-$optional}` segments, `{prefix{$id}suffix}` and `{$}` wildcards
 */
export function interpolatePath({
  path,
  params,
  decoder,
  // `server` is marked @internal and stripped from .d.ts by `stripInternal`.
  // We avoid destructuring it in the function signature so the emitted
  // declaration doesn't reference a property that no longer exists.
  ...rest
}: InterpolatePathOptions): InterPolatePathResult {
  // Tracking if any params are missing in the `params` object
  // when interpolating the path
  let isMissingParams = false
  const usedParams: Record<string, unknown> = Object.create(null)

  if (!path || path === '/')
    return { interpolatedPath: '/', usedParams, isMissingParams }
  if (!path.includes('$'))
    return { interpolatedPath: path, usedParams, isMissingParams }

  if (isServer ?? rest.server) {
    // Fast path for common templates like `/posts/$id` or `/files/$`.
    // Braced segments (`{...}`) are more complex (prefix/suffix/optional) and are
    // handled by the general parser below.
    if (path.indexOf('{') === -1) {
      const length = path.length
      let cursor = 0
      let joined = ''

      while (cursor < length) {
        // Skip slashes between segments. '/' code is 47
        while (cursor < length && path.charCodeAt(cursor) === 47) cursor++
        if (cursor >= length) break

        const start = cursor
        let end = path.indexOf('/', cursor)
        if (end === -1) end = length
        cursor = end

        const part = path.substring(start, end)
        if (!part) continue

        // `$id` or `$` (splat). '$' code is 36
        if (part.charCodeAt(0) === 36) {
          if (part.length === 1) {
            const splat = params._splat
            usedParams._splat = splat
            // TODO: Deprecate *
            usedParams['*'] = splat

            if (!splat) {
              isMissingParams = true
              continue
            }

            const value = encodeParam('_splat', params, decoder)
            joined += '/' + value
          } else {
            const key = part.substring(1)
            if (!isMissingParams && !(key in params)) {
              isMissingParams = true
            }
            usedParams[key] = params[key]

            const value = encodeParam(key, params, decoder) ?? 'undefined'
            joined += '/' + value
          }
        } else {
          joined += '/' + part
        }
      }

      if (path.endsWith('/')) joined += '/'

      const interpolatedPath = joined || '/'
      return { usedParams, interpolatedPath, isMissingParams }
    }
  }

  const length = path.length
  let cursor = 0
  let segment
  let joined = ''
  while (cursor < length) {
    const start = cursor
    segment = parseSegment(path, start, segment)
    const end = segment[5]
    cursor = end + 1

    if (start === end) continue

    const kind = segment[0]

    if (kind === SEGMENT_TYPE_PATHNAME) {
      joined += '/' + path.substring(start, end)
      continue
    }

    if (kind === SEGMENT_TYPE_WILDCARD) {
      const splat = params._splat
      usedParams._splat = splat
      // TODO: Deprecate *
      usedParams['*'] = splat

      const prefix = path.substring(start, segment[1])
      const suffix = path.substring(segment[4], end)

      // Check if _splat parameter is missing. _splat could be missing if undefined or an empty string or some other falsy value.
      if (!splat) {
        isMissingParams = true
        // For missing splat parameters, just return the prefix and suffix without the wildcard
        // If there is a prefix or suffix, return them joined, otherwise omit the segment
        if (prefix || suffix) {
          joined += '/' + prefix + suffix
        }
        continue
      }

      const value = encodeParam('_splat', params, decoder)
      joined += '/' + prefix + value + suffix
      continue
    }

    if (kind === SEGMENT_TYPE_PARAM) {
      const key = path.substring(segment[2], segment[3])
      if (!isMissingParams && !(key in params)) {
        isMissingParams = true
      }
      usedParams[key] = params[key]

      const prefix = path.substring(start, segment[1])
      const suffix = path.substring(segment[4], end)
      const value = encodeParam(key, params, decoder) ?? 'undefined'
      joined += '/' + prefix + value + suffix
      continue
    }

    if (kind === SEGMENT_TYPE_OPTIONAL_PARAM) {
      const key = path.substring(segment[2], segment[3])
      const valueRaw = params[key]

      // Check if optional parameter is missing or undefined
      if (valueRaw == null) continue

      usedParams[key] = valueRaw

      const prefix = path.substring(start, segment[1])
      const suffix = path.substring(segment[4], end)
      const value = encodeParam(key, params, decoder) ?? ''
      joined += '/' + prefix + value + suffix
      continue
    }
  }

  if (path.endsWith('/')) joined += '/'

  const interpolatedPath = joined || '/'

  return { usedParams, interpolatedPath, isMissingParams }
}

function encodePathParam(
  value: string,
  decoder?: InterpolatePathOptions['decoder'],
) {
  const encoded = encodeURIComponent(value)
  return decoder?.(encoded) ?? encoded
}
