import { clsx } from 'clsx';
import { useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';

import Button from '../button';
import { CommonProps, ControlType, Priority, Status } from '../common';
import { useInputAttributes } from '../inputs/contexts';
import Modal from '../modal';
import { isSizeValid } from '../upload/utils/isSizeValid';
import { isTypeValid } from '../upload/utils/isTypeValid';

import MESSAGES from './UploadInput.messages';
import { UploadedFile, UploadError, UploadResponse } from './types';
import UploadButton, { UploadButtonProps } from './uploadButton/UploadButton';
import { DEFAULT_SIZE_LIMIT, imageFileTypes } from './uploadButton/defaults';
import UploadItem, { UploadItemProps } from './uploadItem/UploadItem';

export type UploadInputProps = {
  /**
   * List of already existing, failed or in progress files
   */
  files?: readonly UploadedFile[];

  /**
   * The key of the file in the returned FormData object (default: file)
   */
  fileInputName?: string;

  /**
   * Callback that handles form submission
   *
   * @param formData
   */
  onUploadFile: (formData: FormData) => Promise<UploadResponse>;

  /**
   * Provide a callback if the file can be removed/deleted from the server
   * Your app is responsible for reloading the uploaded files list and updating the component to ensure that the file has in fact been deleted successfully
   *
   * @param id
   */
  onDeleteFile?: (id: string | number) => Promise<any>;

  /**
   * Provide a callback to trigger on validation error
   *
   * @param file
   */
  onValidationError?: (file: UploadedFile) => void;

  /**
   * Provide a callback to trigger on change whenever the files are updated
   *
   * @param files
   */
  onFilesChange?: (files: UploadedFile[]) => void;

  /**
   * Confirmation modal displayed on delete
   */
  deleteConfirm?: {
    /**
     * The title of the confirmation modal on delete
     */
    title?: string;

    /**
     * The body of the confirmation modal on delete
     */
    body?: React.ReactNode;

    /**
     * The confirm button text of the confirmation modal on delete
     */
    confirmText?: string;

    /**
     * The cancel button text of the confirmation modal on delete
     */
    cancelText?: string;
  };

  /**
   * Maximum number of files allowed, if provided, shows error below file item
   */
  maxFiles?: number;

  /**
   * Error message to show when the maximum number of files are uploaded already
   */
  maxFilesErrorMessage?: string;

  /**
   * Error message to show when files over allowed size limit are uploaded
   */
  sizeLimitErrorMessage?: string;
} & Pick<
  UploadButtonProps,
  'disabled' | 'multiple' | 'fileTypes' | 'sizeLimit' | 'description' | 'id' | 'uploadButtonTitle'
> & { onDownload?: UploadItemProps['onDownload'] } & CommonProps;

/**
 * Interface representing a reference to an UploadItem component.
 * Provides a method to focus the UploadItem.
 */
interface UploadItemRef {
  /**
   * Focuses the UploadItem component.
   */
  focus: () => void;

  /**
   * Required id of the UploadItem component.
   */
  id: string | number;

  /**
   * Optional status of the UploadItem component.
   */
  status?: string;
}

/**
 * Generates a unique ID for a file based on its name, size, and the current timestamp
 */
function generateFileId(file: File) {
  const { name, size } = file;
  const uploadTimeStamp = new Date().getTime();
  return `${name}_${size}_${uploadTimeStamp}`;
}

/**
 * The component allows users to upload files, manage the list of uploaded files,
 * and handle file validation and deletion.
 *
 * @param {UploadInputProps} props - The properties for the UploadInput component.
 *
 * @see {@link UploadInput} for further information.
 * @see {@link https://storybook.wise.design/?path=/docs/forms-uploadinput--docs|Storybook Wise Design}
 */
const UploadInput = ({
  files = [],
  fileInputName = 'file',
  className,
  deleteConfirm,
  disabled,
  multiple = false,
  fileTypes = imageFileTypes,
  sizeLimit = DEFAULT_SIZE_LIMIT,
  description,
  onUploadFile,
  onDeleteFile,
  onValidationError,
  onFilesChange,
  onDownload,
  maxFiles,
  maxFilesErrorMessage,
  id,
  sizeLimitErrorMessage,
  uploadButtonTitle,
}: UploadInputProps) => {
  const inputAttributes = useInputAttributes({ nonLabelable: true });
  const [markedFileForDelete, setMarkedFileForDelete] = useState<UploadedFile | null>(null);
  const [lastAttemptedDeleteId, setLastAttemptedDeleteId] = useState<string | number | null>(null);
  const [mounted, setMounted] = useState(false);
  const { formatMessage } = useIntl();
  const uploadInputRef = useRef<HTMLInputElement | null>(null);
  let fileRefs: (HTMLDivElement | UploadItemRef | null)[] = [];

  const PROGRESS_STATUSES = new Set([Status.PENDING, Status.PROCESSING]);

  const [uploadedFiles, setUploadedFiles] = useState<readonly UploadedFile[]>(
    multiple || files.length === 0 ? files : [files[0]],
  );

  const uploadedFilesListReference = useRef(multiple || files.length === 0 ? files : [files[0]]);

  function updateFileList(updateFn: (list: readonly UploadedFile[]) => readonly UploadedFile[]) {
    setUploadedFiles(updateFn);
    uploadedFilesListReference.current = updateFn(uploadedFilesListReference.current);
  }

  function addFileToList(recentUploadedFile: UploadedFile) {
    updateFileList((list) => [...list, recentUploadedFile]);
  }

  function removeFileFromList(file: UploadedFile) {
    updateFileList((list) =>
      list.filter((fileInList) => file !== fileInList && file.id !== fileInList.id),
    );
    fileRefs = fileRefs.filter((ref) => ref && ref.id !== file.id);
  }

  function modifyFileInList(file: UploadedFile, updates: Partial<UploadedFile>) {
    updateFileList((list) =>
      list.map((fileInList) =>
        fileInList === file || fileInList.id === file.id ? { ...file, ...updates } : fileInList,
      ),
    );
  }

  const removeFile = async (file: UploadedFile) => {
    const { id, status } = file;
    fileRefs = fileRefs.filter((item) => item && item.id !== file.id);

    if (status === Status.FAILED) {
      removeFileFromList(file);
      return Promise.resolve();
    }

    if (onDeleteFile && id) {
      modifyFileInList(file, { status: Status.PROCESSING, error: undefined });

      return onDeleteFile(id)
        .then(() => {
          removeFileFromList(file);
        })
        .catch((error) => {
          modifyFileInList(file, { error: error as UploadError });
        });
    }
  };

  function handleFileUploadFailure(file: File, failureMessage: string) {
    const { name } = file;

    const failedUpload = {
      id: generateFileId(file),
      filename: name,
      status: Status.FAILED,
      error: failureMessage,
    };

    addFileToList(failedUpload);

    if (onValidationError) {
      onValidationError(failedUpload);
    }
  }

  function getNumberOfFilesUploaded() {
    const uploadInitiatedStatus = new Set([Status.SUCCEEDED, Status.PENDING]);
    const validFiles = uploadedFilesListReference.current.filter(
      (file) => file.status && uploadInitiatedStatus.has(file.status),
    );
    return validFiles.length;
  }

  function areMaximumFilesUploadedAlready() {
    if (!maxFiles) {
      return false;
    }

    const numberOfValidFiles = getNumberOfFilesUploaded();
    return numberOfValidFiles >= maxFiles;
  }

  const addFiles = (selectedFiles: FileList) => {
    for (let i = 0; i < selectedFiles.length; i += 1) {
      const file = selectedFiles.item(i);

      const formData = new FormData();

      if (file) {
        const allowedFileTypes = typeof fileTypes === 'string' ? fileTypes : fileTypes.join(',');

        if (!isTypeValid(file, allowedFileTypes)) {
          handleFileUploadFailure(file, formatMessage(MESSAGES.fileTypeNotSupported));
          continue;
        }

        if (typeof sizeLimit === 'number' && !isSizeValid(file, sizeLimit * 1000)) {
          const failureMessage = sizeLimitErrorMessage || formatMessage(MESSAGES.fileIsTooLarge);
          handleFileUploadFailure(file, failureMessage);
          continue;
        }

        if (areMaximumFilesUploadedAlready()) {
          const failureMessage =
            maxFilesErrorMessage ||
            formatMessage(MESSAGES.maximumFilesAlreadyUploaded, { maxFilesAllowed: maxFiles });
          handleFileUploadFailure(file, failureMessage);
          continue;
        }

        const existingFile = uploadedFiles.find((f) => f.filename === file.name);
        if (existingFile) {
          removeFileFromList(existingFile);
        }

        formData.append(fileInputName, file);
        const pendingFile = {
          id: generateFileId(file),
          filename: file.name,
          status: Status.PENDING,
        };

        addFileToList(pendingFile);

        onUploadFile(formData)
          .then(({ id, url, error }: UploadResponse) => {
            modifyFileInList(pendingFile, { id, url, error, status: Status.SUCCEEDED });
          })
          .catch((error) => {
            modifyFileInList(pendingFile, { error: error as UploadError, status: Status.FAILED });
          });

        if (!multiple) {
          break;
        }
      }
    }
  };

  useEffect(() => {
    setMounted(true);
  }, []);

  useEffect(() => {
    if (onFilesChange && mounted) {
      onFilesChange([...uploadedFiles]);
    }
  }, [onFilesChange, uploadedFiles]); // eslint-disable-line react-hooks/exhaustive-deps

  type NextFocusable =
    | HTMLDivElement
    | UploadItemRef
    | { ref: HTMLDivElement | UploadItemRef; target: 'button' | 'link' }
    | null;

  const [nextFocusable, setNextFocusable] = useState<NextFocusable>(uploadInputRef.current);

  const handleFocus = (fileId: string | number) => {
    fileRefs = fileRefs.filter((ref) => {
      return ref && ref.id !== markedFileForDelete?.id;
    });

    const filesCount = fileRefs.length;
    let next: UploadItemRef | HTMLDivElement | null = uploadInputRef.current;
    let focusTarget: 'button' | 'link' = 'button';

    // If there will be no files left after deletion, focus the upload button
    if (filesCount === 1) {
      next = uploadInputRef.current;
      setNextFocusable(next);
      return;
    }

    if (filesCount > 1) {
      const currentFileIndex = fileRefs.findIndex((file) => file?.id === fileId);
      const currentFileId = fileRefs?.[currentFileIndex]?.id;
      const lastFileId = fileRefs?.[filesCount - 1]?.id;

      // if last file, select a previous one
      if (currentFileId === lastFileId) {
        next = fileRefs[filesCount - 2];
      } else {
        next = fileRefs[currentFileIndex + 1];
      }

      // If next is an UploadItemRef, check if it has a URL (succeeded)
      if (next && 'status' in next) {
        // Find the file object for this ref
        const fileObj = uploadedFiles.find((f) => f.id === next?.id);
        if (
          fileObj &&
          (fileObj.status === Status.SUCCEEDED || fileObj.status === Status.DONE) &&
          fileObj.url
        ) {
          focusTarget = 'link';
        }
      }
      setNextFocusable(() => {
        if (next && typeof (next as UploadItemRef).focus === 'function') {
          return { ref: next, target: focusTarget };
        }
        return next;
      });
    }
  };

  const handleRefocus = () => {
    const focusTarget = nextFocusable;
    if (lastAttemptedDeleteId) {
      setLastAttemptedDeleteId(null);
      return;
    }
    if (focusTarget) {
      // If there are no files left, focus the upload button
      if (
        uploadedFiles.length === 0 &&
        uploadInputRef.current &&
        typeof uploadInputRef.current.focus === 'function'
      ) {
        setTimeout(() => {
          uploadInputRef.current!.focus();
        }, 0);
      } else if (
        typeof focusTarget === 'object' &&
        'ref' in focusTarget &&
        focusTarget.ref &&
        typeof focusTarget.ref.focus === 'function'
      ) {
        setTimeout(() => {
          if (focusTarget.ref && typeof (focusTarget.ref as UploadItemRef).focus === 'function') {
            // @ts-expect-error: focus may not exist on all possible ref types, but is safe here
            (focusTarget.ref as UploadItemRef).focus(focusTarget.target);
          }
        }, 0);
      } else if (focusTarget && typeof (focusTarget as UploadItemRef).focus === 'function') {
        setTimeout(() => {
          (focusTarget as UploadItemRef).focus();
        }, 0);
      }
    }
  };

  return (
    <>
      <div
        role="group"
        className={clsx('np-upload-input', className, { disabled })}
        {...inputAttributes}
      >
        <div
          className="np-upload-input__section"
          aria-live="polite"
          aria-relevant="all"
          role="region"
        >
          {uploadedFiles.map((file, index) => (
            <UploadItem
              key={file.id}
              ref={(el: UploadItemRef | null) => {
                if (
                  el &&
                  el.id !== markedFileForDelete?.id &&
                  !fileRefs.some((ref) => ref && ref.id === el.id) &&
                  el.status !== 'processing'
                ) {
                  fileRefs.push(el);
                }
              }}
              file={file}
              singleFileUpload={!multiple}
              canDelete={
                (!!onDeleteFile || file.status === Status.FAILED) &&
                (!file.status || !PROGRESS_STATUSES.has(file.status))
              }
              onDelete={
                file.status === Status.FAILED
                  ? async () => {
                      setLastAttemptedDeleteId(file.id);
                      await removeFile(file);
                      handleRefocus();
                    }
                  : () => {
                      setLastAttemptedDeleteId(file.id);
                      setMarkedFileForDelete(file);
                    }
              }
              onDownload={onDownload}
              onFocus={() => handleFocus(file.id)}
            />
          ))}
        </div>
        {(multiple || (!multiple && !uploadedFiles.length)) && (
          <div className="np-upload-input__section np-upload-input__section--uploader">
            <UploadButton
              ref={uploadInputRef}
              id={id}
              uploadButtonTitle={uploadButtonTitle}
              disabled={areMaximumFilesUploadedAlready() || disabled}
              multiple={multiple}
              fileTypes={fileTypes}
              sizeLimit={sizeLimit}
              description={description}
              maxFiles={maxFiles}
              withEntries={Boolean(uploadedFiles.length)}
              onChange={addFiles}
            />
          </div>
        )}
      </div>
      <Modal
        title={
          deleteConfirm?.title !== undefined
            ? deleteConfirm.title
            : formatMessage(MESSAGES.deleteModalTitle)
        }
        body={
          deleteConfirm?.body !== undefined
            ? deleteConfirm.body
            : formatMessage(MESSAGES.deleteModalBody)
        }
        open={!!markedFileForDelete}
        footer={
          <>
            <Button
              block
              onClick={() => {
                setMarkedFileForDelete(null);
              }}
            >
              {deleteConfirm?.cancelText || formatMessage(MESSAGES.deleteModalCancelButtonText)}
            </Button>
            <Button
              block
              priority={Priority.SECONDARY}
              type={ControlType.NEGATIVE}
              tabIndex={markedFileForDelete ? 0 : -1}
              onClick={() => {
                if (markedFileForDelete) {
                  void removeFile(markedFileForDelete);
                }
                setMarkedFileForDelete(null);
                setLastAttemptedDeleteId(null);
              }}
            >
              {deleteConfirm?.confirmText || formatMessage(MESSAGES.deleteModalConfirmButtonText)}
            </Button>
          </>
        }
        onUnmount={handleRefocus}
        onClose={() => {
          setMarkedFileForDelete(null);
        }}
      />
    </>
  );
};

export default UploadInput;
