import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useParams, useSearchParams } from "react-router-dom";

import { OpenAPI } from "openapi-types";

import { useApi } from "../contexts/ApiContext";
import { RouteInfo } from "../contexts/RouterContext";
import { useUser } from "../contexts/UserContext";
import useAsyncError from "../hooks/useAsyncError";
import { PageComponent } from "../types";
import { hasAccess } from "../util/has_access";

import LoadingScreen from "./LoadingScreen";

export class PageLoadFailedError extends Error {
  readonly response: Response;

  constructor(response: Response, message?: string) {
    super(message);
    this.response = response;
  }
}

export interface PageLoaderProps {
  route: RouteInfo;
  page: PageComponent | Promise<PageComponent>;
  offline?: boolean;
  request?: OpenAPI.Request;
  defaultRequest?: OpenAPI.Request;
}

const PageLoader: React.FC<PageLoaderProps> = ({
  route,
  page,
  offline = false,
  request,
  defaultRequest,
}) => {
  const api = useApi();
  const params = useParams();
  const [searchParams] = useSearchParams();
  const { user } = useUser();

  const hasFetchedInitialData = useRef(false);
  const [data, setData] = useState(null);
  const throwError = useAsyncError();

  const operation = useMemo(() => api.operations[route.id], [api.operations, route.id]);

  const getData = useCallback(async () => {
    if (offline) return;
    if (operation == null) {
      throwError(new Error(`Operation ${route.id} not found`));
      return;
    }

    const getQueryEntries = (request: OpenAPI.Request) => {
      if (!request?.query) return [];
      if (request.query instanceof URLSearchParams) return request.query.entries();
      return Object.entries(request.query) ?? [];
    };

    const query =
      Array.from(searchParams.entries()).length === 0 && defaultRequest != null
        ? getQueryEntries(defaultRequest)
        : searchParams.entries();

    try {
      const requestParams = { ...params, ...request?.params };

      const requestQuery = new URLSearchParams([
        ...Array.from(query),
        ...Array.from(request ? getQueryEntries(request) : []),
      ]);

      const response = await operation.call({
        params: requestParams,
        query: requestQuery,
      });

      if (response.ok) {
        setData(await response.json());
      } else {
        throwError(
          new PageLoadFailedError(
            response,
            `Page data load failed with ${response.status} ${response.statusText}`,
          ),
        );
      }
    } catch (error) {
      // TODO Handle error with sentry
      console.error("[PAGE_LOADER]Error loading page data", error);
    }
  }, [api, route.id, params, searchParams, request, defaultRequest, offline, setData, throwError]);

  // TODO: Set search params on initial load
  useEffect(() => {
    if (!hasFetchedInitialData.current) {
      getData();
      hasFetchedInitialData.current = true;
    }
  }, [hasFetchedInitialData, getData]);

  const Page = useMemo(() => lazy(async () => ({ default: await Promise.resolve(page) })), [page]);

  if (!hasAccess(user, operation?.component?.permission, operation?.component?.group)) {
    return null;
  }

  return (
    <Suspense fallback={<LoadingScreen />}>
      {Boolean(data != null || offline) && <Page data={data} refresh={getData} />}
    </Suspense>
  );
};

export default PageLoader;
