import React, { useMemo, useState } from "react";
import { matchPath, PathMatch, useNavigate } from "react-router-dom";

import { useSnackbar } from "notistack";

import {
  getPage,
  getPath,
  getTitle,
  isNavigation,
  parseOperationId,
  stripPathPrefix,
} from "../router/routes";

import { useApi } from "./ApiContext";

export interface RouteInfo {
  id: string;
  app: string;
  view: string;
  action: string;
  title: string;
  /** Whether this route should be used for navigation in the router context */
  navigation: boolean;
  path: string;
  page: string;
}

export type RouterParams = Record<string, string | number | boolean>;
export type RouterQuery = string | URLSearchParams | string[][] | Record<string, string>;

interface RouterContext {
  routes: RouteInfo[];
  getCurrent(): { route: RouteInfo; match: PathMatch } | undefined;
  getRoute(reverse: string): RouteInfo | undefined;
  setCustomRoutes(routes: RouteInfo[]): void;
  navigate(
    route?: number | string | RouteInfo,
    options?: {
      params?: RouterParams;
      query?: RouterQuery;
      replace?: boolean;
    },
  ): void;
}

const RouterContext = React.createContext<RouterContext>({
  routes: [],
  getCurrent: () => void 0,
  getRoute: (reverse) => void reverse,
  setCustomRoutes: (routes) => void routes,
  navigate: (route, options) => void [route, options],
});
export const useRouter = () => React.useContext(RouterContext);

export const RouterContextProvider: React.FC<
  React.PropsWithChildren<{ stripPathPrefix?: string; basename?: string }>
> = ({ children, ...props }) => {
  const api = useApi();
  const [customRoutes, setCustomRoutes] = useState<RouteInfo[]>([]);
  const apiRoutes = useMemo<RouteInfo[]>(() => {
    const routes = [];
    for (const operation of Object.values(api.operations)) {
      const parsedOperationId = parseOperationId(operation.id);
      if (!parsedOperationId) {
        throw new TypeError(`Could not parse operation id ${operation.id}`);
      }
      const { app, view, action } = parsedOperationId;
      const title = getTitle(view, operation.summary);
      const navigation = isNavigation(operation.tags);
      const path = stripPathPrefix(
        getPath(operation.endpoint, operation.method, action),
        props.stripPathPrefix,
      );
      const page = getPage(path, action);
      routes.push({
        id: operation.id,
        app,
        view,
        action,
        title,
        navigation,
        path,
        page,
      });
    }
    return routes;
  }, [api]);
  const routes = useMemo<RouteInfo[]>(
    () => customRoutes.concat(apiRoutes),
    [customRoutes, apiRoutes],
  );
  const routerNavigate = useNavigate();
  const { enqueueSnackbar } = useSnackbar();

  const getCurrent = () => {
    let currentPath = stripPathPrefix(location.pathname, props.basename);
    if (!currentPath.startsWith("/")) {
      currentPath = `/${currentPath}`;
    }

    for (const route of routes) {
      const match = matchPath(route.path, currentPath);

      if (match !== null) {
        return { route, match };
      }
    }

    return undefined;
  };

  const getRoute = (reverse: string) => {
    return routes.find(({ id }) => reverse === id)!;
  };

  const navigate = (
    route?: number | string | RouteInfo,
    options?: {
      params?: RouterParams;
      query?: RouterQuery;
      replace?: boolean;
    },
  ) => {
    // Relative history, e.g. go back or forward x steps
    if (typeof route === "number") {
      routerNavigate(route);
      return;
    }

    // Direct operation id routes, requires reversing the operation id
    if (typeof route === "string") {
      const routeInfo = getRoute(route);

      if (routeInfo == null) {
        enqueueSnackbar("Failed to navigate, view console for more info", {
          variant: "error",
        });
        throw new Error(`Could not find route with reverse: ${route}`);
      }

      route = routeInfo;
    }

    let pathname = route?.path;
    if (pathname != null) {
      for (const [key, value] of Object.entries(options?.params ?? {})) {
        pathname = pathname.replace(`:${key}`, encodeURIComponent(value));
      }
    }

    routerNavigate(
      {
        pathname,
        search: new URLSearchParams(options?.query).toString(),
      },
      { replace: options?.replace },
    );
  };

  return (
    <RouterContext.Provider value={{ routes, getCurrent, getRoute, setCustomRoutes, navigate }}>
      {children}
    </RouterContext.Provider>
  );
};

export default RouterContext;
