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

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

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

    constructor(
        private readonly _toaster: Toaster,
        private readonly _dossierTemplateStatsService: DossierTemplateStatsService,
        private readonly _dictionariesMapService: DictionariesMapService,
        private readonly _dossierDictionariesMapService: DossierDictionariesMapService,
    ) {
        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));
    }

    /**
     * Deletes entry types
     */

    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])),
        );
    }

    /**
     * Retrieve all entry types
     */

    getAllDictionaries(dossierTemplateId: string, dossierId?: string) {
        const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
        return this._getOne<{ types: IDictionary[] }>(['type', dossierTemplateId], this._defaultModelPath, queryParams);
    }

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

    updateDictionary(body: IUpdateDictionary, dossierTemplateId: string, type: string, dossierId?: string): Observable<unknown> {
        const url = `${this._defaultModelPath}/type/${type}/${dossierTemplateId}`;
        const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
        return this._post(body, url, queryParams).pipe(
            catchError((error: unknown) => this.#addUpdateDictionaryErrorToast(error)),
            switchMap(() => this.loadDictionaryDataForDossierTemplate(dossierTemplateId)),
            switchMap(() => this._dossierTemplateStatsService.getFor([dossierTemplateId])),
        );
    }

    /**
     * Creates entry type with colors, hint and caseInsensitive
     */

    addDictionary(dictionary: IDictionary, dossierId?: string): Observable<unknown> {
        const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
        return this._post(dictionary, `${this._defaultModelPath}/type`, queryParams).pipe(
            catchError((error: unknown) => this.#addUpdateDictionaryErrorToast(error)),
            switchMap(() => this.loadDictionaryDataForDossierTemplate(dictionary.dossierTemplateId)),
            switchMap(() => this._dossierTemplateStatsService.getFor([dictionary.dossierTemplateId])),
        );
    }

    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);
        let hasInvalidRows = false;
        for (let i = 0; i < entries.length; i++) {
            const entry = entries.at(i);
            if (!entry.trim() || initialEntriesSet.has(entry)) {
                continue;
            }
            hasInvalidRows ||= entry.length < MIN_WORD_LENGTH;
            entriesToAdd.push(entry);
        }
        if (hasInvalidRows) {
            this._toaster.error(_('dictionary-overview.error.entries-too-short'));

            throw new Error('Entries too short');
        }
        const deletedEntries: 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;
            }
            deletedEntries.push(entry);
        }

        try {
            if (deletedEntries.length) {
                await this._deleteEntries(deletedEntries, dossierTemplateId, type, dictionaryEntryType, dossierId);
            }
            if (entriesToAdd.length) {
                await this._addEntries(entriesToAdd, 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'));
            }
        }
    }

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

    getRedactTextDictionaries(dossierTemplateId: string, dossierDictionaryOnly: boolean): Dictionary[] {
        return this._dictionariesMapService
            .get(dossierTemplateId)
            .filter(d => d.model['typeId'] && !d.hint && d.addToDictionaryAction && (dossierDictionaryOnly || !d.dossierDictionaryOnly))
            .sort((a, b) => a.label.localeCompare(b.label));
    }

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

    getAddHintDictionaries(dossierTemplateId: string, dossierDictionaryOnly: boolean, dictionaryRequest: boolean): Dictionary[] {
        const dictionaries: Dictionary[] = [];

        this._dictionariesMapService.get(dossierTemplateId).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[] {
        const possibleDictionaries: Dictionary[] = [];

        for (const dictionary of this._dictionariesMapService.get(dossierTemplateId)) {
            if (!dictionary.virtual && !dictionary.hint && !dictionary.systemManaged) {
                possibleDictionaries.push(dictionary);
            }
        }

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

    async getDictionariesOptions(dossierTemplateId: string, dossierId: string): Promise<Dictionary[]> {
        const possibleDictionaries: Dictionary[] = [];

        const dossierDictionary: Dictionary = this._dossierDictionariesMapService.get(dossierId, 'dossier_redaction');

        for (const dictionary of this._dictionariesMapService.get(dossierTemplateId)) {
            if (!dictionary.virtual && dictionary.addToDictionaryAction) {
                possibleDictionaries.push(dictionary);
            }
        }

        if (dossierDictionary?.addToDictionaryAction) {
            possibleDictionaries.push(dossierDictionary);
        }

        possibleDictionaries.sort((a, b) => a.label.localeCompare(b.label));

        return possibleDictionaries;
    }

    async loadDossierDictionary(dossierTemplateId: string, dossierId: string): Promise<Dictionary> {
        const promise = this.getForType(dossierTemplateId, 'dossier_redaction', dossierId);
        const dict = await promise.catch(() => undefined);
        if (dict) {
            const dictionary = new Dictionary({
                ...dict,
                type: 'dossier_redaction',
            });
            this._dossierDictionariesMapService.set(dossierId, [dictionary]);
            return dictionary;
        }

        this._dossierDictionariesMapService.set(dossierId, []);
        return undefined;
    }

    loadDictionaryData(dossierTemplatesIds: string[]): Observable<Dictionary[][]> {
        const observables: Observable<Dictionary[]>[] = [];
        for (const dossierTemplateId of dossierTemplatesIds) {
            observables.push(this.loadDictionaryDataForDossierTemplate(dossierTemplateId));
        }
        return forkJoin(observables);
    }

    loadDictionaryDataForDossierTemplate(dossierTemplateId: string): Observable<Dictionary[]> {
        return this.getAllDictionaries(dossierTemplateId).pipe(
            map(typesResponse => typesResponse.types.map(type => new Dictionary(type))),
            map(dictionaries => {
                let manualTypeExists = false;
                for (const dictionary of dictionaries) {
                    if (dictionary.type === SuperTypes.ManualRedaction) {
                        manualTypeExists = true;
                        break;
                    }
                }
                if (!manualTypeExists) {
                    dictionaries.push(new Dictionary({ hexColor: FALLBACK_COLOR, type: SuperTypes.ManualRedaction }, true));
                }

                return dictionaries;
            }),
            tap(dictionaries => this._dictionariesMapService.set(dossierTemplateId, dictionaries)),
        );
    }

    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);
    }

    /**
     * Add dictionary entries with entry type.
     */

    private _addEntries(
        entries: List,
        dossierTemplateId: string,
        type: string,
        dictionaryEntryType: DictionaryEntryType,
        dossierId: string,
    ) {
        const queryParams: List<QueryParam> = [
            { key: 'dossierId', value: dossierId },
            { key: 'dictionaryEntryType', value: dictionaryEntryType },
        ];
        const url = `${this._defaultModelPath}/${type}/${dossierTemplateId}`;
        return firstValueFrom(this._post(entries, url, queryParams));
    }

    /**
     * Delete dictionary entries with entry type.
     */

    private _deleteEntries(
        entries: List,
        dossierTemplateId: string,
        type: string,
        dictionaryEntryType: DictionaryEntryType,
        dossierId: string,
    ) {
        const queryParams = [
            { key: 'dossierId', value: dossierId },
            { key: 'dictionaryEntryType', value: dictionaryEntryType },
        ];
        const url = `${this._defaultModelPath}/delete/${type}/${dossierTemplateId}`;
        return firstValueFrom(this._post(entries, url, queryParams));
    }

    #addUpdateDictionaryErrorToast(error: HttpErrorResponse | unknown): Observable<never> {
        if ((error as HttpErrorResponse).status === HttpStatusCode.Conflict) {
            this._toaster.error(_('add-edit-entity.error.entity-already-exists'));
        } else if ((error as HttpErrorResponse).status === HttpStatusCode.BadRequest) {
            this._toaster.error(_('add-edit-entity.error.invalid-color-or-rank'));
        } else {
            this._toaster.error(_('add-edit-entity.error.generic'));
        }
        return throwError(() => error);
    }
}
