import { effect, inject, Injectable, Signal, signal, WritableSignal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TenantsService } from '@common-ui/tenants';
import { log } from '@common-ui/utils';
import { EntitiesService, getConfig, Toaster } from '@iqser/common-ui';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import {
    ChangeType,
    ChangeTypes,
    EntryStates,
    File,
    IEntityLog,
    IEntityLogEntry,
    SuperTypeMapper,
    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 { EntityLogService } from '@services/files/entity-log.service';
import { FilesService } from '@services/files/files.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 { switchMap, tap } from 'rxjs/operators';
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: WritableSignal<AnnotationWrapper[]>;
    readonly #earmarks = signal<Map<number, AnnotationWrapper[]>>(new Map());
    #originalViewedPages: ViewedPage[] = [];
    readonly #isDocumine = getConfig().IS_DOCUMINE;
    readonly #logger = inject(NGXLogger);
    readonly #toaster = inject(Toaster);
    readonly #isIqserDevMode = inject(UserPreferenceService).isIqserDevMode;
    readonly #tenantsService = inject(TenantsService);
    protected readonly _entityClass = AnnotationWrapper;
    missingTypes = new Set<string>();
    readonly earmarks: Signal<Map<number, AnnotationWrapper[]>>;
    readonly annotations: Signal<AnnotationWrapper[]>;
    readonly annotations$: Observable<AnnotationWrapper[]>;

    constructor(
        private readonly _state: FilePreviewStateService,
        private readonly _viewedPagesService: ViewedPagesService,
        private readonly _viewedPagesMapService: ViewedPagesMapService,
        private readonly _viewModeService: ViewModeService,
        private readonly _dictionaryService: DictionaryService,
        private readonly _permissionsService: PermissionsService,
        private readonly _entityLogService: EntityLogService,
        private readonly _earmarksService: EarmarksService,
        private readonly _multiSelectService: MultiSelectService,
        private readonly _filesService: FilesService,
        private readonly _defaultColorsService: DefaultColorsService,
    ) {
        super();
        const localStorageKey = this.#tenantsService.activeTenantId + '-annotations-' + this._state.fileId;
        const storedAnnotations = JSON.parse(localStorage.getItem(localStorageKey) || '[]') as [];
        this.#annotations = signal<AnnotationWrapper[]>(
            storedAnnotations.map(a => {
                const newAnn = new AnnotationWrapper();
                Object.assign(newAnn, a);
                return newAnn;
            }),
        );
        this.annotations$ = toObservable(this.#annotations);
        this.annotations = this.#annotations.asReadonly();
        this.earmarks = this.#earmarks.asReadonly();

        effect(() => {
            localStorage.setItem(localStorageKey, JSON.stringify(this.#annotations()));
            console.log('FileDataService#annotations', this.#annotations());
        });

        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.loadEntityLog();

        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.#loadViewedPages(file);
            //     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 loadEntityLog() {
        this.#logger.info('[REDACTION_LOG] Loading redaction log...');
        const redactionLog = await this._entityLogService.getEntityLog(this._state.dossierId, this._state.fileId);

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

    async processEntityLog(entityLog: IEntityLog) {
        let annotations = await this.#convertData(entityLog);
        this.#checkMissingTypes();

        return this.#isIqserDevMode ? annotations : annotations.filter(a => !a.isFalsePositive);
    }

    updateAnnotations(file: File, analysisNumber: number) {
        const delta$ = this._entityLogService.getDelta(file.dossierId, file.fileId, analysisNumber);
        return delta$.pipe(
            log('[REDACTION_LOG] Delta loaded'),
            switchMap(delta => this.processEntityLog(delta)),
            tap(annotations => {
                const notDeleted = annotations.filter(annotation => !annotation.isRemoved);
                this.#annotations.update(old => {
                    const notUpdated = old.filter(oldAnnotation => {
                        return !oldAnnotation.pending && !annotations.some(newAnnotation => newAnnotation.id === oldAnnotation.id);
                    });
                    return [...notUpdated, ...notDeleted].sort((a, b) => a.positions[0].page - b.positions[0].page);
                });
            }),
            tap(() => this.#logger.info('[REDACTION_LOG] Annotations updated', this.#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) {
        if (viewMode === ViewModes.STANDARD) {
            return annotations.filter(annotation => !annotation.isRemoved);
        }

        if (viewMode === ViewModes.DELTA) {
            return annotations.filter(annotation => annotation.isChangeLogEntry);
        }

        return annotations.filter(annotation => annotation.isRedacted);
    }

    async #convertData(entityLog: IEntityLog) {
        const file = this._state.file();
        let annotations: AnnotationWrapper[] = [];
        const defaultColors = this._defaultColorsService.find(this._state.dossierTemplateId);
        let dictionaries = this._state.dictionaries;
        let checkDictionary = true;

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

            const isRemoved = entry.state === EntryStates.REMOVED;
            const canBeMappedToASuperType = !!SuperTypeMapper[entry.entryType][entry.state](entry) || isRemoved;
            if (!canBeMappedToASuperType) {
                if (this.#isIqserDevMode) {
                    this.#logger.warn(
                        `[ENTITY_LOG] Entity ${entry.value} (${entry.entryType}, ${entry.state}) cannot be mapped to a super type!`,
                        entry,
                    );
                }
                this.#toaster.devInfo(`Skipping entity ${entry.value} (${entry.entryType}, ${entry.state}). Check console for details.`);
                continue;
            }

            let dictionary = dictionaries.find(dict => dict.type === entry.type);
            if (!dictionary && checkDictionary) {
                const dictionaryRequest = this._dictionaryService.loadTemporaryDictionaryData(
                    this._state.dossierTemplateId,
                    this._state.isReadonly(),
                );
                dictionaries = await firstValueFrom(dictionaryRequest);
                dictionary = dictionaries.find(dict => dict.type === entry.type);
                checkDictionary = false;
            }

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

            if (dictionary?.experimental && !this.#isIqserDevMode) {
                continue;
            }

            const changeType = this.#getChangeLogType(entry, file);

            if (entry.state === EntryStates.PENDING) {
                const originalAnnotation = this.#findOriginalAnnotation(annotations, entry);
                if (originalAnnotation) {
                    entry.oldState = originalAnnotation.entry.state;
                    originalAnnotation.pending = true;
                }
            }

            const annotation = AnnotationWrapper.fromData(
                entry,
                dictionary,
                changeType,
                entityLog.legalBasis ?? [],
                this.#isDocumine,
                defaultColors,
            );

            annotations.push(annotation);

            if (annotation.pending) {
                this.#createPendingAnnotations(annotation, annotations);
            }
        }

        return annotations;
    }

    #getChangeLogType(entityLogEntry: IEntityLogEntry, file: File): ChangeType | undefined {
        const hasManualChanges = entityLogEntry.manualChanges?.length > 0;
        if (file.numberOfAnalyses <= 1 && !file.hasUpdates && !hasManualChanges) {
            return;
        }

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

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

        if (!viewedPage) {
            return;
        }

        // 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 lastChange?.type;
        }

        const viableManualChanges = entityLogEntry.manualChanges.filter(change => change.processed);
        viableManualChanges.sort(chronologicallyBy(x => x.processedDate));
        const lastManualChange = viableManualChanges.at(-1);
        const processedTime = lastManualChange?.processedDate;
        changeOccurredAfterPageIsViewed = processedTime && timestampOf(processedTime) > viewTime;

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

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

    #createPendingAnnotations(annotation: AnnotationWrapper, annotations: AnnotationWrapper[]) {
        for (let i = 0; i < annotation.positions.length; i++) {
            const pendingAnnotation = {
                ...annotation,
                id: `${annotation.id}-${i}`,
                positions: [annotation.positions[i]],
                hiddenInWorkload: true,
            } as AnnotationWrapper;
            annotations.push(pendingAnnotation);
        }
    }

    #findOriginalAnnotation(annotations: AnnotationWrapper[], pendingEntry: IEntityLogEntry) {
        for (let i = annotations.length - 1; i >= 0; i--) {
            const annotation = annotations[i];
            if (annotation.positions[0].page !== pendingEntry.positions[0].pageNumber) {
                return;
            }
            if (annotation.id === pendingEntry.id) {
                return annotation;
            }
        }
    }
}
