import { Plus as PlusIcon } from '@transferwise/icons';
import { clsx } from 'clsx';
import { Component } from 'react';
import { injectIntl, WrappedComponentProps } from 'react-intl';

import { Size, Typography } from '../common';
import Title from '../title';

import messages from './Upload.messages';
import { UploadImageStep, ProcessingStep, CompleteStep } from './steps';
import { postData, asyncFileRead, isSizeValid, isTypeValid, getFileType } from './utils';
import { PostDataFetcher, PostDataHTTPOptions, ResponseError } from './utils/postData/postData';

export const MAX_SIZE_DEFAULT = 5000000;

export enum UploadStep {
  UPLOAD_IMAGE_STEP = 'uploadImageStep',
}

export interface UploadProps extends WrappedComponentProps {
  /** @default 300 */
  animationDelay?: number;
  csButtonText?: string;
  csFailureText?: string;
  csSuccessText?: string;
  csTooLargeMessage?: string;
  csWrongTypeMessage?: string;
  httpOptions?: PostDataHTTPOptions & {
    fileInputName?: string;
    data?: Record<string, string | Blob>;
  };
  /**
   * You can provide a fetcher function with the same interface as the global fetch function, which is used by default.
   * `function fetcher(input: RequestInfo, init?: RequestInit): Promise<Response>`
   */
  fetcher?: PostDataFetcher;
  /**
   * Filesize expressed in B.<br />If set to `null`, no size limit will be applied.
   * @default 5000000 (5 MB)
   */
  maxSize?: number | null;
  psButtonText?: string;
  /** @default false */
  psButtonDisabled?: boolean;
  psProcessingText?: string;
  /** @default 'md' */
  size?: `${Size.SMALL | Size.MEDIUM | Size.LARGE}`;
  /**
   * You can provide multiple rules separated by comma, e.g.: "application/pdf,image/*".
   * Using "*" will allow every file type to be uploaded.
   * @default 'image/*'
   */
  usAccept?: string;
  usButtonText?: string;
  usButtonRetryText?: string;
  /** @default false */
  usDisabled?: boolean;
  usDropMessage?: string;
  usHelpImage?: React.ReactNode;
  /** @default '' */
  usLabel?: string;
  usPlaceholder?: string;
  /**
   * Override for the [InlinePrompt icon's default, accessible name](/?path=/docs/other-statusicon-accessibility--docs)
   * announced by the screen readers
   * */
  errorIconLabel?: string;
  /** @deprecated Only a single variant exists, please remove this prop. */
  uploadStep?: `${UploadStep}`;
  onCancel?: () => void;
  onFailure?: (error: unknown) => void;
  onStart?: (file: File) => void;
  onSuccess?: (response: string | Response, fileName: string) => void;
}

interface UploadState {
  fileName: string;
  isDroppable: boolean;
  isComplete: boolean;
  isError: boolean;
  isImage: boolean;
  isProcessing: boolean;
  isSuccess: boolean;
  response: unknown;
  uploadedImage: string | undefined;
}

export class Upload extends Component<UploadProps, UploadState> {
  declare props: UploadProps & Required<Pick<UploadProps, keyof typeof Upload.defaultProps>>;

  static defaultProps = {
    animationDelay: 300,
    maxSize: MAX_SIZE_DEFAULT,
    psButtonDisabled: false,
    size: 'md',
    usAccept: 'image/*',
    usDisabled: false,
    usLabel: '',
  } satisfies Partial<UploadProps>;

  dragCounter = 0;
  timeouts = 0;

  constructor(props: UploadProps) {
    super(props);

    this.state = {
      fileName: '',
      isDroppable: false,
      isComplete: false,
      isError: false,
      isImage: false,
      isProcessing: false,
      isSuccess: false,
      response: undefined,
      uploadedImage: undefined,
    };
  }

  getErrorMessage(status?: number) {
    const { csFailureText, csTooLargeMessage, csWrongTypeMessage, maxSize, intl } = this.props;
    switch (status) {
      case 413:
        return (
          csTooLargeMessage ||
          (typeof maxSize === 'number'
            ? intl.formatMessage(messages.csTooLargeMessage, { maxSize: roundFileSize(maxSize) })
            : intl.formatMessage(messages.csTooLargeNoLimitMessage))
        );
      case 415:
        return csWrongTypeMessage || intl.formatMessage(messages.csWrongTypeMessage);
      default:
        return csFailureText || intl.formatMessage(messages.csFailureText);
    }
  }

  onDragLeave(event: React.DragEvent<HTMLDivElement>) {
    event.preventDefault();
    this.dragCounter -= 1;
    if (this.dragCounter === 0) {
      this.setState({ isDroppable: false });
    }
  }

  onDragEnter(event: React.DragEvent<HTMLDivElement>) {
    event.preventDefault();
    this.dragCounter += 1;
    const { usDisabled } = this.props;
    const { isProcessing } = this.state;
    if (this.dragCounter === 1 && !usDisabled && !isProcessing) {
      this.setState({ isDroppable: true });
    }
  }

  async onDrop(event: React.DragEvent<HTMLDivElement>) {
    const { isProcessing } = this.state;
    event.preventDefault();
    if (!isProcessing) {
      this.reset();
    }

    if (event.dataTransfer?.files?.[0]) {
      await this.fileDropped(event.dataTransfer.files[0]);
    }
  }

  asyncPost = async (file: File) => {
    const { httpOptions, fetcher } = this.props;
    if (httpOptions == null) {
      throw new Error('Cannot find HTTP options');
    }

    const { fileInputName = file.name, data = {} } = httpOptions;

    const formData = new FormData();
    formData.append(fileInputName, file);
    Object.keys(data).forEach((key) => formData.append(key, data[key]));
    return postData(httpOptions, formData, fetcher);
  };

  handleUploadComplete = (type: 'success' | 'error', response: unknown) => {
    const { animationDelay, onSuccess, onFailure } = this.props;
    const { fileName } = this.state;

    window.clearTimeout(this.timeouts);
    this.timeouts = window.setTimeout(() => {
      this.setState(
        {
          isProcessing: false,
          isComplete: true,
        },
        () => {
          if (type === 'success') {
            onSuccess?.(response as string | Response, fileName);
          } else {
            onFailure?.(response);
          }
        },
      );
    }, animationDelay);
  };

  asyncResponse = (response: unknown, type: 'success' | 'error') => {
    this.setState(
      {
        response,
        isError: type === 'error',
        isSuccess: type === 'success',
      },
      () => this.handleUploadComplete(type, response),
    );
  };

  handleOnClear: React.MouseEventHandler<HTMLButtonElement> = (event) => {
    event.preventDefault();
    const { onCancel } = this.props;
    onCancel?.();

    this.reset();
  };

  reset = () => {
    this.dragCounter = 0;
    window.clearTimeout(this.timeouts);
    this.setState({
      isComplete: false,
      isError: false,
      isProcessing: false,
      isSuccess: false,
    });
  };

  showDataImage = (dataUrl: string) => {
    const { isImage } = this.state;
    if (isImage) {
      this.setState({
        uploadedImage: dataUrl,
      });
    }
  };

  fileDropped = async (file: File) => {
    const { httpOptions, maxSize, onStart, usDisabled, usAccept } = this.props;
    const { isProcessing } = this.state;

    if (usDisabled || isProcessing) {
      return false;
    }

    if (!file) {
      throw new Error('Could not retrieve file');
    }

    this.setState({
      fileName: file.name,
      isDroppable: false,
      isProcessing: true,
    });

    onStart?.(file);

    let file64 = null;

    try {
      file64 = await asyncFileRead(file);
    } catch (error) {
      this.asyncResponse(error, 'error');
    }

    if (!file64) {
      return false;
    }

    this.setState({
      isImage: getFileType(file, file64).includes('image'),
    });

    if (!isTypeValid(file, usAccept, file64)) {
      this.asyncResponse(
        new ResponseError(
          new Response(null, {
            status: 415,
            statusText: 'Unsupported Media Type',
          }),
        ),
        'error',
      );
      return false;
    }

    if (typeof maxSize === 'number' && !isSizeValid(file, maxSize)) {
      this.asyncResponse(
        new ResponseError(
          new Response(null, {
            status: 413,
            statusText: 'Request Entity Too Large',
          }),
        ),
        'error',
      );
      return false;
    }

    if (httpOptions) {
      // Post the file to provided endpoint
      let response;
      try {
        response = await this.asyncPost(file);
      } catch (error) {
        this.asyncResponse(error, 'error');
        return false;
      }
      this.asyncResponse(response, 'success');
      this.showDataImage(file64);
      return true;
    }
    // Post on form submit. And return the encoded image.
    this.showDataImage(file64);
    this.asyncResponse(file64, 'success');
    return true;
  };

  render() {
    const {
      maxSize,
      usDropMessage,
      usAccept,
      usButtonText,
      usButtonRetryText,
      usDisabled,
      usHelpImage,
      usLabel,
      usPlaceholder,
      psButtonText,
      psProcessingText,
      psButtonDisabled,
      csButtonText,
      csSuccessText,
      size,
      intl,
      errorIconLabel,
    } = this.props;

    const {
      response,
      fileName,
      isComplete,
      isDroppable,
      isError,
      isImage,
      isProcessing,
      isSuccess,
      uploadedImage,
    } = this.state;

    const placeholder = ((): string => {
      if (usPlaceholder) {
        return usPlaceholder;
      }

      if (typeof maxSize === 'number') {
        return intl.formatMessage(messages.usPlaceholder, { maxSize: roundFileSize(maxSize) });
      }

      return intl.formatMessage(messages.usPlaceholderNoLimit);
    })();

    return (
      <div
        className={clsx('droppable-area', {
          droppable: true,
          'tw-droppable-sm droppable-sm': size === 'sm',
          'tw-droppable-md droppable-md': size === 'md' || !size,
          'tw-droppable-lg droppable-lg': size === 'lg',
          'droppable-dropping': isDroppable,
          'droppable-processing': isProcessing,
          'droppable-complete': isComplete && !isError,
          'droppable-negative': isError && !isProcessing,
        })}
        onDragEnter={(event) => this.onDragEnter(event)}
        onDragLeave={(event) => this.onDragLeave(event)}
        onDrop={async (event) => this.onDrop(event)}
        onDragOver={(event) => event.preventDefault()}
      >
        {!isProcessing && !isComplete && (
          <UploadImageStep
            fileDropped={async (file) => this.fileDropped(file)}
            isComplete={isComplete}
            usAccept={usAccept}
            usButtonText={usButtonText || intl.formatMessage(messages.usButtonText)}
            usDisabled={usDisabled}
            usHelpImage={usHelpImage}
            usLabel={usLabel}
            usPlaceholder={placeholder}
          />
        )}

        {(isSuccess || isComplete) && !isError && (
          <CompleteStep
            fileName={fileName}
            isComplete={isComplete}
            isImage={isImage}
            csButtonText={csButtonText || intl.formatMessage(messages.csButtonText)}
            csSuccessText={csSuccessText || intl.formatMessage(messages.csSuccessText)}
            uploadedImage={uploadedImage}
            onClear={(event) => this.handleOnClear(event)}
          />
        )}
        {isError && !isProcessing && (
          <UploadImageStep
            fileDropped={async (file) => {
              this.reset();
              await this.fileDropped(file);
            }}
            isComplete={!isError}
            usAccept={usAccept}
            usButtonText={usButtonRetryText || intl.formatMessage(messages.retry)}
            usDisabled={usDisabled}
            usHelpImage={null}
            usLabel={usLabel}
            usPlaceholder={placeholder}
            errorMessage={this.getErrorMessage(
              response != null &&
                typeof response === 'object' &&
                'status' in response &&
                typeof response.status === 'number'
                ? response.status
                : undefined,
            )}
            errorIconLabel={errorIconLabel}
          />
        )}
        {isProcessing && (
          <ProcessingStep
            isComplete={isComplete}
            isError={isError}
            isSuccess={isSuccess}
            psButtonText={psButtonText || intl.formatMessage(messages.psButtonText)}
            psProcessingText={psProcessingText || intl.formatMessage(messages.psProcessingText)}
            psButtonDisabled={psButtonDisabled}
            onClear={(event) => this.handleOnClear(event)}
          />
        )}
        {!isProcessing && (
          <div className="droppable-dropping-card droppable-card" aria-live="polite">
            <div className="droppable-card-content">
              <div className="circle circle-sm text-primary">
                <PlusIcon size="16" />
              </div>
              <Title type={Typography.TITLE_BODY} className="m-t-3">
                {usDropMessage || intl.formatMessage(messages.usDropMessage)}
              </Title>
            </div>
          </div>
        )}
      </div>
    );
  }
}

export default injectIntl(Upload);

const roundFileSize = (bytes: number) => {
  const megabytes = bytes / 1000000;
  if (megabytes >= 0.1) {
    const isRound = Math.floor(megabytes) === megabytes;
    return isRound ? `${megabytes}` : megabytes.toFixed(1);
  }
  if (megabytes >= 0.01) {
    return megabytes.toFixed(2);
  }
  return megabytes.toFixed(3);
};
