import { ApplicationRef, Injectable, OnDestroy } from '@angular/core';
import { FileUploadModel } from '../model/file-upload.model';
import { HttpErrorResponse, HttpEventType, HttpStatusCode } from '@angular/common/http';
import { interval, Subject, Subscription } from 'rxjs';
import { ConfigService } from '@services/config.service';
import { TranslateService } from '@ngx-translate/core';
import { IFileUploadResult, OverwriteFileOption, OverwriteFileOptions } from '@red/domain';
import { isAcceptedFileType, isCsv, isZip } from '@utils/file-drop-utils';
import { ErrorMessageService, GenericService, Toaster } from '@iqser/common-ui';
import { FilesMapService } from '@services/files/files-map.service';
import { switchMap, tap, throttleTime } from 'rxjs/operators';
import { FilesService } from '@services/files/files.service';
import { UploadDownloadDialogService } from './upload-download-dialog.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { HeadersConfiguration } from '@iqser/common-ui/lib/utils';

export interface ActiveUpload {
    subscription: Subscription;
    fileUploadModel: FileUploadModel;
}

@Injectable()
export class FileUploadService extends GenericService<IFileUploadResult> implements OnDestroy {
    static readonly MAX_PARALLEL_UPLOADS = 5;
    #pendingUploads: FileUploadModel[] = [];
    #activeUploads: ActiveUpload[] = [];
    readonly #fetchFiles$ = new Subject<string>();
    readonly #subscriptions = new Subscription();
    protected readonly _defaultModelPath = 'upload';
    files: FileUploadModel[] = [];
    groupedFiles: { [key: string]: FileUploadModel[] } = {};
    activeUploads = 0;

    constructor(
        private readonly _filesService: FilesService,
        private readonly _filesMapService: FilesMapService,
        private readonly _applicationRef: ApplicationRef,
        private readonly _translateService: TranslateService,
        private readonly _configService: ConfigService,
        private readonly _dialogService: UploadDownloadDialogService,
        private readonly _errorMessageService: ErrorMessageService,
        private readonly _toaster: Toaster,
    ) {
        super();
        const fileFetch$ = this.#fetchFiles$.pipe(
            throttleTime(250),
            switchMap(dossierId => this._filesService.loadAll(dossierId)),
        );
        this.#subscriptions.add(fileFetch$.subscribe());
        const interval$ = interval(2500).pipe(tap(() => this._handleUploads()));
        this.#subscriptions.add(interval$.subscribe());
    }

    get activeDossierKeys() {
        return Object.keys(this.groupedFiles).filter(dossierId => this.groupedFiles[dossierId].length > 0);
    }

    ngOnDestroy() {
        this.#subscriptions.unsubscribe();
    }

    scheduleUpload(item: FileUploadModel) {
        if (!item.sizeError && !item.typeError) {
            item.progress = 0;
            item.completed = false;
            item.error = null;
            this.#pendingUploads.push(item);
        }
    }

    async uploadFiles(files: FileUploadModel[], dossierId: string): Promise<number> {
        const maxSizeMB = this._configService.values.MAX_FILE_SIZE_MB;
        const maxSizeBytes = maxSizeMB * 1024 * 1024;
        const dossierFiles = this._filesMapService.get(dossierId);
        let option: OverwriteFileOption = localStorage.getItem('overwriteFileOption') as OverwriteFileOption;
        for (let idx = 0; idx < files.length; ++idx) {
            const file = files[idx];
            let currentOption = option;

            if (isZip(file)) {
                if (dossierFiles.length > 0) {
                    const res = await this._dialogService.openOverwriteFileDialog(null);
                    if (res.cancel) {
                        return;
                    }
                    if (res.option === OverwriteFileOptions.PARTIAL_OVERWRITE) {
                        file.keepManualRedactions = true;
                    }
                    if (res.option === OverwriteFileOptions.SKIP) {
                        files = [];
                    }
                }
            } else if (dossierFiles.find(pf => pf.filename === file.file.name)) {
                if (!option) {
                    const res = await this._dialogService.openOverwriteFileDialog(file.file.name);
                    if (res.cancel) {
                        return;
                    }

                    if (res.rememberChoice) {
                        localStorage.setItem('overwriteFileOption', res.option);
                    }

                    currentOption = res.option;
                    option = res.applyToAllFiles ? currentOption : undefined;
                }

                if (currentOption === OverwriteFileOptions.PARTIAL_OVERWRITE) {
                    file.keepManualRedactions = true;
                }

                if (currentOption === OverwriteFileOptions.SKIP) {
                    files.splice(idx, 1);
                    --idx;
                    continue;
                }
            }
            if (!isAcceptedFileType(file)) {
                file.completed = true;
                file.error = {
                    message: this._translateService.instant('upload-status.error.file-type'),
                };
                file.typeError = true;
            } else if (file.size > maxSizeBytes) {
                file.completed = true;
                file.error = {
                    message: this._translateService.instant('upload-status.error.file-size', {
                        size: maxSizeMB,
                    }),
                };
                file.sizeError = true;
            }
        }

        this.files.push(...files);
        files.forEach(newFile => {
            this._addFileToGroup(newFile);
            this.scheduleUpload(newFile);
        });
        return files.length;
    }

    filterFiles() {
        for (const file of this.files) {
            if (file.completed && !file.error) {
                this.removeFile(file);
            }
        }
    }

    stopAllUploads() {
        this.files = [];
        this.groupedFiles = {};
    }

    removeFile(item: FileUploadModel) {
        this._removeUpload(item);

        const index = this.files.indexOf(item);
        if (index > -1) {
            this._removeFileFromGroup(item);
            this.#fetchFiles$.next(item.dossierId);
            this.files.splice(index, 1);
        }
    }

    uploadFileForm(dossierId: string, keepManualRedactions = false, file?: Blob) {
        const formParams = new FormData();

        formParams.append('keepManualRedactions', keepManualRedactions.toString());

        if (file !== undefined) {
            formParams.append('file', file);
        }

        const headers = HeadersConfiguration.getHeaders({ contentType: false }).append('ngsw-bypass', 'true');

        return this._http.post<IFileUploadResult>(`/${this._serviceName}/${this._defaultModelPath}/${dossierId}`, formParams, {
            headers,
            observe: 'events',
            reportProgress: true,
        });
    }

    private _addFileToGroup(file: FileUploadModel) {
        if (!this.groupedFiles[file.dossierId]) {
            this.groupedFiles[file.dossierId] = [];
        }
        this.groupedFiles[file.dossierId].push(file);
    }

    private _removeFileFromGroup(file: FileUploadModel) {
        const index = this.groupedFiles[file.dossierId].indexOf(file);
        this.groupedFiles[file.dossierId].splice(index, 1);
    }

    private _handleUploads() {
        if (this.#activeUploads.length < FileUploadService.MAX_PARALLEL_UPLOADS && this.#pendingUploads.length > 0) {
            let cnt = FileUploadService.MAX_PARALLEL_UPLOADS - this.#activeUploads.length;
            while (cnt > 0 && this.#pendingUploads.length > 0) {
                // Only schedule CSVs when no other file types in queue.
                // CSVs are sorted at the end of `_pendingUploads`.
                if (isCsv(this.#pendingUploads[0]) && this.#activeUploads.filter(upload => !isCsv(upload.fileUploadModel)).length > 0) {
                    return;
                }

                cnt--;
                const poppedFileUploadModel = this.#pendingUploads.shift();
                const subscription = this._createSubscription(poppedFileUploadModel);
                this.#activeUploads.push({
                    subscription: subscription,
                    fileUploadModel: poppedFileUploadModel,
                });
            }
        }
    }

    private _createSubscription(uploadFile: FileUploadModel) {
        this.activeUploads++;
        const obs = this.uploadFileForm(uploadFile.dossierId, uploadFile.keepManualRedactions, uploadFile.file);
        return obs.subscribe({
            next: event => {
                if (event.type === HttpEventType.UploadProgress) {
                    uploadFile.progress = Math.round((event.loaded / (event.total || event.loaded)) * 100);
                    this._applicationRef.tick();
                }
                if (event.type === HttpEventType.Response) {
                    if (event.status < 300) {
                        uploadFile.progress = 100;
                        uploadFile.completed = true;
                        if (isCsv(uploadFile)) {
                            this._toaster.success(_('file-upload.type.csv'));
                        }
                    } else {
                        uploadFile.completed = true;
                        uploadFile.error = {
                            message: this._translateService.instant('upload-status.error.generic'),
                        };
                    }
                    this._removeUpload(uploadFile);
                }
            },
            error: (err: unknown) => {
                uploadFile.completed = true;
                uploadFile.error = {
                    // Extract error message
                    message: this._errorMessageService.getMessage(err as HttpErrorResponse, 'upload-status.error.generic'),
                };
                this._removeUpload(uploadFile);
                if (
                    uploadFile.retryCount < 5 &&
                    ![HttpStatusCode.BadRequest, HttpStatusCode.Conflict].includes((err as HttpErrorResponse).status)
                ) {
                    uploadFile.retryCount += 1;
                    this.scheduleUpload(uploadFile);
                }
            },
        });
    }

    private _removeUpload(fileUploadModel: FileUploadModel) {
        const index = this.#activeUploads.findIndex(au => au.fileUploadModel === fileUploadModel);
        if (index > -1) {
            const subscription = this.#activeUploads[index].subscription;
            if (subscription) {
                try {
                    subscription.unsubscribe();
                } catch (e) {
                    console.error(e);
                }
            }

            this.#activeUploads.splice(index, 1);
            this.activeUploads--;
        }
    }
}
