import React, { useCallback, useMemo, useState } from "react";

import {
  Card as MuiCard,
  CardProps as MuiCardProps,
  Grid2 as Grid,
  Grid2Props,
} from "@mui/material";

import { useSnackbar } from "notistack";

import { CardContext } from "../../contexts/CardContext";
import { useI18n } from "../../contexts/I18nContext";
import { ValidationError } from "../../util/form_validation";

export * from "./shared";

export interface CardProps<T = unknown> extends Omit<MuiCardProps, "onSubmit"> {
  /**
   * If true, the card will always be in edit mode.
   * Overrides, the `isEditable` prop.
   * Does not affect `isDisabled` or `isOpen`.
   * Keeps the card in `edit` mode even after submitting.
   */
  alwaysEditable?: boolean;
  /**
   * Number of columns to use in the grid.
   * Not neccary to include unless you are trying to achive
   * a very specific layout with multi-column spanning fields or gaps.
   */
  columns?: number;
  /**
   * Start the card in `edit` mode. Should be used with `isEdtiable`.
   * The card will still be able to exit `edit` mode. This is just the default state.
   * Use `alwaysEditable` to keep the card in `edit` mode permanently.
   */
  defaultEditing?: boolean;
  /**
   * Enables `edit` mode and the toggle in `CardHeader` if true.
   * Use `alwaysEditable` to keep the card in `edit` mode permanently.
   * Use `defaultEditing` to set the default state of the card.
   */
  isEditable?: boolean;
  /**
   * Disables the card and all fields if true.
   * This is a global disable, not a per-field disable.
   * Only makes sense when `isEditable` is true.
   */
  isDisabled?: boolean;
  /**
   * Uses compact layout for card and all fields if true.
   * Compact cards are designed to be placed in `RightColumn`.
   */
  isCompact?: boolean;
  /**
   * Enables accordion-like behavior if true.
   * Does not affect `isEditable` or `isDisabled`.
   */
  isCollapsible?: boolean;
  /**
   * Start the card in `open` mode. Should be used with `isCollapsible`.
   * The card will still be able to close. This is just the default state.
   */
  isOpen?: boolean;
  /**
   * Function to call when the form is submitted.
   * All card fields should be treaded as uncontrolled and access via standard DOM Form APIs.
   * The first arguments provies `Object.fromEntries(new FormData(form))` for convenience.
   * The second argument is the form event. This can be used to `getAll` or `get` form values
   * which are "unserializable" by `Object.fromEntries` (such as shared `FormName`s).
   *
   * The function should return a string to display as a success message.
   * If the function returns `null` or `undefined`, no message will be displayed.
   * Shows a generic error message if the function throws an error.
   * You can show your own error message by catching the error, queueing a snackbar and returning null.
   *
   * Type this function using the generic version of the card component.
   *
   * @example
   * ```tsx
   * interface MyCardValues {
   *   name: string;
   *   email: string;
   *   }
   *
   * ...
   *
   * <Card<MyCardValues> onSubmit={...} />
   */
  onSubmit?: (
    values: T,
    event: React.FormEvent<HTMLFormElement>,
  ) => Promise<string | null | undefined>;
  /**
   * The MUI `Grid2` size of the card.
   * Defaults to `12` (full width), which is what you want in most cases.
   * @see https://mui.com/material-ui/react-grid2/
   */
  size?: Grid2Props["size"];
  /**
   * Additional props to pass to the `Grid2` component.
   * @see https://mui.com/material-ui/react-grid2/
   */
  gridProps?: Omit<Grid2Props, "size">;
}

function formDataToObject(
  formData: FormData,
): Record<string, FormDataEntryValue | FormDataEntryValue[]> {
  const values: Record<string, FormDataEntryValue | FormDataEntryValue[]> = {};

  for (const [key, value] of formData.entries()) {
    const existing = values[key];

    if (existing == null) {
      values[key] = value;
    } else if (Array.isArray(existing)) {
      existing.push(value);
    } else {
      values[key] = [existing, value];
    }
  }

  return values;
}

/**
 * A card component that wraps a form with `onSubmit` and provides `cardContext`.
 * This should be your building block for all admin forms not better represented by a Table.
 * Generic type `T` is used to type the `FormData` of `onSubmit`.
 * @example
 * ```tsx
 * interface MyCardValues {
 *   name: string;
 *   email: string;
 * }
 *
 * <Card<MyCardValues>
 *   onSubmit={(values) => {
 *     console.log(values);
 *     return "Success!";
 *  }}>
 *   <CardHeader title="My Card" />
 *   <CardContent>
 *     <CardRow>
 *       <TextField name="name" label="Name" />
 *       <TextField name="email" label="Email" />
 *     <CardRow/>
 *   </CardContent>
 *   <CardActions>
 *     <CardCancelButton />
 *     <CardSaveButton />
 *   </CardActions>
 * </Card>
 */
export function Card<T>({
  alwaysEditable = false,
  children,
  columns = 1,
  defaultEditing = false,
  isCollapsible = false,
  isCompact = false,
  isDisabled: isDisabledDefault = false,
  isEditable,
  isOpen: isOpenDefault = false,
  onSubmit,
  sx,
  size,
  gridProps,
  ...props
}: CardProps<T>) {
  isEditable ??= alwaysEditable;

  const { t } = useI18n();
  const { enqueueSnackbar } = useSnackbar();

  const [isEditing, setIsEditing] = useState(alwaysEditable || defaultEditing || false);
  const [hasChanges, setHasChanges] = useState(false);
  const [isDisabled, setIsDisabled] = useState(isDisabledDefault);
  const [isOpen, setIsOpen] = useState(isOpenDefault);

  const handleSubmit = useCallback(
    async (event: React.FormEvent<HTMLFormElement>) => {
      event.preventDefault();

      if (onSubmit == null) throw new Error("No onSubmit function provided.");
      if (!isEditing && !alwaysEditable) throw new Error("Cannot submit when not in edit mode.");

      const form = event.currentTarget;
      const formData = new FormData(form);
      const values = formDataToObject(formData);

      setIsDisabled(true);

      try {
        const success = await onSubmit(values as T, event);
        if (success != null) {
          enqueueSnackbar(success, { variant: "success" });
        }

        if (!alwaysEditable) {
          setIsEditing(false);
        }

        setIsDisabled(false);
      } catch (error) {
        // TODO Report error to sentry
        console.error("[CARD] Error when submitting data", error);
        if (error instanceof ValidationError) {
          const message =
            error.formErrors.at(0) ??
            t("Invalid form input. Please correct the highlighted fields.");
          enqueueSnackbar(message, { variant: "error" });
        } else {
          enqueueSnackbar(t("Failed to submit data. Contact support or try again later."), {
            variant: "error",
          });
        }

        // Don't exit edit mode (and clear fields) on error. Give the user the chance to save/fix.
        setIsDisabled(false);
      }
    },
    [onSubmit, enqueueSnackbar, t, isEditing, alwaysEditable],
  );

  const contextValues = useMemo(
    () => ({
      alwaysEditable,
      columns,
      hasChanges,
      isCollapsible,
      isCompact,
      isDisabled,
      isEditable,
      isEditing,
      isOpen,
      setHasChanges,
      setIsEditing,
      setIsOpen,
    }),
    [isDisabled, isEditable, isEditing, columns, hasChanges, isOpen, isCompact, isCollapsible],
  );

  return (
    <CardContext.Provider value={contextValues}>
      <Grid size={size ?? 12} {...gridProps}>
        <MuiCard
          component={isEditable || alwaysEditable ? "form" : "article"}
          sx={{
            width: "100%",
            boxShadow: 0,
            borderRadius: 3,
            borderWidth: "1px",
            borderStyle: "solid",
            borderColor: (theme) => theme.palette.divider,
            bgcolor: (theme) => theme.palette.background.paper,
            overflow: "visible",
            ...sx,
          }}
          onSubmit={isEditable || isEditing ? handleSubmit : undefined}
          {...props}
        >
          {children}
        </MuiCard>
      </Grid>
    </CardContext.Provider>
  );
}

export default Card;
