import { effect, Injectable, Signal, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { EntitiesService, getConfig, Toaster } from '@iqser/common-ui';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import {
    ChangeType,
    ChangeTypes,
    File,
    IRedactionLog,
    IRedactionLogEntry,
    LogEntryStatuses,
    ViewedPage,
    ViewMode,
    ViewModes,
} from '@red/domain';
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { EarmarksService } from '@services/files/earmarks.service';
import { FilesService } from '@services/files/files.service';
import { RedactionLogService } from '@services/files/redaction-log.service';
import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service';
import { ViewedPagesService } from '@services/files/viewed-pages.service';
import { PermissionsService } from '@services/permissions.service';
import { UserPreferenceService } from '@users/user-preference.service';
import dayjs from 'dayjs';
import { NGXLogger } from 'ngx-logger';
import { firstValueFrom, Observable } from 'rxjs';
import { FilePreviewStateService } from './file-preview-state.service';
import { MultiSelectService } from './multi-select.service';
import { ViewModeService } from './view-mode.service';

const DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;

export function timestampOf(value: string) {
    return dayjs(value).valueOf();
}

export function chronologicallyBy<T>(property: (x: T) => string) {
    return (a: T, b: T) => timestampOf(property(a)) - timestampOf(property(b));
}

@Injectable()
export class FileDataService extends EntitiesService<AnnotationWrapper, AnnotationWrapper> {
    readonly #annotations = signal<AnnotationWrapper[]>([]);
    readonly #earmarks = signal<Map<number, AnnotationWrapper[]>>(new Map());
    #originalViewedPages: ViewedPage[] = [];
    protected readonly _entityClass = AnnotationWrapper;
    missingTypes = new Set<string>();
    readonly earmarks: Signal<Map<number, AnnotationWrapper[]>>;
    readonly annotations: Signal<AnnotationWrapper[]>;
    readonly annotations$: Observable<AnnotationWrapper[]>;
    readonly #isDocumine = getConfig().IS_DOCUMINE;

    constructor(
        private readonly _state: FilePreviewStateService,
        private readonly _viewedPagesService: ViewedPagesService,
        private readonly _viewedPagesMapService: ViewedPagesMapService,
        private readonly _viewModeService: ViewModeService,
        private readonly _userPreferenceService: UserPreferenceService,
        private readonly _dictionaryService: DictionaryService,
        private readonly _permissionsService: PermissionsService,
        private readonly _redactionLogService: RedactionLogService,
        private readonly _earmarksService: EarmarksService,
        private readonly _multiSelectService: MultiSelectService,
        private readonly _filesService: FilesService,
        private readonly _toaster: Toaster,
        private readonly _logger: NGXLogger,
        private readonly _defaultColorsService: DefaultColorsService,
    ) {
        super();
        this.annotations$ = toObservable(this.#annotations);
        this.annotations = this.#annotations.asReadonly();
        this.earmarks = this.#earmarks.asReadonly();
        effect(() => {
            const viewMode = this._viewModeService.viewMode();
            const earmarks = ([] as AnnotationWrapper[]).concat(...this.#earmarks().values());
            const annotations = this.#getVisibleAnnotations(this.#annotations(), viewMode);
            this.setEntities(viewMode === ViewModes.TEXT_HIGHLIGHTS ? earmarks : annotations);
        });
    }

    setEntities(entities: AnnotationWrapper[]): void {
        // this is a light version of setEntities to skip looping too much
        // used mostly for earmarks (which are usually a lot)
        const all = new Map(this.all.map(annotation => [annotation.id, annotation]));
        const newEntities = [];

        for (const entity of entities) {
            const oldEntity = all.get(entity.id);

            if (oldEntity && JSON.stringify(oldEntity) === JSON.stringify(entity)) {
                newEntities.push(oldEntity);
            } else {
                newEntities.push(entity);
            }
        }

        this._all$.next(newEntities);
    }

    async loadAnnotations(file: File) {
        if (!file || file.isUnprocessed) {
            this._logger.info('[ANNOTATIONS] File is null or unprocessed, skipping annotations loading');
            return;
        }

        this._logger.info('[ANNOTATIONS] Loading annotations...');

        await this.#loadViewedPages(file);
        await this.loadRedactionLog();

        this._logger.info('[ANNOTATIONS] Annotations loaded');
    }

    async annotationsChanged() {
        this._multiSelectService.deactivate();
        const file = this._state.file();
        const fileReloaded = await this._filesService.reload(file.dossierId, file);
        if (!fileReloaded) {
            await this.loadAnnotations(file);
        }
    }

    async loadEarmarks(): Promise<Map<number, AnnotationWrapper[]>> {
        const rawHighlights = await this._earmarksService.getEarmarks(this._state.dossierId, this._state.fileId);
        const earmarks = rawHighlights.groupBy(h => h.pageNumber);
        this.#earmarks.set(earmarks);

        return earmarks;
    }

    async loadRedactionLog() {
        this._logger.info('[REDACTION_LOG] Loading redaction log...');
        const redactionLog = await this._redactionLogService.getRedactionLog(this._state.dossierId, this._state.fileId);

        this._logger.info('[REDACTION_LOG] Redaction log loaded', redactionLog);
        let annotations = await this.#convertData(redactionLog);
        this.#checkMissingTypes();
        annotations = this._userPreferenceService.isIqserDevMode ? annotations : annotations.filter(a => !a.isFalsePositive);
        this.#annotations.set(annotations);
    }

    #checkMissingTypes() {
        if (this.missingTypes.size > 0) {
            this._toaster.error(_('error.missing-types'), {
                disableTimeOut: true,
                params: { missingTypes: Array.from(this.missingTypes).join(', ') },
            });
            this.missingTypes.clear();
        }
    }

    async #loadViewedPages(file: File) {
        if (!this._permissionsService.canMarkPagesAsViewed(file)) {
            this._viewedPagesMapService.set(file.fileId, []);
            this._logger.info('[VIEWED_PAGES] Cannot mark pages as viewed, skip loading viewed pages');
            return;
        }

        this.#originalViewedPages = await this._viewedPagesService.load(file.dossierId, file.fileId);
        this._logger.info('[VIEWED_PAGES] Loaded viewed pages', this.#originalViewedPages);
        this._viewedPagesMapService.set(file.fileId, this.#originalViewedPages);
    }

    #getVisibleAnnotations(annotations: AnnotationWrapper[], viewMode: ViewMode) {
        return annotations.filter(annotation => {
            if (viewMode === 'STANDARD') {
                return !annotation.isChangeLogRemoved;
            }

            if (viewMode === 'DELTA') {
                return annotation.isChangeLogEntry;
            }

            return annotation.previewAnnotation;
        });
    }

    async #convertData(redactionLog: IRedactionLog) {
        if (!redactionLog.redactionLogEntry) {
            return [];
        }

        const file = this._state.file();
        const annotations: AnnotationWrapper[] = [];
        const sourceIds: { [key: string]: AnnotationWrapper[] } = {};
        const dictionaries = this._state.dictionaries;
        const defaultColors = this._defaultColorsService.find(this._state.dossierTemplateId);
        let checkDictionary = true;

        for (const entry of redactionLog.redactionLogEntry) {
            let dictionary = dictionaries.find(dict => dict.type === entry.type);
            if (!dictionary && checkDictionary) {
                const dictionaryRequest = this._dictionaryService.loadDictionaryDataForDossierTemplate(this._state.dossierTemplateId);
                await firstValueFrom(dictionaryRequest);
                checkDictionary = false;
                dictionary = dictionaries.find(dict => dict.type === entry.type);
            }

            if (!dictionary) {
                this.missingTypes.add(entry.type);
                continue;
            }

            const pageNumber = entry.positions[0]?.page;
            const manual = entry.manualChanges?.length > 0;
            if (!manual && file.excludedPages.includes(pageNumber)) {
                continue;
            }

            const changeLogValues = this.#getChangeLogValues(entry, file);
            if (changeLogValues.hidden) {
                continue;
            }

            const annotation = AnnotationWrapper.fromData(
                entry,
                dictionaries,
                defaultColors,
                changeLogValues.changeLogType,
                redactionLog.legalBasis ?? [],
                !!dictionary?.hint,
                this.#isDocumine,
            );

            if (entry.sourceId) {
                if (!sourceIds[entry.sourceId]) {
                    sourceIds[entry.sourceId] = [];
                }
                sourceIds[entry.sourceId].push(annotation);
            }

            annotations.push(annotation);
        }

        const sourceKeys = Object.keys(sourceIds);
        return annotations.filter(r => !sourceKeys.includes(r.id));
    }

    #getChangeLogValues(
        redactionLogEntry: IRedactionLogEntry,
        file: File,
    ): {
        hidden: boolean;
        changeLogType?: ChangeType;
    } {
        const hasManualChanges = redactionLogEntry.manualChanges?.length > 0;
        if (file.numberOfAnalyses <= 1 && !file.hasUpdates && !hasManualChanges) {
            return {
                changeLogType: undefined,
                hidden: false,
            };
        }

        const viableChanges = redactionLogEntry.changes?.filter(c => c.analysisNumber > 1);
        const lastChange = viableChanges?.sort(chronologicallyBy(x => x.dateTime)).at(-1);

        const page = redactionLogEntry.positions?.[0].page;
        const viewedPage = this.#originalViewedPages.find(p => p.page === page);

        if (!viewedPage) {
            return {
                changeLogType: undefined,
                hidden: lastChange ? lastChange.type === ChangeTypes.REMOVED : false,
            };
        }

        // page has been seen -> let's see if it's a change
        const viewTime = timestampOf(viewedPage.viewedTime) - DELTA_VIEW_TIME;
        let changeOccurredAfterPageIsViewed = lastChange && timestampOf(lastChange.dateTime) > viewTime;

        if (changeOccurredAfterPageIsViewed) {
            this.#markPageAsUnseenIfNeeded(viewedPage, lastChange.dateTime);
            return {
                changeLogType: lastChange?.type,
                hidden: false,
            };
        }

        const visibleStatuses: string[] = [LogEntryStatuses.APPROVED, LogEntryStatuses.REQUESTED];
        const viableManualChanges = redactionLogEntry.manualChanges.filter(change => visibleStatuses.includes(change.annotationStatus));
        viableManualChanges.sort(chronologicallyBy(x => x.processedDate));
        const lastManualChange = viableManualChanges.at(-1);
        const isSuggestion = lastManualChange?.annotationStatus === LogEntryStatuses.REQUESTED;
        const processedTime = isSuggestion ? lastManualChange.requestedDate : lastManualChange?.processedDate;
        changeOccurredAfterPageIsViewed = processedTime && timestampOf(processedTime) > viewTime;

        if (changeOccurredAfterPageIsViewed) {
            this.#markPageAsUnseenIfNeeded(viewedPage, processedTime);
            return {
                changeLogType: ChangeTypes.CHANGED,
                hidden: false,
            };
        }

        // Page doesn't have a view-time or no relevant changes - hide removed anyway
        return {
            changeLogType: undefined,
            hidden: lastChange ? lastChange.type === ChangeTypes.REMOVED : false,
        };
    }

    #markPageAsUnseenIfNeeded(viewedPage: ViewedPage, processedTime: string) {
        const showAsUnseen = timestampOf(viewedPage.viewedTime) < timestampOf(processedTime);
        if (showAsUnseen) {
            this._viewedPagesMapService.delete(this._state.fileId, viewedPage);
        }
    }
}
