import { useSyncExternalStore } from 'react'
import { useServerContext } from './vite/one-server-only'
import type { One } from './vite/types'

/**
 * Re-export for convenience
 */
export type RouteMatch = One.RouteMatch

// client-side matches store for navigation updates
let clientMatches: RouteMatch[] = []
const clientMatchesListeners = new Set<() => void>()

export function subscribeToClientMatches(callback: () => void) {
  clientMatchesListeners.add(callback)
  return () => clientMatchesListeners.delete(callback)
}

export function getClientMatchesSnapshot() {
  return clientMatches
}

/**
 * Update the client-side matches store.
 * Called after navigation to update the matches with new loader data.
 * @internal
 */
export function setClientMatches(matches: RouteMatch[]) {
  clientMatches = matches
  for (const listener of clientMatchesListeners) {
    listener()
  }
}

/**
 * Update the loaderData for a single match by routeId, leaving all other matches intact.
 * @internal
 */
export function updateMatchLoaderData(routeId: string, loaderData: unknown) {
  clientMatches = clientMatches.map((m) =>
    m.routeId === routeId ? { ...m, loaderData } : m
  )
  for (const listener of clientMatchesListeners) {
    listener()
  }
}

/**
 * Returns an array of all matched routes from root to the current page.
 * Each match contains the route's loader data, params, and route ID.
 *
 * On the server (SSR), this returns the matches computed during the request.
 * On the client, this returns cached matches from hydration or the last navigation.
 *
 * @example
 * ```tsx
 * // In a layout component
 * function DocsLayout({ children }) {
 *   const matches = useMatches()
 *   const pageMatch = matches[matches.length - 1]
 *   const headings = pageMatch?.loaderData?.headings
 *
 *   return (
 *     <div>
 *       <TableOfContents headings={headings} />
 *       {children}
 *     </div>
 *   )
 * }
 * ```
 *
 * @example
 * ```tsx
 * // Building breadcrumbs
 * function Breadcrumbs() {
 *   const matches = useMatches()
 *
 *   return (
 *     <nav>
 *       {matches.map((match) => (
 *         <a key={match.routeId} href={match.pathname}>
 *           {match.loaderData?.title ?? match.routeId}
 *         </a>
 *       ))}
 *     </nav>
 *   )
 * }
 * ```
 */
export function useMatches(): RouteMatch[] {
  const serverContext = useServerContext()

  // on server, return from context directly
  if (process.env.VITE_ENVIRONMENT === 'ssr') {
    return serverContext?.matches ?? []
  }

  // on client, use sync external store for reactivity
  // the server snapshot (3rd arg) is used during hydration to match SSR output
  const clientStoreMatches = useSyncExternalStore(
    subscribeToClientMatches,
    getClientMatchesSnapshot,
    // server snapshot for hydration - must match what SSR rendered
    () => serverContext?.matches ?? []
  )

  // always return client store on client - it's initialized from server context
  // during hydration via initClientMatches, then updated on navigation
  return clientStoreMatches
}

/**
 * Find a specific match by route ID.
 *
 * @example
 * ```tsx
 * const docsMatch = useMatch('docs/_layout')
 * const navItems = docsMatch?.loaderData?.navItems
 * ```
 */
export function useMatch(routeId: string): RouteMatch | undefined {
  const matches = useMatches()
  return matches.find((m) => m.routeId === routeId)
}

/**
 * Get the current page's match (the last/deepest match).
 *
 * @example
 * ```tsx
 * const pageMatch = usePageMatch()
 * const { title, description } = pageMatch?.loaderData ?? {}
 * ```
 */
export function usePageMatch(): RouteMatch | undefined {
  const matches = useMatches()
  return matches[matches.length - 1]
}
