import invariant from "tiny-invariant";

import {
	LinkInfo,
	LinkOptions,
	NavigateOptions,
	ToOptions,
	ValidFromPath,
	ResolveRelativePath,
} from "./link";
import {
	cleanPath,
	interpolatePath,
	joinPaths,
	matchPathname,
	parsePathname,
	resolvePath,
	trimPath,
	trimPathLeft,
} from "./path";
import {
	Route,
	AnySearchSchema,
	AnyRoute,
	RootRoute,
	AnyContext,
	AnyRootRoute,
} from "./route";
import { RoutesInfo, AnyRoutesInfo, RoutesById } from "./routeInfo";
import { AnyRouteMatch, RouteMatch, RouteMatchState } from "./routeMatch";
import { defaultParseSearch, defaultStringifySearch } from "./searchParams";
import {
	functionalUpdate,
	last,
	NoInfer,
	pick,
	PickAsRequired,
	Timeout,
	Updater,
	replaceEqualDeep,
	partialDeepEqual,
} from "./utils";
import {
	createBrowserHistory,
	createMemoryHistory,
	RouterHistory,
} from "./history";
import { RouteComponent } from "./react";

//

declare global {
	interface Window {
		__DEHYDRATED__?: Record<string, any>;
	}
}

export interface Register {
	// router: Router
}

export type AnyRouter = Router<any, any, any>;

export type RegisteredRouter = Register extends {
	router: Router<infer TRoute, infer TRoutesInfo>;
}
	? Router<TRoute, TRoutesInfo>
	: Router;

export type RegisteredRoutesInfo = Register extends {
	router: Router<infer TRoute, infer TRoutesInfo>;
}
	? TRoutesInfo
	: AnyRoutesInfo;

export interface LocationState {}

export interface ParsedLocation<
	TSearchObj extends AnySearchSchema = {},
	TState extends LocationState = LocationState,
> {
	href: string;
	pathname: string;
	search: TSearchObj;
	searchStr: string;
	state: TState;
	hash: string;
	key?: string;
}

export interface FromLocation {
	pathname: string;
	search?: unknown;
	key?: string;
	hash?: string;
}

export type SearchSerializer = (searchObj: Record<string, any>) => string;
export type SearchParser = (searchStr: string) => Record<string, any>;
export type FilterRoutesFn = <TRoute extends AnyRoute>(
	routes: TRoute[],
) => TRoute[];

type RouterContextOptions<TRouteTree extends AnyRoute> =
	AnyContext extends TRouteTree["__types"]["routerContext"]
		? {
				context?: TRouteTree["__types"]["routerContext"];
		  }
		: {
				context: TRouteTree["__types"]["routerContext"];
		  };

export interface RouterOptions<
	TRouteTree extends AnyRoute,
	TDehydrated extends Record<string, any>,
> {
	history?: RouterHistory;
	stringifySearch?: SearchSerializer;
	parseSearch?: SearchParser;
	filterRoutes?: FilterRoutesFn;
	defaultPreload?: false | "intent";
	defaultPreloadDelay?: number;
	defaultComponent?: RouteComponent;
	defaultErrorComponent?: RouteComponent<{
		error: Error;
		info: { componentStack: string };
	}>;
	defaultPendingComponent?: RouteComponent;
	defaultLoaderMaxAge?: number;
	defaultLoaderGcMaxAge?: number;
	caseSensitive?: boolean;
	routeTree?: TRouteTree;
	basepath?: string;
	createRoute?: (opts: { route: AnyRoute; router: AnyRouter }) => void;
	onRouteChange?: () => void;
	fetchServerDataFn?: FetchServerDataFn;
	context?: TRouteTree["__types"]["routerContext"];
	Provider?: React.ComponentType<{ children: any }>;
	dehydrate?: () => TDehydrated;
	hydrate?: (dehydrated: TDehydrated) => void;
}

type FetchServerDataFn = (ctx: {
	router: AnyRouter;
	routeMatch: RouteMatch;
}) => Promise<any>;

export interface RouterState<
	TRoutesInfo extends AnyRoutesInfo = AnyRoutesInfo,
	TState extends LocationState = LocationState,
> {
	status: "idle" | "pending";
	latestLocation: ParsedLocation<TRoutesInfo["fullSearchSchema"], TState>;
	currentMatches: RouteMatch<TRoutesInfo, TRoutesInfo["routeIntersection"]>[];
	currentLocation: ParsedLocation<TRoutesInfo["fullSearchSchema"], TState>;
	pendingMatches?: RouteMatch<TRoutesInfo, TRoutesInfo["routeIntersection"]>[];
	pendingLocation?: ParsedLocation<TRoutesInfo["fullSearchSchema"], TState>;
	lastUpdated: number;
}

export type ListenerFn = () => void;

export interface BuildNextOptions {
	to?: string | number | null;
	params?: true | Updater<unknown>;
	search?: true | Updater<unknown>;
	hash?: true | Updater<string>;
	state?: LocationState;
	key?: string;
	from?: string;
	fromCurrent?: boolean;
	__matches?: RouteMatch[];
}

export type MatchCacheEntry = {
	gc: number;
	match: RouteMatch;
};

export interface MatchLocation {
	to?: string | number | null;
	fuzzy?: boolean;
	caseSensitive?: boolean;
	from?: string;
	fromCurrent?: boolean;
}

export interface MatchRouteOptions {
	pending?: boolean;
	caseSensitive?: boolean;
	includeSearch?: boolean;
	fuzzy?: boolean;
}

type LinkCurrentTargetElement = {
	preloadTimeout?: null | ReturnType<typeof setTimeout>;
};

export interface DehydratedRouterState
	extends Pick<
		RouterState,
		"status" | "latestLocation" | "currentLocation" | "lastUpdated"
	> {
	// currentMatches: DehydratedRouteMatch[]
}

export interface DehydratedRouter {
	state: DehydratedRouterState;
}

export type MatchCache = Record<string, MatchCacheEntry>;

interface DehydratedRouteMatch {
	id: string;
	state: Pick<RouteMatchState<any, any>, "status">;
}

export const defaultFetchServerDataFn: FetchServerDataFn = async ({
	router,
	routeMatch,
}) => {
	const next = router.buildNext({
		to: ".",
		search: (d: any) => ({
			...(d ?? {}),
			__data: {
				matchId: routeMatch.id,
			},
		}),
	});

	const res = await fetch(next.href, {
		method: "GET",
		signal: routeMatch.abortController.signal,
	});

	if (res.ok) {
		return res.json();
	}

	throw new Error("Failed to fetch match data");
};

export type RouterConstructorOptions<
	TRouteTree extends AnyRoute,
	TDehydrated extends Record<string, any>,
> = Omit<RouterOptions<TRouteTree, TDehydrated>, "context"> &
	RouterContextOptions<TRouteTree>;

export class Router<
	TRouteTree extends AnyRoute = AnyRoute,
	TRoutesInfo extends AnyRoutesInfo = RoutesInfo<TRouteTree>,
	TDehydrated extends Record<string, any> = Record<string, any>,
> {
	types!: {
		// Super secret internal stuff
		RootRoute: TRouteTree;
		RoutesInfo: TRoutesInfo;
	};

	options: PickAsRequired<
		RouterOptions<TRouteTree, TDehydrated>,
		"stringifySearch" | "parseSearch" | "context"
	>;
	context!: NonNullable<TRouteTree["__types"]["routerContext"]>;
	history!: RouterHistory;
	#unsubHistory?: () => void;
	basepath!: string;
	// __location: Location<TRoutesInfo['fullSearchSchema']>
	routeTree!: RootRoute;
	routesById!: RoutesById<TRoutesInfo>;
	navigateTimeout: undefined | Timeout;
	nextAction: undefined | "push" | "replace";
	navigationPromise: undefined | Promise<void>;

	__store: Store<RouterState<TRoutesInfo>>;
	state: RouterState<TRoutesInfo>;
	startedLoadingAt = Date.now();
	resolveNavigation: () => void = () => {};

	constructor(options?: RouterConstructorOptions<TRouteTree, TDehydrated>) {
		this.options = {
			defaultPreloadDelay: 50,
			context: undefined!,
			...options,
			stringifySearch: options?.stringifySearch ?? defaultStringifySearch,
			parseSearch: options?.parseSearch ?? defaultParseSearch,
			fetchServerDataFn: options?.fetchServerDataFn ?? defaultFetchServerDataFn,
		};

		this.__store = new Store<RouterState<TRoutesInfo>>(
			getInitialRouterState(),
			{
				onUpdate: (state) => {
					this.state = state;
				},
			},
		);
		this.state = this.__store.state;

		this.update(options);

		const next = this.buildNext({
			hash: true,
			fromCurrent: true,
			search: true,
			state: true,
		});

		if (this.state.latestLocation.href !== next.href) {
			this.#commitLocation({ ...next, replace: true });
		}
	}

	reset = () => {
		this.__store.setState((s) => Object.assign(s, getInitialRouterState()));
	};

	mount = () => {
		// Mount only does anything on the client
		if (!isServer) {
			// If the router matches are empty, start loading the matches
			if (!this.state.currentMatches.length) {
				this.safeLoad();
			}
		}

		return () => {};
	};

	hydrate = async (__do_not_use_server_ctx?: any) => {
		let ctx = __do_not_use_server_ctx;
		// Client hydrates from window
		if (typeof document !== "undefined") {
			ctx = window.__DEHYDRATED__;

			invariant(
				ctx,
				"Expected to find a __DEHYDRATED__ property on window... but we did not. THIS IS VERY BAD",
			);
		}

		this.options.hydrate?.(ctx);

		return await this.load();
	};

	update = (opts?: RouterOptions<any, any>): this => {
		Object.assign(this.options, opts);

		this.context = this.options.context;

		if (
			!this.history ||
			(this.options.history && this.options.history !== this.history)
		) {
			if (this.#unsubHistory) {
				this.#unsubHistory();
			}

			this.history =
				this.options.history ??
				(isServer ? createMemoryHistory() : createBrowserHistory()!);

			const parsedLocation = this.#parseLocation();

			this.__store.setState((s) => ({
				...s,
				latestLocation: parsedLocation,
				currentLocation: parsedLocation,
			}));

			this.#unsubHistory = this.history.listen(() => {
				this.safeLoad({
					next: this.#parseLocation(this.state.latestLocation),
				});
			});
		}

		const { basepath, routeTree } = this.options;

		this.basepath = `/${trimPath(basepath ?? "") ?? ""}`;

		if (routeTree) {
			this.routesById = {} as any;
			this.routeTree = this.#buildRouteTree(routeTree) as RootRoute;
		}

		return this;
	};

	buildNext = (opts: BuildNextOptions): ParsedLocation => {
		const next = this.#buildLocation(opts);

		const __matches = this.matchRoutes(next.pathname);

		return this.#buildLocation({
			...opts,
			__matches,
		});
	};

	cancelMatches = () => {
		[
			...this.state.currentMatches,
			...(this.state.pendingMatches || []),
		].forEach((match) => {
			match.cancel();
		});
	};

	safeLoad = (opts?: { next?: ParsedLocation }) => {
		this.load(opts).catch((err) => {
			console.warn(err);
			invariant(false, "Encountered an error during router.load()! ☝️.");
		});
	};

	load = async (opts?: { next?: ParsedLocation }): Promise<void> => {
		let now = Date.now();
		const startedAt = now;
		this.startedLoadingAt = startedAt;

		// Cancel any pending matches
		this.cancelMatches();

		let matches!: RouteMatch<any, any>[];

		this.__store.batch(() => {
			if (opts?.next) {
				// Ingest the new location
				this.__store.setState((s) => ({
					...s,
					latestLocation: opts.next!,
				}));
			}

			// Match the routes
			matches = this.matchRoutes(this.state.latestLocation.pathname, {
				strictParseParams: true,
			});

			this.__store.setState((s) => ({
				...s,
				status: "pending",
				pendingMatches: matches,
				pendingLocation: this.state.latestLocation,
			}));
		});

		// Load the matches
		await this.loadMatches(
			matches,
			this.state.pendingLocation!,
			// opts
		);

		if (this.startedLoadingAt !== startedAt) {
			// Ignore side-effects of outdated side-effects
			return this.navigationPromise;
		}

		const previousMatches = this.state.currentMatches;

		const exiting: AnyRouteMatch[] = [],
			staying: AnyRouteMatch[] = [];

		previousMatches.forEach((d) => {
			if (matches.find((dd) => dd.id === d.id)) {
				staying.push(d);
			} else {
				exiting.push(d);
			}
		});

		const entering = matches.filter((d) => {
			return !previousMatches.find((dd) => dd.id === d.id);
		});

		now = Date.now();

		exiting.forEach((d) => {
			d.__onExit?.({
				params: d.params,
				search: d.state.routeSearch,
			});

			// Clear non-loading error states when match leaves
			if (d.state.status === "error") {
				this.__store.setState((s) => ({
					...s,
					status: "idle",
					error: undefined,
				}));
			}
		});

		staying.forEach((d) => {
			d.route.options.onTransition?.({
				params: d.params,
				search: d.state.routeSearch,
			});
		});

		entering.forEach((d) => {
			d.__onExit = d.route.options.onLoaded?.({
				params: d.params,
				search: d.state.search,
			});
		});

		const prevLocation = this.state.currentLocation;

		this.__store.setState((s) => ({
			...s,
			status: "idle",
			currentLocation: this.state.latestLocation,
			currentMatches: matches,
			pendingLocation: undefined,
			pendingMatches: undefined,
		}));

		matches.forEach((match) => {
			match.__commit();
		});

		if (prevLocation!.href !== this.state.currentLocation.href) {
			this.options.onRouteChange?.();
		}

		this.resolveNavigation();
	};

	getRoute = <TId extends keyof TRoutesInfo["routesById"]>(
		id: TId,
	): TRoutesInfo["routesById"][TId] => {
		const route = this.routesById[id];

		invariant(route, `Route with id "${id as string}" not found`);

		return route;
	};

	loadRoute = async (
		navigateOpts: BuildNextOptions = this.state.latestLocation,
	): Promise<RouteMatch[]> => {
		const next = this.buildNext(navigateOpts);
		const matches = this.matchRoutes(next.pathname, {
			strictParseParams: true,
		});
		await this.loadMatches(matches, next);
		return matches;
	};

	preloadRoute = async (
		navigateOpts: BuildNextOptions = this.state.latestLocation,
	) => {
		const next = this.buildNext(navigateOpts);
		const matches = this.matchRoutes(next.pathname, {
			strictParseParams: true,
		});

		await this.loadMatches(matches, next, {
			preload: true,
		});
		return matches;
	};

	matchRoutes = (pathname: string, opts?: { strictParseParams?: boolean }) => {
		const matches: AnyRouteMatch[] = [];

		if (!this.routeTree) {
			return matches;
		}

		const existingMatches = [
			...this.state.currentMatches,
			...(this.state.pendingMatches ?? []),
		];

		const findInRouteTree = async (
			routes: Route<any, any>[],
		): Promise<void> => {
			const parentMatch = last(matches);
			let params = parentMatch?.params ?? {};

			const filteredRoutes = this.options.filterRoutes?.(routes) ?? routes;

			let matchingRoutes: Route[] = [];

			const findMatchInRoutes = (parentRoutes: Route[], routes: Route[]) => {
				routes.some((route) => {
					const children = route.children as undefined | Route[];
					if (!route.path && children?.length) {
						return findMatchInRoutes(
							[...matchingRoutes, route],
							children as any,
						);
					}

					const fuzzy = !!(route.path !== "/" || children?.length);

					const matchParams = matchPathname(this.basepath, pathname, {
						to: route.fullPath,
						fuzzy,
						caseSensitive:
							route.options.caseSensitive ?? this.options.caseSensitive,
					});

					if (matchParams) {
						let parsedParams;

						try {
							parsedParams =
								route.options.parseParams?.(matchParams!) ?? matchParams;
						} catch (err) {
							if (opts?.strictParseParams) {
								throw err;
							}
						}

						params = {
							...params,
							...parsedParams,
						};
					}

					if (!!matchParams) {
						matchingRoutes = [...parentRoutes, route];
					}

					return !!matchingRoutes.length;
				});

				return !!matchingRoutes.length;
			};

			findMatchInRoutes([], filteredRoutes);

			if (!matchingRoutes.length) {
				return;
			}

			matchingRoutes.forEach((foundRoute) => {
				const interpolatedPath = interpolatePath(foundRoute.path, params);
				const matchId = interpolatePath(foundRoute.id, params, true);

				const match =
					existingMatches.find((d) => d.id === matchId) ||
					new RouteMatch(this, foundRoute, {
						id: matchId,
						params,
						pathname: joinPaths([this.basepath, interpolatedPath]),
					});

				matches.push(match);
			});

			const foundRoute = last(matchingRoutes)!;

			const foundChildren = foundRoute.children as any;

			if (foundChildren?.length) {
				findInRouteTree(foundChildren);
			}
		};

		findInRouteTree([this.routeTree as any]);

		return matches;
	};

	loadMatches = async (
		resolvedMatches: RouteMatch[],
		location: ParsedLocation,
		opts?: {
			preload?: boolean;
			// filter?: (match: RouteMatch<any, any>) => any
		},
	) => {
		let firstBadMatchIndex: number | undefined;

		// Check each match middleware to see if the route can be accessed
		try {
			await Promise.all(
				resolvedMatches.map(async (match, index) => {
					try {
						await match.route.options.beforeLoad?.({
							router: this as any,
							match,
						});
					} catch (err) {
						if (isRedirect(err)) {
							throw err;
						}

						firstBadMatchIndex = firstBadMatchIndex ?? index;

						const errorHandler =
							match.route.options.onBeforeLoadError ??
							match.route.options.onError;
						try {
							errorHandler?.(err);
						} catch (errorHandlerErr) {
							if (isRedirect(errorHandlerErr)) {
								throw errorHandlerErr;
							}

							match.__store.setState((s) => ({
								...s,
								error: errorHandlerErr,
								status: "error",
								updatedAt: Date.now(),
							}));
							return;
						}

						match.__store.setState((s) => ({
							...s,
							error: err,
							status: "error",
							updatedAt: Date.now(),
						}));
					}
				}),
			);
		} catch (err) {
			if (isRedirect(err)) {
				if (!opts?.preload) {
					this.navigate(err as any);
				}
				return;
			}

			throw err; // we should never end up here
		}

		const validResolvedMatches = resolvedMatches.slice(0, firstBadMatchIndex);
		const matchPromises = validResolvedMatches.map(async (match, index) => {
			const parentMatch = validResolvedMatches[index - 1];

			match.__load({ preload: opts?.preload, location, parentMatch });

			await match.__loadPromise;

			if (parentMatch) {
				await parentMatch.__loadPromise;
			}
		});

		await Promise.all(matchPromises);
	};

	reload = () => {
		this.navigate({
			fromCurrent: true,
			replace: true,
			search: true,
		} as any);
	};

	resolvePath = (from: string, path: string) => {
		return resolvePath(this.basepath!, from, cleanPath(path));
	};

	navigate = async <
		TFrom extends ValidFromPath<TRoutesInfo> = "/",
		TTo extends string = "",
	>({
		from,
		to = "" as any,
		search,
		hash,
		replace,
		params,
	}: NavigateOptions<TRoutesInfo, TFrom, TTo>) => {
		// If this link simply reloads the current route,
		// make sure it has a new key so it will trigger a data refresh

		// If this `to` is a valid external URL, return
		// null for LinkUtils
		const toString = String(to);
		const fromString = typeof from === "undefined" ? from : String(from);
		let isExternal;

		try {
			new URL(`${toString}`);
			isExternal = true;
		} catch (e) {}

		invariant(
			!isExternal,
			"Attempting to navigate to external url with this.navigate!",
		);

		return this.#commitLocation({
			from: fromString,
			to: toString,
			search,
			hash,
			replace,
			params,
		});
	};

	matchRoute = <
		TFrom extends ValidFromPath<TRoutesInfo> = "/",
		TTo extends string = "",
		TResolved extends string = ResolveRelativePath<TFrom, NoInfer<TTo>>,
	>(
		location: ToOptions<TRoutesInfo, TFrom, TTo>,
		opts?: MatchRouteOptions,
	): false | TRoutesInfo["routesById"][TResolved]["__types"]["allParams"] => {
		location = {
			...location,
			to: location.to
				? this.resolvePath(location.from ?? "", location.to)
				: undefined,
		} as any;

		const next = this.buildNext(location);
		const baseLocation = opts?.pending
			? this.state.pendingLocation
			: this.state.currentLocation;

		if (!baseLocation) {
			return false;
		}

		const match = matchPathname(this.basepath, baseLocation.pathname, {
			...opts,
			to: next.pathname,
		}) as any;

		if (!match) {
			return false;
		}

		if (opts?.includeSearch ?? true) {
			return partialDeepEqual(baseLocation.search, next.search) ? match : false;
		}

		return match;
	};

	buildLink = <
		TFrom extends ValidFromPath<TRoutesInfo> = "/",
		TTo extends string = "",
	>({
		from,
		to = "." as any,
		search,
		params,
		hash,
		target,
		replace,
		activeOptions,
		preload,
		preloadDelay: userPreloadDelay,
		disabled,
	}: LinkOptions<TRoutesInfo, TFrom, TTo>): LinkInfo => {
		// If this link simply reloads the current route,
		// make sure it has a new key so it will trigger a data refresh

		// If this `to` is a valid external URL, return
		// null for LinkUtils

		try {
			new URL(`${to}`);
			return {
				type: "external",
				href: to,
			};
		} catch (e) {}

		const nextOpts = {
			from,
			to,
			search,
			params,
			hash,
			replace,
		};

		const next = this.buildNext(nextOpts);

		preload = preload ?? this.options.defaultPreload;
		const preloadDelay =
			userPreloadDelay ?? this.options.defaultPreloadDelay ?? 0;

		// Compare path/hash for matches
		const currentPathSplit = this.state.currentLocation.pathname.split("/");
		const nextPathSplit = next.pathname.split("/");
		const pathIsFuzzyEqual = nextPathSplit.every(
			(d, i) => d === currentPathSplit[i],
		);
		// Combine the matches based on user options
		const pathTest = activeOptions?.exact
			? this.state.currentLocation.pathname === next.pathname
			: pathIsFuzzyEqual;
		const hashTest = activeOptions?.includeHash
			? this.state.currentLocation.hash === next.hash
			: true;
		const searchTest =
			activeOptions?.includeSearch ?? true
				? partialDeepEqual(this.state.currentLocation.search, next.search)
				: true;

		// The final "active" test
		const isActive = pathTest && hashTest && searchTest;

		// The click handler
		const handleClick = (e: MouseEvent) => {
			if (
				!disabled &&
				!isCtrlEvent(e) &&
				!e.defaultPrevented &&
				(!target || target === "_self") &&
				e.button === 0
			) {
				e.preventDefault();

				// All is well? Navigate!
				this.#commitLocation(nextOpts as any);
			}
		};

		// The click handler
		const handleFocus = (e: MouseEvent) => {
			if (preload) {
				this.preloadRoute(nextOpts).catch((err) => {
					console.warn(err);
					console.warn("Error preloading route! ☝️");
				});
			}
		};

		const handleTouchStart = (e: TouchEvent) => {
			this.preloadRoute(nextOpts).catch((err) => {
				console.warn(err);
				console.warn("Error preloading route! ☝️");
			});
		};

		const handleEnter = (e: MouseEvent) => {
			const target = (e.target || {}) as LinkCurrentTargetElement;

			if (preload) {
				if (target.preloadTimeout) {
					return;
				}

				target.preloadTimeout = setTimeout(() => {
					target.preloadTimeout = null;
					this.preloadRoute(nextOpts).catch((err) => {
						console.warn(err);
						console.warn("Error preloading route! ☝️");
					});
				}, preloadDelay);
			}
		};

		const handleLeave = (e: MouseEvent) => {
			const target = (e.target || {}) as LinkCurrentTargetElement;

			if (target.preloadTimeout) {
				clearTimeout(target.preloadTimeout);
				target.preloadTimeout = null;
			}
		};

		return {
			type: "internal",
			next,
			handleFocus,
			handleClick,
			handleEnter,
			handleLeave,
			handleTouchStart,
			isActive,
			disabled,
		};
	};

	// dehydrate = (): DehydratedRouter => {
	//   return {
	//     state: {
	//       ...pick(this.state, [
	//         'latestLocation',
	//         'currentLocation',
	//         'status',
	//         'lastUpdated',
	//       ]),
	//       // currentMatches: this.state.currentMatches.map((match) => ({
	//       //   id: match.id,
	//       //   state: {
	//       //     status: match.state.status,
	//       //     // status: 'idle',
	//       //   },
	//       // })),
	//     },
	//   }
	// }

	// hydrate = (dehydratedRouter: DehydratedRouter) => {
	//   this.__store.setState((s) => {
	//     // Match the routes
	//     // const currentMatches = this.matchRoutes(
	//     //   dehydratedRouter.state.latestLocation.pathname,
	//     //   {
	//     //     strictParseParams: true,
	//     //   },
	//     // )

	//     // currentMatches.forEach((match, index) => {
	//     // const dehydratedMatch = dehydratedRouter.state.currentMatches[index]
	//     // invariant(
	//     //   dehydratedMatch && dehydratedMatch.id === match.id,
	//     //   'Oh no! There was a hydration mismatch when attempting to hydrate the state of the router! 😬',
	//     // )
	//     // match.__store.setState((s) => ({
	//     //   ...s,
	//     //   ...dehydratedMatch.state,
	//     // }))
	//     // })

	//     return {
	//       ...s,
	//       ...dehydratedRouter.state,
	//       // currentMatches,
	//     }
	//   })
	// }

	#buildRouteTree = (routeTree: AnyRoute) => {
		const recurseRoutes = (routes: Route[], parentRoute: Route | undefined) => {
			routes.forEach((route, i) => {
				route.init({ originalIndex: i, router: this });

				const existingRoute = (this.routesById as any)[route.id];

				invariant(
					!existingRoute,
					`Duplicate routes found with id: ${String(route.id)}`,
				);
				(this.routesById as any)[route.id] = route;

				const children = route.children as Route[];

				if (children?.length) {
					recurseRoutes(children, route);

					route.children = children
						.map((d, i) => {
							const parsed = parsePathname(
								trimPathLeft(cleanPath(d.path ?? "/")),
							);

							while (parsed.length > 1 && parsed[0]?.value === "/") {
								parsed.shift();
							}

							let score = 0;

							parsed.forEach((d, i) => {
								let modifier = 1;
								while (i--) {
									modifier *= 0.001;
								}
								if (d.type === "pathname" && d.value !== "/") {
									score += 1 * modifier;
								} else if (d.type === "param") {
									score += 2 * modifier;
								} else if (d.type === "wildcard") {
									score += 3 * modifier;
								}
							});

							return { child: d, parsed, index: i, score };
						})
						.sort((a, b) => {
							if (a.score !== b.score) {
								return a.score - b.score;
							}

							return a.index - b.index;
						})
						.map((d) => d.child);
				}
			});
		};

		recurseRoutes([routeTree] as Route[], undefined);

		const recurceCheckRoutes = (
			routes: Route[],
			parentRoute: Route | undefined,
		) => {
			routes.forEach((route) => {
				if (route.isRoot) {
					invariant(
						!parentRoute,
						"Root routes can only be used as the root of a route tree.",
					);
				} else {
					invariant(
						parentRoute ? route.parentRoute === parentRoute : true,
						`Expected a route with path "${route.path}" to be passed to its parent route "${route.parentRoute?.id}" in an addChildren() call, but was instead passed as a child of the "${parentRoute?.id}" route.`,
					);
				}

				if (route.children) {
					recurceCheckRoutes(route.children as Route[], route);
				}
			});
		};

		recurceCheckRoutes([routeTree] as Route[], undefined);

		return routeTree;
	};

	#parseLocation = (previousLocation?: ParsedLocation): ParsedLocation => {
		let { pathname, search, hash, state } = this.history.location;

		const parsedSearch = this.options.parseSearch(search);

		return {
			pathname: pathname,
			searchStr: search,
			search: replaceEqualDeep(previousLocation?.search, parsedSearch),
			hash: hash.split("#").reverse()[0] ?? "",
			href: `${pathname}${search}${hash}`,
			state: state as LocationState,
			key: state?.key || "__init__",
		};
	};

	#buildLocation = (dest: BuildNextOptions = {}): ParsedLocation => {
		dest.fromCurrent = dest.fromCurrent ?? dest.to === "";

		const fromPathname = dest.fromCurrent
			? this.state.latestLocation.pathname
			: dest.from ?? this.state.latestLocation.pathname;

		let pathname = resolvePath(
			this.basepath ?? "/",
			fromPathname,
			`${dest.to ?? ""}`,
		);

		const fromMatches = this.matchRoutes(this.state.latestLocation.pathname, {
			strictParseParams: true,
		});

		const prevParams = { ...last(fromMatches)?.params };

		let nextParams =
			(dest.params ?? true) === true
				? prevParams
				: functionalUpdate(dest.params!, prevParams);

		if (nextParams) {
			dest.__matches
				?.map((d) => d.route.options.stringifyParams)
				.filter(Boolean)
				.forEach((fn) => {
					nextParams = { ...nextParams!, ...fn!(nextParams!) };
				});
		}

		pathname = interpolatePath(pathname, nextParams ?? {});

		const preSearchFilters =
			dest.__matches
				?.map((match) => match.route.options.preSearchFilters ?? [])
				.flat()
				.filter(Boolean) ?? [];

		const postSearchFilters =
			dest.__matches
				?.map((match) => match.route.options.postSearchFilters ?? [])
				.flat()
				.filter(Boolean) ?? [];

		// Pre filters first
		const preFilteredSearch = preSearchFilters?.length
			? preSearchFilters?.reduce(
					(prev, next) => next(prev),
					this.state.latestLocation.search,
			  )
			: this.state.latestLocation.search;

		// Then the link/navigate function
		const destSearch =
			dest.search === true
				? preFilteredSearch // Preserve resolvedFrom true
				: dest.search
				? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
				: preSearchFilters?.length
				? preFilteredSearch // Preserve resolvedFrom filters
				: {};

		// Then post filters
		const postFilteredSearch = postSearchFilters?.length
			? postSearchFilters.reduce((prev, next) => next(prev), destSearch)
			: destSearch;

		const search = replaceEqualDeep(
			this.state.latestLocation.search,
			postFilteredSearch,
		);

		const searchStr = this.options.stringifySearch(search);
		let hash =
			dest.hash === true
				? this.state.latestLocation.hash
				: functionalUpdate(dest.hash!, this.state.latestLocation.hash);
		hash = hash ? `#${hash}` : "";

		const nextState =
			dest.state === true
				? this.state.latestLocation.state
				: functionalUpdate(dest.state, this.state.latestLocation.state)!;

		return {
			pathname,
			search,
			searchStr,
			state: nextState,
			hash,
			href: this.history.createHref(`${pathname}${searchStr}${hash}`),
			key: dest.key,
		};
	};

	#commitLocation = async (
		location: BuildNextOptions & { replace?: boolean },
	) => {
		const next = this.buildNext(location);
		const id = "" + Date.now() + Math.random();

		if (this.navigateTimeout) clearTimeout(this.navigateTimeout);

		let nextAction: "push" | "replace" = "replace";

		if (!location.replace) {
			nextAction = "push";
		}

		const isSameUrl = this.state.latestLocation.href === next.href;

		if (isSameUrl && !next.key) {
			nextAction = "replace";
		}

		const href = `${next.pathname}${next.searchStr}${
			next.hash ? `${next.hash}` : ""
		}`;

		this.history[nextAction === "push" ? "push" : "replace"](href, {
			id,
			...next.state,
		});

		return (this.navigationPromise = new Promise((resolve) => {
			const previousNavigationResolve = this.resolveNavigation;

			this.resolveNavigation = () => {
				previousNavigationResolve();
				resolve();
			};
		}));
	};
}

// Detect if we're in the DOM
const isServer =
	typeof window === "undefined" || !window.document.createElement;

function getInitialRouterState(): RouterState<any, any> {
	return {
		status: "idle",
		latestLocation: null!,
		currentLocation: null!,
		currentMatches: [],
		lastUpdated: Date.now(),
	};
}

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

export type AnyRedirect = Redirect<any, any, any>;

export type Redirect<
	TRoutesInfo extends AnyRoutesInfo = RegisteredRoutesInfo,
	TFrom extends TRoutesInfo["routePaths"] = "/",
	TTo extends string = "",
> = NavigateOptions<TRoutesInfo, TFrom, TTo> & {
	code?: number;
};

export function redirect<
	TRoutesInfo extends AnyRoutesInfo = RegisteredRoutesInfo,
	TFrom extends TRoutesInfo["routePaths"] = "/",
	TTo extends string = "",
>(opts: Redirect<TRoutesInfo, TFrom, TTo>): Redirect<TRoutesInfo, TFrom, TTo> {
	(opts as any).isRedirect = true;
	return opts;
}

export function isRedirect(obj: any): obj is AnyRedirect {
	return !!obj?.isRedirect;
}
