import * as Solid from 'solid-js'

import {
  deepEqual,
  exactPathTest,
  functionalUpdate,
  hasKeys,
  isDangerousProtocol,
  preloadWarning,
  removeTrailingSlash,
} from '@tanstack/router-core'

import { isServer } from '@tanstack/router-core/isServer'
import { Dynamic } from '@solidjs/web'
import { useRouter } from './useRouter'

import { useIntersectionObserver } from './utils'

import { useHydrated } from './ClientOnly'
import type {
  AnyRouter,
  Constrain,
  LinkOptions,
  RegisteredRouter,
  RoutePaths,
} from '@tanstack/router-core'
import type {
  ValidateLinkOptions,
  ValidateLinkOptionsArray,
} from './typePrimitives'
import type { ComponentProps, JSX, ValidComponent } from '@solidjs/web'

function mergeRefs<T>(...refs: Array<unknown>): (el: T) => void {
  const setRef = (ref: unknown, el: T) => {
    if (typeof ref === 'function') {
      ref(el)
    } else if (Array.isArray(ref)) {
      for (const nestedRef of ref) {
        setRef(nestedRef, el)
      }
    }
  }

  return (el: T) => {
    for (const ref of refs) {
      setRef(ref, el)
    }
  }
}

function splitProps<T extends Record<string, any>, TKey extends keyof T>(
  props: T,
  keys: ReadonlyArray<TKey>,
): [Pick<T, TKey>, Omit<T, TKey>] {
  const _local = {} as Pick<T, TKey>
  const _rest = {} as Omit<T, TKey>

  // A safe way to polyfill splitProps if native getter copy is too complex
  // is just to return [props, Solid.omit(props, keys)] but it modifies typing.
  // Actually, Solid.omit exists!
  // Note: Solid.omit uses rest params (...keys), so we must spread the array.
  return [props as any, Solid.omit(props, ...(keys as any)) as any]
}
const timeoutMap = new WeakMap<object, ReturnType<typeof setTimeout>>()
const cancelPreload = (eventTarget: object) => {
  clearTimeout(timeoutMap.get(eventTarget))
  timeoutMap.delete(eventTarget)
}

export function useLinkProps<
  TRouter extends AnyRouter = RegisteredRouter,
  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
  TTo extends string = '',
  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
  TMaskTo extends string = '',
>(
  options: UseLinkPropsOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
): ComponentProps<'a'> {
  const router = useRouter()
  const shouldHydrateHash = !isServer && !!router.options.ssr
  const hasHydrated = useHydrated()

  let hasRenderFetched = false

  // Defaults are resolved through accessors at the use sites instead of
  // merging them into the props. Every merge/omit proxy layered here gets
  // re-enumerated by spread() on each navigation, and V8 dispatches proxy
  // traps in native runtime code — keeping this path proxy-free is what
  // keeps Link updates cheap.
  const local = options
  const activeProps = () => local.activeProps ?? STATIC_ACTIVE_PROPS_GET
  const inactiveProps = () => local.inactiveProps ?? STATIC_INACTIVE_PROPS_GET

  const propsSafeToSpread = Solid.omit(
    options as Record<string, any>,
    'activeProps',
    'inactiveProps',
    'activeOptions',
    'to',
    'preload',
    'preloadDelay',
    'preloadIntentProximity',
    'hashScrollIntoView',
    'replace',
    'startTransition',
    'resetScroll',
    'viewTransition',
    'target',
    'disabled',
    'style',
    'class',
    'onClick',
    'onBlur',
    'onFocus',
    'onMouseEnter',
    'onMouseLeave',
    'onMouseOver',
    'onMouseOut',
    'onTouchStart',
    'ignoreBlocker',
    'params',
    'search',
    'hash',
    'state',
    'mask',
    'reloadDocument',
    'unsafeRelative',
    'from',
  )

  const currentLocation = Solid.createMemo(() => router.stores.location.get(), {
    equals: (prev, next) => prev.href === next.href,
  })

  const _options = () => options

  const next = Solid.createMemo(
    () => {
      // Rebuild when inherited search/hash or the current route context changes.
      const _fromLocation = currentLocation()
      const options = { _fromLocation, ..._options() } as any
      // untrack because router-core will also access stores, which are signals in solid
      return Solid.untrack(() => router.buildLocation(options))
    },
    {
      lazy: true,
      // Navigations usually leave most links' built locations unchanged;
      // comparing hrefs lets downstream memos (href, isActive) skip work.
      equals: (prev, next) =>
        prev.href === next.href &&
        prev.external === next.external &&
        prev.maskedLocation?.href === next.maskedLocation?.href,
    },
  )

  const hrefOption = Solid.createMemo(
    () => {
      if (_options().disabled) return undefined
      // Use publicHref - it contains the correct href for display
      // When a rewrite changes the origin, publicHref is the full URL
      // Otherwise it's the origin-stripped path
      // This avoids constructing URL objects in the hot path
      const location = next().maskedLocation ?? next()
      const publicHref = location.publicHref
      const external = location.external

      if (external) {
        return { href: publicHref, external: true }
      }

      return {
        href: router.history.createHref(publicHref) || '/',
        external: false,
      }
    },
    { lazy: true },
  )

  const externalLink = Solid.createMemo(
    () => {
      const _href = hrefOption()
      if (_href?.external) {
        // Block dangerous protocols for external links
        if (isDangerousProtocol(_href.href, router.protocolAllowlist)) {
          if (process.env.NODE_ENV !== 'production') {
            console.warn(`Blocked Link with dangerous protocol: ${_href.href}`)
          }
          return undefined
        }
        return _href.href
      }
      const to = _options().to
      const safeInternal = isSafeInternal(to)
      if (safeInternal) return undefined
      if (typeof to !== 'string' || to.indexOf(':') === -1) return undefined
      try {
        new URL(to as any)
        // Block dangerous protocols like javascript:, blob:, data:
        if (isDangerousProtocol(to, router.protocolAllowlist)) {
          if (process.env.NODE_ENV !== 'production') {
            console.warn(`Blocked Link with dangerous protocol: ${to}`)
          }
          return undefined
        }
        return to
      } catch {}
      return undefined
    },
    { lazy: true },
  )

  const preload = Solid.createMemo(
    () => {
      if (_options().reloadDocument || externalLink() || local.disabled) {
        return false
      }
      return local.preload ?? router.options.defaultPreload
    },
    { lazy: true },
  )
  const preloadDelay = () =>
    local.preloadDelay ?? router.options.defaultPreloadDelay ?? 0

  const isActive = Solid.createMemo(
    () => {
      if (externalLink()) return false
      const activeOptions = local.activeOptions
      const current = currentLocation()
      const nextLocation = next()

      if (activeOptions?.exact) {
        const testExact = exactPathTest(
          current.pathname,
          nextLocation.pathname,
          router.basepath,
        )
        if (!testExact) {
          return false
        }
      } else {
        const currentPath = removeTrailingSlash(
          current.pathname,
          router.basepath,
        )
        const nextPath = removeTrailingSlash(
          nextLocation.pathname,
          router.basepath,
        )

        const pathIsFuzzyEqual =
          currentPath.startsWith(nextPath) &&
          (currentPath.length === nextPath.length ||
            currentPath[nextPath.length] === '/')
        if (!pathIsFuzzyEqual) {
          return false
        }
      }

      if (activeOptions?.includeSearch ?? true) {
        const searchTest = deepEqual(current.search, nextLocation.search, {
          partial: !activeOptions?.exact,
          ignoreUndefined: !activeOptions?.explicitUndefined,
        })
        if (!searchTest) {
          return false
        }
      }

      if (activeOptions?.includeHash) {
        const currentHash =
          shouldHydrateHash && !hasHydrated() ? '' : current.hash
        return currentHash === nextLocation.hash
      }
      return true
    },
    { lazy: true },
  )

  const doPreload = () =>
    router
      .preloadRoute({ ...options, _builtLocation: next() } as any)
      .catch((err: any) => {
        console.warn(err)
        console.warn(preloadWarning)
      })

  const [ref, setRefSignal] = Solid.createSignal<Element | null>(null)

  const setRef = (el: Element | null) => {
    Solid.runWithOwner(null, () => {
      setRefSignal(el)
    })
  }

  const enqueuePreload = (
    e?: MouseEvent | FocusEvent | IntersectionObserverEntry,
  ) => {
    if (!e) {
      cancelPreload(ref)
      return
    }

    if (
      !(
        (e as IntersectionObserverEntry).isIntersecting ??
        preload() === 'intent'
      )
    ) {
      if ((e as IntersectionObserverEntry).isIntersecting === false) {
        cancelPreload(ref)
      }
      return
    }

    if (!preloadDelay()) {
      doPreload()
      return
    }

    if (!timeoutMap.has(ref)) {
      timeoutMap.set(
        ref,
        setTimeout(() => {
          timeoutMap.delete(ref)
          doPreload()
        }, preloadDelay()),
      )
    }
  }

  useIntersectionObserver(ref, enqueuePreload, () => preload() !== 'viewport')

  Solid.createEffect(preload, (preloadValue) => {
    if (hasRenderFetched) {
      return
    }
    if (preloadValue === 'render') {
      Solid.untrack(() => doPreload())
      hasRenderFetched = true
    }
  })

  if (Solid.untrack(externalLink)) {
    const externalHref = Solid.untrack(externalLink)
    return Solid.merge(
      propsSafeToSpread,
      {
        ref: mergeRefs(setRef, options.ref),
        href: externalHref,
      },
      splitProps(local, [
        'target',
        'disabled',
        'style',
        'class',
        'onClick',
        'onBlur',
        'onFocus',
        'onMouseEnter',
        'onMouseLeave',
        'onMouseOut',
        'onMouseOver',
        'onTouchStart',
      ])[0],
    ) as any
  }

  // The click handler
  const handleClick = (e: MouseEvent) => {
    // Check actual element's target attribute as fallback
    const elementTarget = (
      e.currentTarget as HTMLAnchorElement | SVGAElement
    ).getAttribute('target')
    const effectiveTarget =
      local.target !== undefined ? local.target : elementTarget

    if (
      !local.disabled &&
      !isCtrlEvent(e) &&
      !e.defaultPrevented &&
      (!effectiveTarget || effectiveTarget === '_self') &&
      e.button === 0
    ) {
      e.preventDefault()

      // All is well? Navigate!
      // N.B. we don't call `router.commitLocation(next) here because we want to run `validateSearch` before committing
      router.navigate({
        ...options,
        replace: local.replace,
        resetScroll: local.resetScroll,
        hashScrollIntoView: local.hashScrollIntoView,
        startTransition: local.startTransition,
        viewTransition: local.viewTransition,
        ignoreBlocker: local.ignoreBlocker,
      })
    }
  }

  const handleTouchStart = () => {
    if (preload() !== 'intent') return
    doPreload()
  }

  const handleLeave = () => {
    if (preload() === 'intent') {
      cancelPreload(ref)
    }
  }

  const simpleStyling = Solid.createMemo(
    () =>
      activeProps() === STATIC_ACTIVE_PROPS_GET &&
      inactiveProps() === STATIC_INACTIVE_PROPS_GET &&
      local.class === undefined &&
      local.style === undefined,
    { lazy: true },
  )

  const onClick = createComposedHandler(() => local.onClick, handleClick)
  const onBlur = createComposedHandler(() => local.onBlur, handleLeave)
  const onFocus = createComposedHandler(() => local.onFocus, enqueuePreload)
  const onMouseEnter = createComposedHandler(
    () => local.onMouseEnter,
    enqueuePreload,
  )
  const onMouseOver = createComposedHandler(
    () => local.onMouseOver,
    enqueuePreload,
  )
  const onMouseLeave = createComposedHandler(
    () => local.onMouseLeave,
    handleLeave,
  )
  const onMouseOut = createComposedHandler(() => local.onMouseOut, handleLeave)
  const onTouchStart = createComposedHandler(
    () => local.onTouchStart,
    handleTouchStart,
  )

  type ResolvedLinkStateProps = Omit<ComponentProps<'a'>, 'style'> & {
    style?: JSX.CSSProperties
  }

  const resolvedStateProps = Solid.createMemo(
    (): ResolvedLinkStateProps =>
      (isActive()
        ? functionalUpdate(activeProps() as any, {})
        : functionalUpdate(inactiveProps(), {})) ?? EMPTY_OBJECT,
    { lazy: true },
  )

  const resolvedClass = Solid.createMemo(
    () => {
      if (simpleStyling()) return isActive() ? 'active' : undefined
      return (
        [local.class, resolvedStateProps().class].filter(Boolean).join(' ') ||
        undefined
      )
    },
    { lazy: true },
  )

  const resolvedStyle = Solid.createMemo(
    () => {
      if (simpleStyling()) return local.style
      const style = { ...local.style, ...resolvedStateProps().style }
      return hasKeys(style) ? style : undefined
    },
    { lazy: true },
  )

  // The returned object must be a plain object with a stable key set so the
  // consuming spread() never enumerates through proxy traps. Reactivity lives
  // in the property getters; values that no longer apply resolve to undefined,
  // which spread()/assign() treats as attribute removal. Keys returned by
  // activeProps/inactiveProps are discovered once at setup.
  const extraStateKeys = new Set<string>()
  Solid.untrack(() => {
    for (const stateProps of [
      functionalUpdate(activeProps() as any, {}),
      functionalUpdate(inactiveProps(), {}),
    ]) {
      if (stateProps) {
        for (const key of Object.keys(stateProps)) {
          if (key !== 'class' && key !== 'style') extraStateKeys.add(key)
        }
      }
    }
  })

  const composedRef = mergeRefs(setRef, (el: Element) => {
    const r = _options().ref as any
    if (typeof r === 'function') r(el)
  })

  const linkProps: Record<string, any> = {}
  for (const key of Object.keys(propsSafeToSpread)) {
    Object.defineProperty(
      linkProps,
      key,
      Object.getOwnPropertyDescriptor(propsSafeToSpread, key)!,
    )
  }
  for (const key of extraStateKeys) {
    Object.defineProperty(linkProps, key, {
      get: () => (resolvedStateProps() as Record<string, any>)[key],
      enumerable: true,
      configurable: true,
    })
  }

  const defineGetters = (getters: Record<string, () => any>) => {
    for (const key of Object.keys(getters)) {
      Object.defineProperty(linkProps, key, {
        get: getters[key],
        enumerable: true,
        configurable: true,
      })
    }
  }

  linkProps.ref = composedRef
  linkProps.onClick = onClick
  linkProps.onBlur = onBlur
  linkProps.onFocus = onFocus
  linkProps.onMouseEnter = onMouseEnter
  linkProps.onMouseOver = onMouseOver
  linkProps.onMouseLeave = onMouseLeave
  linkProps.onMouseOut = onMouseOut
  linkProps.onTouchStart = onTouchStart

  defineGetters({
    href: () => hrefOption()?.href,
    disabled: () => !!local.disabled,
    target: () => local.target,
    role: () => (local.disabled ? 'link' : undefined),
    'aria-disabled': () => (local.disabled ? 'true' : undefined),
    'data-status': () => (isActive() ? 'active' : undefined),
    'aria-current': () => (isActive() ? 'page' : undefined),
    class: resolvedClass,
    style: resolvedStyle,
  })

  return linkProps as any
}

const STATIC_ACTIVE_PROPS = { class: 'active' }
const STATIC_ACTIVE_PROPS_GET = () => STATIC_ACTIVE_PROPS
const EMPTY_OBJECT = {}
const STATIC_INACTIVE_PROPS_GET = () => EMPTY_OBJECT

/** Call a JSX.EventHandlerUnion with the event. */
function callHandler<T, TEvent extends Event>(
  event: TEvent & { currentTarget: T; target: Element },
  handler: JSX.EventHandlerUnion<T, TEvent>,
) {
  if (typeof handler === 'function') {
    handler(event)
  } else {
    handler[0](handler[1], event)
  }
  return event.defaultPrevented
}

function createComposedHandler<T, TEvent extends Event>(
  getHandler: () => JSX.EventHandlerUnion<T, TEvent> | undefined,
  fallback: (event: TEvent) => void,
) {
  return (event: TEvent & { currentTarget: T; target: Element }) => {
    const handler = getHandler()
    if (!handler || !callHandler(event, handler)) fallback(event)
  }
}

export type UseLinkPropsOptions<
  TRouter extends AnyRouter = RegisteredRouter,
  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
  TTo extends string | undefined = '.',
  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
  TMaskTo extends string = '.',
> = ActiveLinkOptions<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
  Omit<ComponentProps<'a'>, 'style'> & { style?: JSX.CSSProperties }

export type ActiveLinkOptions<
  TComp = 'a',
  TRouter extends AnyRouter = RegisteredRouter,
  TFrom extends string = string,
  TTo extends string | undefined = '.',
  TMaskFrom extends string = TFrom,
  TMaskTo extends string = '.',
> = LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
  ActiveLinkOptionProps<TComp>

type ActiveLinkProps<TComp> = Partial<
  LinkComponentSolidProps<TComp> & {
    [key: `data-${string}`]: unknown
  }
>

export interface ActiveLinkOptionProps<TComp = 'a'> {
  /**
   * A function that returns additional props for the `active` state of this link.
   * These props override other props passed to the link (`style`'s are merged, `class`'s are concatenated)
   */
  activeProps?: ActiveLinkProps<TComp> | (() => ActiveLinkProps<TComp>)
  /**
   * A function that returns additional props for the `inactive` state of this link.
   * These props override other props passed to the link (`style`'s are merged, `class`'s are concatenated)
   */
  inactiveProps?: ActiveLinkProps<TComp> | (() => ActiveLinkProps<TComp>)
}

export type LinkProps<
  TComp = 'a',
  TRouter extends AnyRouter = RegisteredRouter,
  TFrom extends string = string,
  TTo extends string | undefined = '.',
  TMaskFrom extends string = TFrom,
  TMaskTo extends string = '.',
> = ActiveLinkOptions<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
  LinkPropsChildren

export interface LinkPropsChildren {
  // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
  children?: JSX.Element | ((state: { isActive: boolean }) => JSX.Element)
}

type LinkComponentSolidProps<TComp> = TComp extends ValidComponent
  ? Omit<ComponentProps<TComp>, keyof CreateLinkProps>
  : never

export type LinkComponentProps<
  TComp = 'a',
  TRouter extends AnyRouter = RegisteredRouter,
  TFrom extends string = string,
  TTo extends string | undefined = '.',
  TMaskFrom extends string = TFrom,
  TMaskTo extends string = '.',
> = LinkComponentSolidProps<TComp> &
  LinkProps<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo>

export type CreateLinkProps = LinkProps<
  any,
  any,
  string,
  string,
  string,
  string
>

export type LinkComponent<
  in out TComp,
  in out TDefaultFrom extends string = string,
> = <
  TRouter extends AnyRouter = RegisteredRouter,
  const TFrom extends string = TDefaultFrom,
  const TTo extends string | undefined = undefined,
  const TMaskFrom extends string = TFrom,
  const TMaskTo extends string = '',
>(
  props: LinkComponentProps<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
) => JSX.Element

export interface LinkComponentRoute<
  in out TDefaultFrom extends string = string,
> {
  defaultFrom: TDefaultFrom;
  <
    TRouter extends AnyRouter = RegisteredRouter,
    const TTo extends string | undefined = undefined,
    const TMaskTo extends string = '',
  >(
    props: LinkComponentProps<
      'a',
      TRouter,
      this['defaultFrom'],
      TTo,
      this['defaultFrom'],
      TMaskTo
    >,
  ): JSX.Element
}

export function createLink<const TComp>(
  Comp: Constrain<TComp, any, (props: CreateLinkProps) => JSX.Element>,
): LinkComponent<TComp> {
  return (props) => <Link {...props} _asChild={Comp} />
}

export const Link: LinkComponent<'a'> = (props) => {
  const [local, rest] = splitProps(props as typeof props & { _asChild: any }, [
    '_asChild',
    'children',
  ])
  const [_, linkProps] = splitProps(useLinkProps(rest as unknown as any), [
    'type',
  ])

  // Resolve children once using Solid.children to avoid
  // re-accessing the children getter (which in Solid 2.0 would
  // re-invoke createComponent each time for JSX children).
  const resolvedChildren = Solid.children(() => local.children as JSX.Element)
  const children = () => {
    const ch = resolvedChildren()
    if (typeof ch === 'function') {
      return (ch as Function)({
        get isActive() {
          return (linkProps as any)['data-status'] === 'active'
        },
      })
    }

    return ch
  }

  if (local._asChild === 'svg') {
    const [_, svgLinkProps] = splitProps(linkProps as any, ['class'])
    return (
      <svg>
        <a {...svgLinkProps}>{children()}</a>
      </svg>
    )
  }

  if (!local._asChild) {
    return <a {...linkProps}>{children()}</a>
  }

  return (
    <Dynamic component={local._asChild as ValidComponent} {...linkProps}>
      {children()}
    </Dynamic>
  )
}

function isCtrlEvent(e: MouseEvent) {
  return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
}

function isSafeInternal(to: unknown) {
  if (typeof to !== 'string') return false
  const zero = to.charCodeAt(0)
  if (zero === 47) return to.charCodeAt(1) !== 47 // '/' but not '//'
  return zero === 46 // '.', '..', './', '../'
}

export type LinkOptionsFnOptions<
  TOptions,
  TComp,
  TRouter extends AnyRouter = RegisteredRouter,
> =
  TOptions extends ReadonlyArray<any>
    ? ValidateLinkOptionsArray<TRouter, TOptions, string, TComp>
    : ValidateLinkOptions<TRouter, TOptions, string, TComp>

export type LinkOptionsFn<TComp> = <
  const TOptions,
  TRouter extends AnyRouter = RegisteredRouter,
>(
  options: LinkOptionsFnOptions<TOptions, TComp, TRouter>,
) => TOptions

export const linkOptions: LinkOptionsFn<'a'> = (options) => {
  return options as any
}
