import { subscribeFlightData } from '@solidjs/web/server-functions/client'
import type { AnyRouteMatch, AnyRouter } from '@tanstack/solid-router'
import { getRouterInstance } from '@tanstack/start-client-core'
import type {
  SolidStartFlightData,
  SolidStartFlightMatch,
} from './solid-rpc-flight'

let subscribed = false

export function subscribeSolidStartFlightData() {
  if (subscribed) {
    return
  }
  subscribed = true

  subscribeFlightData<SolidStartFlightData>(async (data) => {
    if (!isSolidStartFlightData(data)) {
      return
    }

    const router = await getRouterInstance()
    if ('dehydratedData' in data) {
      await router.options.hydrate?.(data.dehydratedData as never)
    }

    const currentLocation = isCurrentLocation(router, data.href)
    const matches = currentLocation
      ? router.stores.matches.get()
      : router.matchRoutes(router.buildLocation({ href: data.href } as never))
    if (matches.length !== data.matches.length) {
      return
    }

    const flightMatches = new Map(
      data.matches.map((match) => [match.id, match]),
    )
    const hydratedMatches: Array<AnyRouteMatch> = []
    for (const match of matches) {
      const flightMatch = flightMatches.get(match.id)
      if (!flightMatch) {
        return
      }
      hydratedMatches.push(applyFlightMatch(match, flightMatch))
    }

    if (currentLocation) {
      publishCurrentMatches(router, hydratedMatches)
    } else {
      seedRedirectMatches(router, hydratedMatches)
    }
  })
}

function applyFlightMatch(
  match: AnyRouteMatch,
  flightMatch: SolidStartFlightMatch,
): AnyRouteMatch {
  const hasRouteState =
    flightMatch.loaderData !== undefined ||
    flightMatch.beforeLoadContext !== undefined ||
    'error' in flightMatch ||
    flightMatch.notFound === true ||
    match.error !== undefined ||
    match._notFound === true

  if (!hasRouteState) {
    return match
  }

  const nextMatch = {
    ...match,
    error: flightMatch.error,
    invalid: false,
    isFetching: false,
    preload: false,
    status: flightMatch.status,
    updatedAt: flightMatch.updatedAt,
    _flight: undefined,
    _notFound: flightMatch.notFound,
  } as AnyRouteMatch

  if ('loaderData' in flightMatch) {
    nextMatch.loaderData = flightMatch.loaderData
  }
  if ('beforeLoadContext' in flightMatch) {
    ;(
      nextMatch as AnyRouteMatch & { __beforeLoadContext?: unknown }
    ).__beforeLoadContext = flightMatch.beforeLoadContext
    nextMatch.context = {
      ...nextMatch.context,
      ...(flightMatch.beforeLoadContext as object | undefined),
    }
  }
  if ('ssr' in flightMatch) {
    nextMatch.ssr = flightMatch.ssr
  }

  return nextMatch
}

function publishCurrentMatches(
  router: AnyRouter,
  matches: Array<AnyRouteMatch>,
) {
  for (const match of matches) {
    router._cache.delete(match.id)
  }
  router._committed = matches
  router.batch(() => {
    router.stores.setMatches(matches)
    router.stores.status.set('idle')
    router.stores.resolvedLocation.set(router.stores.location.get())
  })
}

function seedRedirectMatches(router: AnyRouter, matches: Array<AnyRouteMatch>) {
  for (const match of matches) {
    router._cache.set(match.id, {
      ...match,
      context: {},
      preload: true,
    })
  }
}

function isCurrentLocation(router: AnyRouter, href: string) {
  const current = new URL(router.latestLocation.href, window.location.href)
  const target = new URL(href, current)
  return (
    current.pathname === target.pathname &&
    current.search === target.search &&
    current.hash === target.hash
  )
}

function isSolidStartFlightData(value: unknown): value is SolidStartFlightData {
  return (
    isObject(value) &&
    typeof value.href === 'string' &&
    Array.isArray(value.matches)
  )
}

function isObject(value: unknown): value is Record<string, unknown> {
  return !!value && typeof value === 'object'
}
