/*
 * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
 * Ministerpräsidenten des Landes Schleswig-Holstein
 * Staatskanzlei
 * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
 *
 * Lizenziert unter der EUPL, Version 1.2 oder - sobald
 * diese von der Europäischen Kommission genehmigt wurden -
 * Folgeversionen der EUPL ("Lizenz");
 * Sie dürfen dieses Werk ausschließlich gemäß
 * dieser Lizenz nutzen.
 * Eine Kopie der Lizenz finden Sie hier:
 *
 * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
 *
 * Sofern nicht durch anwendbare Rechtsvorschriften
 * gefordert oder in schriftlicher Form vereinbart, wird
 * die unter der Lizenz verbreitete Software "so wie sie
 * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
 * ausdrücklich oder stillschweigend - verbreitet.
 * Die sprachspezifischen Genehmigungen und Beschränkungen
 * unter der Lizenz sind dem Lizenztext zu entnehmen.
 */
import { BlobWithFileName, createEmptyStateResource, createErrorStateResource, createStateResource, EMPTY_ARRAY, EMPTY_STRING, getMessageForInvalidParam, HttpError, HttpHeader, isNotNil, isUnprocessableEntity, isValidationFieldFileSizeExceedError, sanitizeFileName, StateResource, } from '@alfa-client/tech-shared';
import { SnackBarService } from '@alfa-client/ui';
import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { faker } from '@faker-js/faker';
import { getUrl, Resource, ResourceUri } from '@ngxp/rest';
import { saveAs } from 'file-saver';
import { isNil } from 'lodash-es';
import { BehaviorSubject, forkJoin, Observable, of, throwError } from 'rxjs';
import { catchError, map, mergeMap, startWith, switchMap } from 'rxjs/operators';
import { BinaryFileListResource, BinaryFileResource, FileUploadType, ToUploadFile, UploadFile, UploadFilesByType, } from './binary-file.model';
import { BinaryFileRepository } from './binary-file.repository';

@Injectable({ providedIn: 'root' })
export class BinaryFileService {
  uploadFiles: BehaviorSubject<UploadFilesByType> = new BehaviorSubject({});

  constructor(
    private repository: BinaryFileRepository,
    private snackbarService: SnackBarService,
  ) {}

  public isUploadInProgress(type: FileUploadType): Observable<boolean> {
    return this.uploadFiles.asObservable().pipe(
      map((files: UploadFilesByType) => files[type].map((file: UploadFile) => file.uploadedFile)),
      switchMap((files: Observable<StateResource<BinaryFileResource>>[]) => forkJoin(files)),
      map((files: StateResource<BinaryFileResource>[]) => files.some((s) => s.loading)),
    );
  }

  public getUploadedFiles(type: FileUploadType): Observable<UploadFile[]> {
    return this.uploadFiles.asObservable().pipe(map((files: UploadFilesByType) => files[type]));
  }

  public deleteUploadedFile(type: FileUploadType, key: string): void {
    //Implement me
  }

  public clearUploadedFiles(type: FileUploadType): void {
    //Implement me
  }

  //TODO Rename
  public uploadFileNew(toUploadFile: ToUploadFile): void {
    if (!(toUploadFile.uploadUrl in this.uploadFiles.value)) this.uploadFiles.value[toUploadFile.type] = EMPTY_ARRAY;
    this.uploadFiles.value[toUploadFile.type].push(this._buildUploadFile(toUploadFile));
  }

  _buildUploadFile(toUploadFile: ToUploadFile): UploadFile {
    return {
      key: faker.string.uuid(),
      fileToUpload: toUploadFile.file,
      uploadedFile: this._handleUpload(toUploadFile),
    };
  }

  _handleUpload(toUploadFile: ToUploadFile): Observable<StateResource<BinaryFileResource>> {
    return this.repository.uploadFileNew(toUploadFile.uploadUrl, toUploadFile.file).pipe(
      map((binaryFileResource: BinaryFileResource) => createStateResource(binaryFileResource)),
      catchError((response: any) => of(createErrorStateResource(<HttpError>response.error))),
      startWith(createEmptyStateResource<BinaryFileResource>(true)),
    );
  }

  //TODO Rename to uploadFileOld OR refactor all use cases to uploadFileNew
  public uploadFile(
    resource: Resource,
    linkRel: string,
    file: File,
    showValidationErrorSnackBar: boolean = true,
  ): Observable<StateResource<BinaryFileResource>> {
    return this.repository.uploadFile(getUrl(resource, linkRel), file).pipe(
      mergeMap((response: HttpResponse<Object>) => this.getFile(response.headers.get(HttpHeader.LOCATION))),
      catchError((errorResponse) => this.handleError(errorResponse.error, showValidationErrorSnackBar)),
      startWith(createEmptyStateResource<BinaryFileResource>(true)),
    );
  }

  private handleError(errorResponse: HttpErrorResponse, showValidationErrorSnackBar: boolean): Observable<StateResource<any>> {
    return of(this.handleErrorByStatus(errorResponse, showValidationErrorSnackBar));
  }

  handleErrorByStatus(error: HttpErrorResponse, showValidationErrorSnackBar: boolean): StateResource<any> {
    if (isUnprocessableEntity(error.status)) {
      this.handleSnackBar(error, showValidationErrorSnackBar);
      return createErrorStateResource(error.error);
    }
    throwError({ error });
  }

  handleSnackBar(error: HttpErrorResponse, showValidationErrorSnackBar: boolean) {
    if (showValidationErrorSnackBar && isValidationFieldFileSizeExceedError(error.error)) {
      this.snackbarService.showError(getMessageForInvalidParam(EMPTY_STRING, error.error.invalidParams[0]));
    }
  }

  public downloadFile(file: BinaryFileResource, fileNamePrefix: string): Observable<StateResource<Blob>> {
    return this.repository.download(file).pipe(
      map((data) => this.saveBinaryFile(data, file, fileNamePrefix)),
      startWith(createEmptyStateResource<Blob>(true)),
      catchError(() => this.handleDownloadError()),
    );
  }

  handleDownloadError(): Observable<StateResource<Blob>> {
    this.snackbarService.showError('Die Datei konnte nicht heruntergeladen werden.');
    return of(createEmptyStateResource<Blob>());
  }

  saveBinaryFile(data: any, file: BinaryFileResource, fileNamePrefix: string): StateResource<Blob> {
    if (isNil(data)) {
      return createEmptyStateResource(true);
    }
    this.save(data, this.buildFileName(file, fileNamePrefix));
    return createStateResource(data);
  }

  private buildFileName(file: BinaryFileResource, fileNamePrefix: string): string {
    if (isNotNil(fileNamePrefix)) {
      return sanitizeFileName(`${fileNamePrefix}_${file.name}`);
    }
    return file.name;
  }

  public downloadArchive(uri: ResourceUri): Observable<StateResource<Blob>> {
    return this.repository.downloadArchive(uri).pipe(
      map((data: BlobWithFileName) => this.saveData(data)),
      startWith(createEmptyStateResource<Blob>(true)),
    );
  }

  saveData(dataWithFileName: BlobWithFileName): StateResource<Blob> {
    const data: Blob = dataWithFileName.blob;
    if (isNil(data)) {
      return createEmptyStateResource(true);
    }
    this.save(data, dataWithFileName.fileName);
    return createStateResource(data);
  }

  save(data: any, fileName: string): void {
    saveAs(data, fileName);
  }

  public getFile(uri: ResourceUri): Observable<StateResource<BinaryFileResource>> {
    return this.repository.getFile(uri).pipe(
      map((fileList: BinaryFileResource) => createStateResource(fileList)),
      startWith(createEmptyStateResource<BinaryFileResource>(true)),
    );
  }

  public getFiles(resource: Resource, linkRel: string): Observable<StateResource<BinaryFileListResource>> {
    return this.repository.getFiles(resource, linkRel).pipe(
      map((fileList: BinaryFileListResource) => createStateResource(fileList)),
      startWith(createEmptyStateResource<BinaryFileListResource>(true)),
    );
  }
}