import clsx from 'clsx';
import React, {
  PropsWithChildren,
  ReactElement,
  useCallback,
  useEffect,
  useState,
} from 'react';
import { ValidationError } from 'yup';
import { OptionalObjectSchema } from 'yup/lib/object';
import { noop } from '../../../helpers/utils';
import { Data } from '../../../types/data';
import { Button, ButtonContext } from '../../Buttons';
import { ObjectSchemaDefinition } from '../../FormStation';
import { IconName, Icons } from '../../Icons';
import { DynamicListColumn } from '../DynamicDataList.model';
import classes from './DynamicListDataEntry.scss';

export enum DynamicListDataEntryMode {
  Add,
  Edit,
}

export interface DynamicListDataEntryProps<T extends Data> {
  /** The column definition */
  columns: DynamicListColumn<T>[];
  /** Spacing between columns */
  columnSizes: string;
  /** Header row height */
  columnGap?: string;
  /** List row height */
  rowHeight?: string;
  /** Size of action button and checkbox */
  actionSize?: string;
  /** Horizontal alignment of text */
  horizontalTextAlign?: 'left' | 'right' | 'center';
  /** Vertical alignment of text */
  verticalTextAlign?: 'start' | 'center' | 'end';
  /** Property name that is used to determine data position (default: undefined) */
  positionKey?: keyof T;
  /** Whether add new item button should be disabled */
  disabled?: boolean;
  /** If set to true, rows can be be repositioned using the input field (default: undefined) */
  allowReordering?: boolean;
  /** If set to true, this component can be dragged (default: undefined) */
  allowDragging?: boolean;
  /** Position the new data will receive */
  newDataPosition?: number;
  /** Optional Yup validation object for validating new data */
  rowValidationSchema?: OptionalObjectSchema<ObjectSchemaDefinition<T>>;
  /** Sets default values to be populated in the new row when new data is allowed. */
  defaultValuesForNewData?: Partial<T>;
  /** Overrides the default action button context behavior */
  actionButtonContext?: ButtonContext;
  /** CSS Class name for additional styles */
  className?: string;
  /** Whether Data Entry row should stick to the top below Header (default: true) */
  sticky?: boolean;
  /** If header is shown */
  showHeader?: boolean;
  /** If set to true, the position column will be shown (default: false) */
  showPositionColumn?: boolean;
  /** Determines if the component is in Add or Edit mode (default: Add) */
  mode?: DynamicListDataEntryMode;
  /** Emits when the action button is clicked. Data is supplied as a parameter */
  onActionClicked?: (data: T) => void;
  /** override default add button (+) */
  customAddButton?: (onAddItem: () => Promise<void>) => ReactElement;
}

export const DynamicListDataEntry = <T extends Data>({
  columns,
  columnSizes,
  columnGap,
  rowHeight,
  actionSize,
  horizontalTextAlign,
  verticalTextAlign,
  positionKey,
  disabled = false,
  allowReordering,
  allowDragging,
  newDataPosition,
  rowValidationSchema,
  defaultValuesForNewData,
  actionButtonContext,
  className = '',
  sticky = true,
  showHeader,
  showPositionColumn = false,
  mode = DynamicListDataEntryMode.Add,
  onActionClicked = noop,
  customAddButton,
}: PropsWithChildren<DynamicListDataEntryProps<T>>): JSX.Element => {
  const customStyles = {
    gridAutoRows: `minmax(50px, ${rowHeight})`,
    gridTemplateColumns: columnSizes,
    gridColumnGap: columnGap,
    justifyItems: horizontalTextAlign,
    alignItems: verticalTextAlign,
  } as React.CSSProperties;

  if (sticky) {
    customStyles.position = 'sticky';
    customStyles.top = showHeader ? '50px' : 0;
  }

  const [state, setState] = useState<T>(
    (defaultValuesForNewData as T) ?? ({} as T),
  );
  const [isDirty, setIsDirty] = useState<boolean>(false);
  const [error, setError] = useState<Record<string, string>>({});

  const validateSchema = useCallback(
    async (data: T): Promise<boolean> => {
      if (!rowValidationSchema) {
        return true;
      }

      try {
        await rowValidationSchema.validate(data, { abortEarly: false });
        setError({});

        return true;
      } catch (e) {
        const newErrors: Record<string, string> = {};
        (e as ValidationError).inner.forEach((validationError) => {
          const path = validationError.path;
          if (path !== undefined && newErrors?.[path] === undefined) {
            newErrors[path] = validationError.message;
          }
        });
        setError(newErrors);

        return false;
      }
    },
    [rowValidationSchema],
  );

  // Input errors should be cleared when the field is disabled
  useEffect(() => {
    if (disabled) {
      setError({});
    } else if (isDirty) {
      validateSchema(state);
    }
  }, [disabled, state, isDirty, validateSchema]);

  /**
   * Updates new data object with supplied key/value pair
   * @param property data property
   * @param value new value
   */
  const valueChangedHandler = (property: keyof T, value: unknown): void => {
    const newState = { ...state, [property]: value };

    setIsDirty(true);
    setState(newState);

    validateSchema(newState);
  };

  /**
   * Emits data with new position if required props are set, else just emits the data
   * @param data T
   */
  const onAddItemHandler = async (): Promise<void> => {
    const isValid = await validateSchema(state);

    if (!isValid) {
      setIsDirty(true);
      return;
    }

    let transformedState: T = state;
    for (const { onAddTransformer, propertyName } of columns) {
      if (onAddTransformer && propertyName) {
        transformedState = {
          ...transformedState,
          [propertyName]: onAddTransformer(
            transformedState[propertyName],
            state,
          ),
        };
      }
    }

    if (
      allowReordering &&
      positionKey !== undefined &&
      newDataPosition !== undefined
    ) {
      onActionClicked({ ...transformedState, [positionKey]: newDataPosition });
    } else {
      onActionClicked(transformedState);
    }

    // Reset the state back to an empty object
    setIsDirty(false);
    setState((defaultValuesForNewData as T) ?? ({} as T));
  };

  return (
    <div
      className={clsx(
        classes.container,
        'dynamic-list-data-entry-container',
        className,
      )}
      style={customStyles}
      data-test-id="dynamic-list-data-entry"
    >
      {showPositionColumn && (
        <div className={clsx(classes.position)}>
          {allowDragging && (
            <div className={classes.draggable}>
              <Icons icon={IconName.Drag} className={clsx(classes.dragIcon)} />
            </div>
          )}

          <div className={clsx(classes.input)}>
            <input disabled={true} type="text" value={newDataPosition} />
          </div>
        </div>
      )}
      {columns.map((column) => (
        <div
          key={column.propertyName as string}
          className={clsx(classes.column)}
          data-test-id={`dynamic-list-data-entry-property:${
            column.propertyName as string
          }`}
        >
          {renderField<T>(
            column,
            column.propertyName && state[column.propertyName],
            error[column.propertyName as string],
            column.propertyName
              ? (value) =>
                  valueChangedHandler(column.propertyName as keyof T, value)
              : noop,
            disabled,
            mode,
          )}
        </div>
      ))}
      <div className={clsx(classes.actionButtonContainer)}>
        {(customAddButton && customAddButton(onAddItemHandler)) || (
          <Button
            type="button"
            className={clsx(classes.actionButton)}
            icon={
              mode === DynamicListDataEntryMode.Add
                ? IconName.Plus
                : IconName.Checkmark
            }
            buttonContext={
              actionButtonContext === undefined
                ? isDirty
                  ? ButtonContext.Active
                  : ButtonContext.Icon
                : actionButtonContext
            }
            height={actionSize}
            width={actionSize}
            onButtonClicked={onAddItemHandler}
            dataTestId="dynamic-list-add-button"
            disabled={disabled}
          />
        )}
      </div>
    </div>
  );
};

const renderField = function <T extends Data>(
  column: DynamicListColumn<T>,
  currentValue: unknown,
  error: string | undefined,
  onValueChanged: (value: unknown) => void,
  disabled = false,
  mode: DynamicListDataEntryMode = DynamicListDataEntryMode.Add,
): React.ReactNode {
  if (mode === DynamicListDataEntryMode.Edit && column.dataEditRender) {
    return column.dataEditRender(currentValue, error, onValueChanged, disabled);
  } else if (column.dataEntryRender) {
    return column.dataEntryRender(
      currentValue,
      error,
      onValueChanged,
      disabled,
    );
  } else {
    return '';
  }
};
