import React, { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Suspense } from "react";

import { SxProps } from "@mui/system";

import { OpenAPI } from "openapi-types";

import { ApiOperation } from "../api";
import { useApi } from "../contexts/ApiContext";
import { useUser } from "../contexts/UserContext";
import useAsyncError from "../hooks/useAsyncError";
import { hasAccess } from "../util/has_access";

import LoadingScreen from "./LoadingScreen";

export class DataLoadFailedError extends Error {
  readonly response: Response;

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

export interface ComponentLoaderProps extends OpenAPI.Request {
  operation: ApiOperation | string;
  sx?: SxProps;
}

interface ComponentLoaderInnerProps extends OpenAPI.Request {
  operation: ApiOperation & Required<Pick<ApiOperation, "component">>;
  sx?: SxProps;
}

export const ComponentLoader: React.FC<ComponentLoaderProps> = ({ operation, ...rest }) => {
  const api = useApi();
  const { user } = useUser();

  if (typeof operation === "string") {
    operation = api.operations[operation];
  }

  if (operation == null) {
    console.error(`[COMPONENT_LOADER] Could not find operation ${operation}`);
    return null;
  }

  if (operation.component?.component == null) {
    console.error(`[COMPONENT_LOADER] Could not find component for operation ${operation.id}`);
    return null;
  }

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

  rest.body ??= operation.component.body;
  // NOTE: Consider merging with the rest properties instead of
  // using the operation component's properties as defaults.
  rest.params ??= operation.component.params;
  rest.query ??= operation.component.query;
  rest.headers ??= operation.component.headers;

  return (
    <ComponentLoaderInner
      operation={operation as ApiOperation & Required<Pick<ApiOperation, "component">>}
      {...rest}
    />
  );
};

const ComponentLoaderInner: React.FC<ComponentLoaderInnerProps> = ({ operation, ...rest }) => {
  const throwError = useAsyncError();

  // TODO: Find a suitable type
  const [data, setData] = useState<unknown>();
  const initialLoad = useRef(true);

  const component = operation.component.component ?? ((() => null) as React.FC);
  const Component = useMemo(
    () => lazy(async () => ({ default: await Promise.resolve(component) })),
    [component],
  );

  const refresh = useCallback(async () => {
    const response = await operation.call(rest);
    if (response.ok) {
      setData(await response.json());
    } else {
      // TODO: Report to Sentry
      // TODO: Handle in the component, this will just unmount it.
      //       Things that suddenly dissapear are bit confusing, don't you think?
      console.error(
        `[COMPONENT_LOADER] Data load failed with ${response.status} ${response.statusText}`,
      );
      setData(null);
    }
  }, [operation, rest, throwError]);

  useEffect(() => {
    if (initialLoad.current) {
      initialLoad.current = false;
    } else {
      refresh();
    }
  }, [rest.query, rest.params]);

  return (
    <Suspense fallback={<LoadingScreen />}>
      {data == null ? null : <Component data={data} refresh={refresh} {...rest} />}
    </Suspense>
  );
};

export default ComponentLoader;
