import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { EntitiesService, isIqserDevMode, Toaster } from '@iqser/common-ui';
import { List } from '@iqser/common-ui/lib/utils';
import { Dictionary, DictionaryEntryType, DictionaryEntryTypes, IDictionary, IUpdateDictionary, SuperTypes } from '@red/domain';
import { firstValueFrom, forkJoin, Observable } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
import { IMAGE_CATEGORIES } from '../../modules/file-preview/utils/constants';
import { DictionariesMapService } from './dictionaries-map.service';
import { DossierTemplateStatsService } from './dossier-template-stats.service';

const IMAGE_TYPES = ['image', 'formula', 'ocr'];

@Injectable({
    providedIn: 'root',
})
export class DictionaryService extends EntitiesService<IDictionary, Dictionary> {
    protected readonly _defaultModelPath = 'dictionary';
    protected readonly _entityClass = Dictionary;
    readonly #isIqserDevMode = isIqserDevMode();

    constructor(
        private readonly _toaster: Toaster,
        private readonly _dossierTemplateStatsService: DossierTemplateStatsService,
        private readonly _dictionariesMapService: DictionariesMapService,
    ) {
        super();
    }

    /**
     * Retrieves all dictionary entries of an entry type
     */

    getForType(dossierTemplateId: string, type: string, dossierId?: string) {
        const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
        return firstValueFrom(this._getOne([type, dossierTemplateId], this._defaultModelPath, queryParams));
    }

    deleteDictionaries(dictionaryIds: List, dossierTemplateId: string, dossierId?: string): Observable<unknown> {
        const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
        const url = `${this._defaultModelPath}/type/${dossierTemplateId}/delete`;
        return this._post(dictionaryIds, url, queryParams).pipe(
            switchMap(() => this.loadDictionaryDataForDossierTemplate(dossierTemplateId)),
            switchMap(() => this._dossierTemplateStatsService.getFor([dossierTemplateId])),
        );
    }

    getAllDictionaries(dossierTemplateId: string, readOnly = false, dossierId?: string) {
        const queryParams = [];
        if (dossierId) {
            queryParams.push({ key: 'dossierId', value: dossierId });
        }
        if (readOnly) {
            queryParams.push({ key: 'includeDeleted', value: true });
        }
        return this._getOne<{ types: IDictionary[] }>(['type', dossierTemplateId], this._defaultModelPath, queryParams).pipe(
            map(typesResponse => typesResponse.types.map(type => new Dictionary(type))),
        );
    }

    /**
     * Updates colors, hint and caseInsensitive of an entry type.
     */

    async update(body: IUpdateDictionary, dossierTemplateId: string, type: string, dossierId?: string): Promise<unknown> {
        const url = `${this._defaultModelPath}/type/${type}/${dossierTemplateId}`;
        const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
        const request = this._post(body, url, queryParams).pipe(
            switchMap(() => this.loadDictionaryDataForDossierTemplate(dossierTemplateId)),
            switchMap(() => this._dossierTemplateStatsService.getFor([dossierTemplateId])),
        );

        return await firstValueFrom(request);
    }

    async updateFlag(dossierTemplateId: string, type: string, dossierId: string, addToDictionary: boolean): Promise<unknown> {
        const url = `${this._defaultModelPath}/updateFlag/${type}/${dossierTemplateId}/${dossierId}`;
        const queryParams = [{ key: 'addToDictionary', value: addToDictionary }];
        const request = this._post(null, url, queryParams).pipe(
            switchMap(() => this.loadDictionaryDataForDossier(dossierTemplateId, dossierId)),
        );

        return await firstValueFrom(request);
    }

    async add(dictionary: IDictionary, dossierId?: string): Promise<unknown> {
        const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
        const request = this._post(dictionary, `${this._defaultModelPath}/type`, queryParams).pipe(
            switchMap(() => this.loadDictionaryDataForDossierTemplate(dictionary.dossierTemplateId)),
            switchMap(() => this._dossierTemplateStatsService.getFor([dictionary.dossierTemplateId])),
        );

        return await firstValueFrom(request);
    }

    async saveEntries(
        entries: List,
        initialEntries: List,
        dossierTemplateId: string,
        type: string,
        dossierId: string,
        showToast = true,
        dictionaryEntryType = DictionaryEntryTypes.ENTRY,
    ) {
        const entriesToAdd: Array<string> = [];
        const initialEntriesSet = new Set(initialEntries);
        for (let i = 0; i < entries.length; i++) {
            const entry = entries.at(i);
            if (!entry.trim() || initialEntriesSet.has(entry)) {
                continue;
            }
            entriesToAdd.push(entry);
        }
        const entriesToDelete: Array<string> = [];
        const entriesSet = new Set(entries);
        for (let i = 0; i < initialEntries.length; i++) {
            const entry = initialEntries.at(i);
            if (entriesSet.has(entry)) {
                continue;
            }
            entriesToDelete.push(entry);
        }

        try {
            await this.#updateEntries(entriesToAdd, entriesToDelete, dossierTemplateId, type, dictionaryEntryType, dossierId);

            if (showToast) {
                this._toaster.success(_('dictionary-overview.success.generic'));
            }
        } catch (error) {
            if ((error as HttpErrorResponse).status === 400) {
                this._toaster.error(_('dictionary-overview.error.400'));
            } else {
                this._toaster.error(_('dictionary-overview.error.generic'));
            }
            throw error;
        }
    }

    hasManualType(dossierTemplateId: string): boolean {
        return !!this._dictionariesMapService.get(dossierTemplateId).find(e => e.type === SuperTypes.ManualRedaction && !e.virtual);
    }

    hasType(dossierTemplateId: string, type: string): boolean {
        return !!this._dictionariesMapService.get(dossierTemplateId).find(e => e.type === type && !e.virtual);
    }

    getDictionaries(dossierTemplateId: string) {
        return this._dictionariesMapService
            .get(dossierTemplateId)
            .filter(d => d.model['typeId'] && (d.hasDictionary || d.addToDictionaryAction));
    }

    getRedactTextDictionaries(dossierId: string, dossierDictionaryOnly: boolean, dossierTemplateId: string): Dictionary[] {
        const types = dossierDictionaryOnly ? this.#extractDossierLevelTypes(dossierId) : this.getDictionariesOptions(dossierTemplateId);
        return types
            .filter(d => d.model['typeId'] && !d.hint && d.addToDictionaryAction && (dossierDictionaryOnly || !d.dossierDictionaryOnly))
            .sort((a, b) => a.label.localeCompare(b.label));
    }

    getEditableRedactionTypes(
        dossierId: string,
        isImage: boolean,
        isHint: boolean,
        isOCR: boolean,
        currentlySelectedType: string,
    ): Dictionary[] {
        return this.#extractDossierLevelTypes(dossierId)
            .filter(
                d =>
                    d.model['typeId'] &&
                    (isImage
                        ? (isOCR ? [...IMAGE_CATEGORIES, 'ocr'] : IMAGE_CATEGORIES).includes(d.type)
                        : (isHint ? d.hint : !d.hint) &&
                          (d.addToDictionaryAction || currentlySelectedType === d.type) &&
                          !d.virtual &&
                          !d.systemManaged &&
                          ![...IMAGE_CATEGORIES, 'ocr'].includes(d.type)),
            )
            .sort((a, b) => a.label.localeCompare(b.label));
    }

    getAddHintDictionaries(dossierId: string, dossierDictionaryOnly: boolean, dictionaryRequest: boolean): Dictionary[] {
        const dictionaries: Dictionary[] = [];
        this.#extractDossierLevelTypes(dossierId).forEach((d: Dictionary) => {
            if (d.hint) {
                if (dictionaryRequest) {
                    if (d.hasDictionary && d.addToDictionaryAction) {
                        if (d.dossierDictionaryOnly) {
                            if (dossierDictionaryOnly) {
                                dictionaries.push(d);
                            }
                        } else {
                            dictionaries.push(d);
                        }
                    }
                } else if (!IMAGE_TYPES.includes(d.type)) {
                    dictionaries.push(d);
                }
            }
        });

        return dictionaries.sort((a, b) => a.label.localeCompare(b.label));
    }

    getRedactionTypes(dossierTemplateId: string): Dictionary[] {
        return this._dictionariesMapService
            .get(dossierTemplateId)
            .filter(dictionary => !dictionary.virtual && !dictionary.hint && !dictionary.systemManaged)
            .sort((a, b) => a.label.localeCompare(b.label));
    }

    getDictionariesOptions(dossierTemplateId: string): Dictionary[] {
        return this._dictionariesMapService
            .get(dossierTemplateId)
            .filter(dictionary => !dictionary.virtual && dictionary.addToDictionaryAction)
            .sort((a, b) => a.label.localeCompare(b.label));
    }

    loadDictionaryDataForDossierTemplate(dossierTemplateId: string): Observable<Dictionary[]> {
        return this.getAllDictionaries(dossierTemplateId).pipe(
            tap(dictionaries => this._dictionariesMapService.set(dossierTemplateId, dictionaries)),
        );
    }

    loadDictionaryDataForDossier(dossierTemplateId: string, dossierId: string): Observable<Dictionary[]> {
        return this.getAllDictionaries(dossierTemplateId, false, dossierId).pipe(
            tap(dictionaries => this._dictionariesMapService.set(dossierId, dictionaries)),
        );
    }

    loadTemporaryDictionaryData(dossierTemplateId: string, readOnlyFile = true): Observable<Dictionary[]> {
        return this.getAllDictionaries(dossierTemplateId, readOnlyFile);
    }

    loadDictionaryEntriesByType(types: string[], dossierTemplateId: string, dossierId: string): Observable<Dictionary[]> {
        const queryParams = [{ key: 'dossierId', value: dossierId }];
        const requests = [];
        for (const type of types) {
            const request = this.getAll(`${this._defaultModelPath}/merged/${type}/${dossierTemplateId}`, queryParams);
            requests.push(request);
        }

        return forkJoin(requests);
    }

    #updateEntries(
        entriesToAdd: List,
        entriesToDelete: List,
        dossierTemplateId: string,
        type: string,
        dictionaryEntryType: DictionaryEntryType,
        dossierId: string,
    ) {
        const queryParams = [
            { key: 'dossierId', value: dossierId },
            { key: 'dictionaryEntryType', value: dictionaryEntryType },
        ];
        const url = `${this._defaultModelPath}/update/${type}/${dossierTemplateId}`;
        return firstValueFrom(this._post({ entriesToAdd, entriesToDelete }, url, queryParams));
    }

    #extractDossierLevelTypes(dossierId: string) {
        return this._dictionariesMapService
            .get(dossierId)
            .filter(dictionary => dictionary.model['typeId']?.includes(dossierId))
            .filter(dictionary => !(dictionary.experimental && !this.#isIqserDevMode));
    }
}
