import { DirectUpload } from '@rails/activestorage';
import { httpRequest, ResponseError } from '@utils';
import ProgressBar from './progress-bar';
import {
  FileUploadError,
  errorFromDirectUploadMessage,
  ERROR_CODE_ATTACH
} from './file-upload-error';

const BYTES_TO_MB_RATIO = 1_048_576;
/**
  Uploader class is a delegate for DirectUpload instance
  used to track lifecycle and progress of an upload.
  */
export default class Uploader {
  directUpload: DirectUpload;
  progressBar: ProgressBar;
  autoAttachUrl?: string;
  maxFileSize: number;
  file: File;

  constructor(
    input: HTMLInputElement,
    file: File,
    directUploadUrl: string,
    autoAttachUrl?: string,
    maxFileSize?: string
  ) {
    this.file = file;
    this.directUpload = new DirectUpload(file, directUploadUrl, this);
    this.progressBar = new ProgressBar(input, this.directUpload.id + '', file);
    this.autoAttachUrl = autoAttachUrl;
    try {
      this.maxFileSize = parseInt(maxFileSize || '0', 10);
    } catch (e) {
      this.maxFileSize = 0;
    }
  }

  /**
    Upload (and optionally attach) the file.
    Returns the blob signed id on success.
    Throws a FileUploadError on failure.
    */
  async start() {
    this.progressBar.start();
    if (this.maxFileSize > 0 && this.file.size > this.maxFileSize) {
      throw `La taille du fichier ne peut dépasser
             ${this.maxFileSize / BYTES_TO_MB_RATIO} Mo
             (in english: File size can't be bigger than
             ${this.maxFileSize / BYTES_TO_MB_RATIO} Mo).`;
    }
    try {
      const blobSignedId = await this.upload();

      if (this.autoAttachUrl) {
        await this.attach(blobSignedId, this.autoAttachUrl);
        // On response, the attachment HTML fragment will replace the progress bar.
      } else {
        this.progressBar.end();
        this.progressBar.destroy();
      }

      return blobSignedId;
    } catch (error) {
      this.progressBar.error((error as Error).message);
      throw error;
    }
  }

  /**
    Upload the file using the DirectUpload instance, and return the blob signed_id.
    Throws a FileUploadError on failure.
    */
  private async upload(): Promise<string> {
    return new Promise((resolve, reject) => {
      this.directUpload.create((errorMsg, attributes) => {
        if (errorMsg) {
          const error = errorFromDirectUploadMessage(errorMsg);
          reject(error);
        } else {
          resolve(attributes.signed_id);
        }
      });
    });
  }

  /**
    Attach the file by sending a POST request to the autoAttachUrl.
    Throws a FileUploadError on failure (containing the first validation
    error message, if any).
    */
  private async attach(blobSignedId: string, autoAttachUrl: string) {
    const formData = new FormData();
    formData.append('blob_signed_id', blobSignedId);

    try {
      await httpRequest(autoAttachUrl, {
        method: 'post',
        body: formData,
        headers: { 'x-http-method-override': 'PUT' }
      }).turbo();
    } catch (e) {
      const error = e as ResponseError;
      const errors = (error.jsonBody as { errors: string[] })?.errors;
      const message = errors && errors[0];
      throw new FileUploadError(
        message ||
          `Impossible d'associer le fichier (in english: error attaching file).'`,
        error.response?.status,
        ERROR_CODE_ATTACH
      );
    }
  }

  uploadRequestDidProgress(event: ProgressEvent) {
    const progress = (event.loaded / event.total) * 100;
    if (progress) {
      this.progressBar.progress(progress);
    }
  }

  directUploadWillStoreFileWithXHR(xhr: XMLHttpRequest) {
    xhr.upload.addEventListener('progress', (event) =>
      this.uploadRequestDidProgress(event)
    );
  }
}