import { useEffect, useRef } from 'react';

import { FileUpload, FileUploadProps } from '../../form/FileUpload/FileUpload';
import { FileUploadStatus } from '../../form/FileUpload/constants';
import { FileUploadFileMeta } from '../../form/FileUpload/types';
import { FormField } from '../../form/FormField/FormField';
import { assertEmptyObject } from '../../utils/assertEmptyObject';
import { makeTestId } from '../../utils/makeTestId';
import { uuidv4 } from '../../utils/uuidv4';
import { CommonFormikProps } from '../common/types';
import { useFormikFieldsProps } from '../hooks/useFormikFieldsProps';

type FileUploadTaskSubscribeHandler<T> = (state: FileUploadFileMeta<T>) => void;

interface FileUploadTask<T> {
  taskId: string;
  subscribe(handler: FileUploadTaskSubscribeHandler<T>): void;
  unsubscribe(): void;
  abort: void | (() => void);
}

/**
 * A function that performs file uploading.
 *
 * Inside this function should be implemented changing uploading statuses.
 *
 * It can return a function that will be called if the user will decide to abort uploading.
 *
 * ### Example
 * ```tsx
 * export const xhrUploadController: FormikFileUploadController<unknown> = ({ file, setState, getState }) => {
 *   const xhr = new XMLHttpRequest();
 *
 *   xhr.upload.onload = () => {
 *     const state = getState();
 *     setState({
 *       taskId: state.taskId,
 *       filename: state.filename,
 *       size: state.size,
 *       status: FileUploadStatus.Done,
 *       result: xhr.response,
 *     });
 *   };
 *
 *   xhr.upload.onerror = () => {
 *     const state = getState();
 *     setState({
 *       taskId: state.taskId,
 *       filename: state.filename,
 *       size: state.size,
 *       status: FileUploadStatus.Error,
 *       cause: xhr.status.toString(),
 *     });
 *   };
 *
 *   xhr.upload.onprogress = (e) => {
 *     const state = getState();
 *     setState({
 *       taskId: state.taskId,
 *       filename: state.filename,
 *       size: e.total,
 *       status: FileUploadStatus.Uploading,
 *       uploaded: e.loaded,
 *     });
 *   };
 *
 *   xhr.open('post', '/upload', true);
 *
 *   const formData = new FormData();
 *   formData.append('file', file);
 *   xhr.send(formData);
 *
 *   return () => {
 *     xhr.abort();
 *   };
 * };
 * ```
 *
 * @see FormikFileUpload
 */
export type FormikFileUploadController<T> = (params: {
  file: File;
  setState(value: FileUploadFileMeta<T>): void;
  getState(): FileUploadFileMeta<T>;
}) => void | (() => void);

/**
 * Creates upload task based on uploadController
 */
function createFileUploadTask<T>(
  file: File,
  uploadController: FormikFileUploadController<T>,
): FileUploadTask<T> {
  const taskId = uuidv4();
  let handlers: FileUploadTaskSubscribeHandler<T>[] = [];

  let state: FileUploadFileMeta<T> = {
    taskId,
    filename: file.name,
    status: FileUploadStatus.Uploading,
    size: file.size,
    uploaded: 0,
  };

  const abort = uploadController({
    file,
    getState: () => state,
    setState(data: FileUploadFileMeta<T>) {
      state = data;
      handlers.forEach((fn) => fn(state));
    },
  });

  return {
    taskId,
    abort,
    subscribe(handler: FileUploadTaskSubscribeHandler<T>) {
      handlers.push(handler);
      handler(state);
    },
    unsubscribe() {
      handlers = [];
    },
  };
}

export type FormikFileUploadProps<T = unknown> = Pick<
  FileUploadProps,
  'caption' | 'mimeTypes' | 'onInputError'
> & {
  /**
   * A function that performs file uploading.
   */
  uploadController: FormikFileUploadController<T>;
} & (
    | ({
        multiple: true;
      } & CommonFormikProps<FileUploadFileMeta<T>[]>)
    | ({
        multiple?: false;
      } & CommonFormikProps<FileUploadFileMeta<T> | null>)
  );

/**
 * Formik field that allows upload files.
 *
 * ```tsx
 * import { FormikFileUpload } from 'ui-kit';
 *
 * <FormikFileUpload
 *   name="files"
 *   label="Label"
 *   mimeTypes={['image/pdf']}
 *   uploadController={uploadController}
 * />
 * ```
 */
export function FormikFileUpload<T>(props: FormikFileUploadProps<T>) {
  const { id, formFieldProps, controlProps } = useFormikFieldsProps<FileUploadProps>(props as any);

  const {
    className,
    descriptionMessageId,
    description,
    errorMessageId,
    error,
    label,
    requirement,
    onHintClick,
    hintText,
    testId,
    ...restFormFieldProps
  } = formFieldProps;
  assertEmptyObject(restFormFieldProps);

  const tasks = useRef<Record<string, FileUploadTask<T>>>({});

  let value: FileUploadFileMeta<T>[] = [];
  if (controlProps.value) {
    if (Array.isArray(controlProps.value)) {
      value = [...controlProps.value];
    } else {
      value = [controlProps.value as FileUploadFileMeta<T>];
    }
  }

  useEffect(
    () => () => {
      Object.values(tasks.current).forEach((task) => task.unsubscribe());
    },
    [],
  );

  const handleSelect = (files: File[]) => {
    files.forEach((file) => {
      const task = createFileUploadTask(file, props.uploadController);
      tasks.current[task.taskId] = task;

      task.subscribe((data) => {
        let newValue: FileUploadFileMeta<T>[];
        if (props.multiple) {
          const index = value.findIndex((item) => item.taskId === data.taskId);
          newValue =
            index === -1
              ? [...value, data]
              : value.map((item) => (item.taskId === data.taskId ? data : item));
        } else {
          newValue = [data];
        }

        value = newValue;
        controlProps.onChange?.(props.multiple ? newValue : newValue[0]);
      });
    });
  };

  const handleCancel = (taskId: string) => {
    tasks.current[taskId]?.abort?.();
    tasks.current[taskId]?.unsubscribe();
    delete tasks.current[taskId];

    const newValue = value.filter((item) => item.taskId !== taskId);
    value = newValue;

    controlProps.onChange?.(props.multiple ? newValue : null);
  };

  return (
    <FormField
      className={className}
      description={description}
      descriptionMessageId={descriptionMessageId}
      error={error}
      errorMessageId={errorMessageId}
      hintText={hintText}
      id={id}
      label={label}
      onHintClick={onHintClick}
      requirement={requirement}
      testId={testId}
    >
      <FileUpload
        ariaDescribedBy={controlProps.ariaDescribedBy}
        ariaErrorMessage={controlProps.ariaErrorMessage}
        ariaInvalid={controlProps.ariaInvalid}
        ariaLabel={controlProps.ariaLabel}
        caption={props.caption}
        disabled={controlProps.disabled}
        fileList={value}
        id={id}
        mimeTypes={props.mimeTypes}
        multiple={props.multiple}
        onCancel={handleCancel}
        onInputError={props.onInputError}
        onSelect={handleSelect}
        testId={makeTestId(testId, 'file-upload')}
      />
    </FormField>
  );
}
