import { HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse } from '@angular/common/http';
import { computed, effect, inject, Injectable, Signal } from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { LoadingService, wipeCache } from '@iqser/common-ui';
import { getParam } from '@iqser/common-ui/lib/utils';
import { TranslateService } from '@ngx-translate/core';
import { Dictionary, Dossier, DOSSIER_ID, DOSSIER_TEMPLATE_ID, File, FILE_ID } from '@red/domain';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { DossierDictionariesMapService } from '@services/entity-services/dossier-dictionaries-map.service';
import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider';
import { FileManagementService } from '@services/files/file-management.service';
import { FilesMapService } from '@services/files/files-map.service';
import { FilesService } from '@services/files/files.service';
import { PermissionsService } from '@services/permissions.service';
import { NGXLogger } from 'ngx-logger';
import { firstValueFrom, from, merge, Observable, of, pairwise, Subject, switchMap } from 'rxjs';
import { filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators';
import { ViewModeService } from './view-mode.service';

const ONE_MEGABYTE = 1024 * 1024;

function getRemainingTime(event: HttpProgressEvent, startTime: number) {
    const currTime = new Date().getTime();
    const remaining = event.total - event.loaded;
    const speed = event.loaded / ((currTime - startTime) / 1000);
    return Math.round(remaining / speed);
}

function isDownload(event: HttpEvent<Blob>): event is HttpProgressEvent {
    return event.type === HttpEventType.DownloadProgress && event.total > ONE_MEGABYTE;
}

@Injectable()
export class FilePreviewStateService {
    readonly #reloadBlob$ = new Subject();
    readonly file$: Observable<File>;
    readonly file: Signal<File>;
    readonly dossier: Signal<Dossier>;
    readonly isReadonly: Signal<boolean>;
    readonly isWritable: Signal<boolean>;
    readonly dossierDictionary: Signal<Dictionary>;
    readonly blob$: Observable<Blob>;
    readonly dossierId = getParam(DOSSIER_ID);
    readonly dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
    readonly fileId = getParam(FILE_ID);
    readonly shouldUpdate = computed(() => this.file().excludedPages);

    constructor(
        private readonly _permissionsService: PermissionsService,
        private readonly _filesService: FilesService,
        private readonly _dossiersService: DossiersService,
        private readonly _fileManagementService: FileManagementService,
        private readonly _dictionariesMapService: DictionariesMapService,
        private readonly _translateService: TranslateService,
        private readonly _loadingService: LoadingService,
        private readonly _viewModeService: ViewModeService,
        private readonly _logger: NGXLogger,
    ) {
        this.dossier = toSignal(dossiersServiceResolver().getEntityChanged$(this.dossierId));
        this.file$ = inject(FilesMapService).watch$(this.dossierId, this.fileId);
        this.file = toSignal(this.file$);
        this.isWritable = computed(() => {
            const isWritable = this._permissionsService.canPerformAnnotationActions(this.file(), this.dossier());
            this._logger.info('[FILE] Is writeable:', isWritable);
            return isWritable;
        });
        this.isReadonly = computed(() => !this.isWritable());

        this.blob$ = this.#blob$;

        this.dossierDictionary = toSignal(inject(DossierDictionariesMapService).watch$(this.dossierId, 'dossier_redaction'));
        this.#dossierFilesChange$
            .pipe(
                switchMap(() => this._filesService.loadAll(this.dossierId)),
                takeUntilDestroyed(),
            )
            .subscribe();
        effect(
            () => {
                if (this._viewModeService.isEarmarks() && !this.file().hasHighlights) {
                    this._viewModeService.switchToStandard();
                }
            },
            { allowSignalWrites: true },
        );
    }

    get dictionaries(): Dictionary[] {
        const dictionaries = this._dictionariesMapService.get(this.dossierTemplateId);
        if (this.dossierDictionary()) {
            dictionaries.push(this.dossierDictionary());
        }

        return dictionaries;
    }

    get blob(): Promise<Blob> {
        return firstValueFrom(this.blob$);
    }

    get #blob$() {
        const file$ = this.file$.pipe(
            startWith(undefined),
            pairwise(),
            filter(([oldFile, newFile]) => oldFile?.cacheIdentifier !== newFile.cacheIdentifier),
        );
        const reloadBlob$ = this.#reloadBlob$.pipe(
            withLatestFrom(file$),
            map(([, file]) => file),
        );
        return merge(file$, reloadBlob$).pipe(
            switchMap(([oldFile, newFile]) => this.#downloadOriginalFile(newFile.cacheIdentifier, !!oldFile?.cacheIdentifier)),
        );
    }

    get #dossierFilesChange$() {
        return this._dossiersService.dossierFileChanges$.pipe(
            filter(dossierId => dossierId === this.dossierId),
            map(() => true),
        );
    }

    reloadBlob(): void {
        this.#reloadBlob$.next(true);
    }

    #getRemainingTimeVerbose(event: HttpProgressEvent, startTime: number) {
        const remainingTime = getRemainingTime(event, startTime);
        if (remainingTime > 60) {
            const minutes: string = this._translateService.instant('minutes');
            return `${Math.round(remainingTime / 60)} ${minutes}`;
        }
        const seconds: string = this._translateService.instant('seconds');
        return `${remainingTime} ${seconds}`;
    }

    #downloadOriginalFile(cacheIdentifier: string, wipeCaches = true): Observable<Blob> {
        const downloadFile$ = this.#getFileToDownload(cacheIdentifier);
        const obs = wipeCaches ? from(wipeCache('files')) : of({});
        return obs.pipe(switchMap(() => downloadFile$));
    }

    #getFileToDownload(cacheIdentifier: string): Observable<Blob> {
        const downloadFile$ = this._fileManagementService.downloadOriginal(this.dossierId, this.fileId, 'events', cacheIdentifier);
        let start;
        return downloadFile$.pipe(
            tap(() => (start ? undefined : (start = new Date().getTime()))),
            tap(event => this.#showLoadingIfIsDownloadEvent(event, start)),
            filter(event => event.type === HttpEventType.Response),
            map((event: HttpResponse<Blob>) => event.body),
        );
    }

    #showLoadingIfIsDownloadEvent(event: HttpEvent<Blob>, start) {
        if (isDownload(event)) {
            this.#updateDownloadProgress(event, start);
        }
    }

    #updateDownloadProgress(event: HttpProgressEvent, startTime: number) {
        const progress = Math.round((event.loaded / event.total) * 100);
        const loading: string = this._translateService.instant('loading');
        this._loadingService.update({
            title: loading + ' ' + this.file().filename,
            type: 'progress-bar',
            value: progress,
            remainingTime: this.#getRemainingTimeVerbose(event, startTime),
        });
    }
}
