import create from 'zustand';
import {immer} from 'zustand/middleware/immer';
import {enableMapSet} from 'immer';
import {WritableDraft} from 'immer/dist/types/types-external';
import {UploadedFile} from '../uploaded-file';
import {UploadStrategy, UploadStrategyConfig} from './strategy/upload-strategy';
import {MessageDescriptor} from '../../i18n/message-descriptor';
import {FileEntry} from '../file-entry';
import {S3MultipartUpload} from './strategy/s3-multipart-upload';
import {Settings} from '../../core/settings/settings';
import {Disk} from '../types/backend-metadata';
import {TusUpload} from './strategy/tus-upload';
import {AxiosUpload} from './strategy/axios-upload';
import {S3Upload} from './strategy/s3-upload';
import {ProgressTimeout} from './progress-timeout';
import {message} from '../../i18n/message';
import {validateUpload} from './validate-upload';
import {toast} from '../../ui/toast/toast';

enableMapSet();

interface FileUpload {
  file: UploadedFile;
  percentage: number;
  bytesUploaded: number;
  status: 'pending' | 'inProgress' | 'aborted' | 'failed' | 'completed';
  errorMessage?: string | MessageDescriptor;
  entry?: FileEntry;
  request?: UploadStrategy;
  timer?: ProgressTimeout;
}

export interface FileUploadState {
  concurrency: number;
  fileUploads: Map<string, FileUpload>;
  inProgressUploadsCount: number;
  completedUploadsCount: number;
  uploadMultiple: (
    files: (File | UploadedFile)[] | FileList,
    options?: Omit<
      UploadStrategyConfig,
      'onProgress' | 'showToastOnRestrictionFail'
    > // progress would be called for each upload simultaneously
  ) => string[];
  uploadSingle: (
    file: File | UploadedFile,
    options?: UploadStrategyConfig
  ) => string;
  clearInactive: () => void;
  abortUpload: (id: string) => void;
  updateFileUpload: (id: string, state: Partial<FileUpload>) => void;
  addFileUpload: (file: UploadedFile, options: Partial<FileUpload>) => void;
  runQueue: () => void;
}

interface StoreProps {
  settings: Settings;
}

export const createFileUploadStore = ({settings}: StoreProps) =>
  create<FileUploadState>()(
    immer((set, get) => ({
      concurrency: 3,
      fileUploads: new Map<string, FileUpload>(),
      inProgressUploadsCount: 0,
      completedUploadsCount: 0,

      clearInactive() {
        set(state => {
          state.fileUploads.forEach((upload, key) => {
            if (upload.status !== 'inProgress') {
              state.fileUploads.delete(key);
            }
          });
        });
        get().runQueue();
      },

      abortUpload(id) {
        const upload = get().fileUploads.get(id);
        if (upload) {
          upload.request?.abort();
          get().updateFileUpload(id, {status: 'aborted'});
          get().runQueue();
        }
      },

      updateFileUpload(id, newUploadState) {
        set(state => {
          const fileUpload = state.fileUploads.get(id);
          if (fileUpload) {
            state.fileUploads.set(id, {
              ...fileUpload,
              ...newUploadState,
            });

            // only need to update inProgress count if status of the uploads in queue change
            if ('status' in newUploadState) {
              updateTotals(state);
            }
          }
        });
      },

      addFileUpload(file, options) {
        set(state => {
          // abort upload after 30sec of no progress
          if (options.timer && options.request) {
            options.timer.timeoutHandler = () => {
              options.request?.abort();
              get().updateFileUpload(file.id, {
                status: 'failed',
                errorMessage: message('Upload timed out'),
              });
              get().runQueue();
            };
          }
          state.fileUploads.set(file.id, {
            file,
            percentage: 0,
            bytesUploaded: 0,
            status: 'pending',
            ...options,
          });
          updateTotals(state);
        });
      },

      runQueue() {
        const uploads = [...get().fileUploads.values()];
        const activeUploads = uploads.filter(u => u.status === 'inProgress');

        let concurrency = get().concurrency;
        if (
          activeUploads.filter(
            activeUpload =>
              // only upload one file from folder at a time to avoid creating duplicate folders
              activeUpload.file.relativePath ||
              // only allow one s3 multipart upload at a time, it will already upload multiple parts in parallel
              activeUpload.request instanceof S3MultipartUpload ||
              // only allow one tus upload if file is larger than chunk size, tus will have parallel uploads already in that case
              (activeUpload.request instanceof TusUpload &&
                settings.uploads.chunk_size &&
                activeUpload.file.size > settings.uploads.chunk_size)
          ).length
        ) {
          concurrency = 1;
        }

        if (activeUploads.length < concurrency) {
          const pendingUploads = uploads.filter(u => u.status === 'pending');
          const next = pendingUploads.find(a => !!a.request);
          if (next) {
            get().updateFileUpload(next.file.id, {
              status: 'inProgress',
            });
            next.request!.start();
          }
        }
      },

      uploadSingle(file, userOptions) {
        const uploadedFile =
          file instanceof UploadedFile ? file : new UploadedFile(file);

        if (userOptions?.restrictions) {
          const errorMessage = validateUpload(
            uploadedFile,
            userOptions.restrictions
          );
          if (errorMessage) {
            get().addFileUpload(uploadedFile, {
              errorMessage,
              status: 'failed',
            });

            if (userOptions.showToastOnRestrictionFail) {
              toast.danger(errorMessage);
            }

            get().runQueue();

            return uploadedFile.id;
          }
        }

        const timer = new ProgressTimeout();
        const config: UploadStrategyConfig = {
          metadata: {
            ...userOptions?.metadata,
            relativePath: uploadedFile.relativePath,
            disk: userOptions?.metadata?.disk || Disk.uploads,
            parentId: userOptions?.metadata?.parentId || '',
          },
          chunkSize: settings.uploads.chunk_size,
          onError: errorMessage => {
            get().updateFileUpload(uploadedFile.id, {
              errorMessage,
              status: 'failed',
            });
            get().runQueue();
            timer.done();
            userOptions?.onError?.();
          },
          onSuccess: entry => {
            get().updateFileUpload(uploadedFile.id, {
              status: 'completed',
              entry,
            });
            get().runQueue();
            timer.done();
            userOptions?.onSuccess?.(entry);
          },
          onProgress: ({bytesUploaded, bytesTotal}) => {
            const percentage = (bytesUploaded / bytesTotal) * 100;
            get().updateFileUpload(uploadedFile.id, {
              percentage,
              bytesUploaded,
            });
            timer.progress();
            userOptions?.onProgress?.({bytesUploaded, bytesTotal});
          },
        };

        const strategy = chooseUploadStrategy({
          settings,
          file: uploadedFile,
          strategyConfig: config,
        });

        strategy.create(uploadedFile, config).then(request => {
          get().addFileUpload(uploadedFile, {request, timer});
          get().runQueue();
        });

        return uploadedFile.id;
      },

      uploadMultiple: (files, options) => {
        return [...files].map(file => {
          return get().uploadSingle(file, options);
        });
      },
    }))
  );

const OneMB = 1024 * 1024;
const FourMB = 4 * OneMB;
const HundredMB = 100 * OneMB;

interface ChooseUploadStrategyProps {
  settings: Settings;
  strategyConfig: UploadStrategyConfig;
  file: UploadedFile;
}
const chooseUploadStrategy = ({
  settings,
  strategyConfig,
  file,
}: ChooseUploadStrategyProps) => {
  const disk = strategyConfig.metadata?.disk || Disk.uploads;
  const driver =
    disk === Disk.uploads
      ? settings.uploads.uploads_driver
      : settings.uploads.public_driver;

  if (driver?.endsWith('s3') && settings.uploads.s3_direct_upload) {
    return file.size >= HundredMB ? S3MultipartUpload : S3Upload;
  } else {
    // 4MB = Axios, otherwise Tus
    return file.size >= FourMB && !settings.uploads.disable_tus
      ? TusUpload
      : AxiosUpload;
  }
};

const updateTotals = (state: WritableDraft<FileUploadState>) => {
  state.completedUploadsCount = [...state.fileUploads.values()].filter(
    u => u.status === 'completed'
  ).length;
  state.inProgressUploadsCount = [...state.fileUploads.values()].filter(
    u => u.status === 'inProgress'
  ).length;
};
