import { Injectable } from '@angular/core';
import { ManualRedactionService } from './manual-redaction.service';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { firstValueFrom, Observable } from 'rxjs';
import { getFirstRelevantTextPart } from '../../../utils';
import { Core } from '@pdftron/webviewer';
import {
    DictionaryEntryTypes,
    EarmarkOperation,
    ILegalBasisChangeRequest,
    IRecategorizationRequest,
    IRectangle,
    IResizeRequest,
} from '@red/domain';
import { toPosition } from '../utils/pdf-calculation.utils';
import { AnnotationDrawService } from '../../pdf-viewer/services/annotation-draw.service';
import {
    AcceptRecommendationData,
    AcceptRecommendationDialogComponent,
    AcceptRecommendationReturnType,
} from '../dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component';
import { defaultDialogConfig } from '@iqser/common-ui';
import { filter } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog';
import { FilePreviewStateService } from './file-preview-state.service';
import { FilePreviewDialogService } from './file-preview-dialog.service';
import { FileDataService } from './file-data.service';
import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service';
import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service';
import { SkippedService } from './skipped.service';
import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service';
import {
    RemoveRedactionDialogComponent,
    RemoveRedactionPermissions,
    RemoveRedactionResult,
} from '../dialogs/remove-redaction-dialog/remove-redaction-dialog.component';
import { RemoveRedactionOptions } from '../dialogs/remove-redaction-dialog/remove-redaction-options';
import { IqserDialog } from '../../../../../../../libs/common-ui/src/lib/dialog/iqser-dialog.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { isJustOne, List } from '@iqser/common-ui/lib/utils';

@Injectable()
export class AnnotationActionsService {
    constructor(
        private readonly _manualRedactionService: ManualRedactionService,
        private readonly _dialogService: FilePreviewDialogService,
        private readonly _dialog: MatDialog,
        private readonly _iqserDialog: IqserDialog,
        private readonly _pdf: PdfViewer,
        private readonly _documentViewer: REDDocumentViewer,
        private readonly _annotationManager: REDAnnotationManager,
        private readonly _annotationDrawService: AnnotationDrawService,
        private readonly _state: FilePreviewStateService,
        private readonly _fileDataService: FileDataService,
        private readonly _skippedService: SkippedService,
        private readonly _dossierTemplatesService: DossierTemplatesService,
    ) {}

    acceptSuggestion(annotations: AnnotationWrapper[]) {
        const { dossierId, fileId } = this._state;
        const ids = annotations.map(a => a.id);
        const request = this._manualRedactionService.approve(ids, dossierId, fileId);
        this.#processObsAndEmit(request).then();
    }

    removeHighlights(highlights: AnnotationWrapper[]): void {
        const data = this.#getHighlightOperationData(EarmarkOperation.REMOVE, highlights);
        this._dialogService.openDialog('highlightAction', data);
    }

    convertHighlights(highlights: AnnotationWrapper[]): void {
        const data = this.#getHighlightOperationData(EarmarkOperation.CONVERT, highlights);
        this._dialogService.openDialog('highlightAction', data);
    }

    rejectSuggestion(annotations: AnnotationWrapper[]) {
        const { dossierId, fileId } = this._state;
        this.#processObsAndEmit(
            this._manualRedactionService.declineOrRemove(
                annotations.map(a => a.id),
                dossierId,
                fileId,
                annotations[0].isModifyDictionary,
            ),
        ).then();
    }

    forceAnnotation(annotations: AnnotationWrapper[], hint: boolean = false) {
        const { dossierId, fileId } = this._state;
        const data = { dossier: this._state.dossier(), annotations, hint };
        this._dialogService.openDialog('forceAnnotation', data, (request: ILegalBasisChangeRequest) => {
            this.#processObsAndEmit(
                this._manualRedactionService.bulkForce(
                    annotations.map(a => ({ ...request, annotationId: a.id })),
                    dossierId,
                    fileId,
                    annotations[0].isIgnoredHint,
                ),
            ).then();
        });
    }

    changeLegalBasis(annotations: AnnotationWrapper[]) {
        const { dossierId, fileId } = this._state;
        this._dialogService.openDialog(
            'changeLegalBasis',
            { annotations, dossier: this._state.dossier() },
            (data: { comment: string; legalBasis: string; section: string; value: string }) => {
                const body = annotations.map(annotation => ({
                    annotationId: annotation.id,
                    comment: data.comment,
                    legalBasis: data.legalBasis,
                    section: data.section,
                    value: data.value,
                }));

                this.#processObsAndEmit(this._manualRedactionService.changeLegalBasis(body, dossierId, fileId)).then();
            },
        );
    }

    async removeOrSuggestRemoveRedaction(redaction: AnnotationWrapper, permissions) {
        const removePermissions: RemoveRedactionPermissions = {
            canRemoveOrSuggestToRemoveOnlyHere: permissions.canRemoveOrSuggestToRemoveOnlyHere,
            canRemoveOrSuggestToRemoveFromDictionary: permissions.canRemoveOrSuggestToRemoveFromDictionary,
            canMarkAsFalsePositive: permissions.canMarkAsFalsePositive,
        };

        const result: RemoveRedactionResult = await this._iqserDialog
            .openDefault(RemoveRedactionDialogComponent, {
                data: {
                    redaction,
                    dossier: this._state.dossier(),
                    falsePositiveContext: this._getFalsePositiveText(redaction),
                    permissions: removePermissions,
                },
            })
            .result();

        if (result) {
            if (result.option.value === RemoveRedactionOptions.FALSE_POSITIVE) {
                this.#setAsFalsePositive(redaction, result.comment);
            } else {
                const removeFromDictionary = result.option.value === RemoveRedactionOptions.IN_DOSSIER;
                this.#removeRedaction(redaction, result.comment, removeFromDictionary);
            }
        }

        if (result.option.extraOption) {
            const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId);
            const { ...body } = dossierTemplate;
            body.applyDictionaryUpdatesToAllDossiersByDefault = result.applyToAllDossiers;
            await this._dossierTemplatesService.createOrUpdate(body);
        }
    }

    recategorizeImages(annotations: AnnotationWrapper[]) {
        const data = { annotations, dossier: this._state.dossier() };
        const { dossierId, fileId } = this._state;
        this._dialogService.openDialog('recategorizeImage', data, ({ comment, type }: { type: string; comment: string }) => {
            const body: List<IRecategorizationRequest> = annotations.map(({ id }) => ({
                annotationId: id,
                type,
                comment,
            }));
            this.#processObsAndEmit(this._manualRedactionService.recategorizeImage(body, dossierId, fileId)).then();
        });
    }

    undoDirectAction(annotations: AnnotationWrapper[]) {
        const { dossierId, fileId } = this._state;
        const modifyDictionary = annotations[0].isModifyDictionary;
        this.#processObsAndEmit(
            this._manualRedactionService.undoRequest(
                annotations.map(a => a.id),
                dossierId,
                fileId,
                modifyDictionary,
            ),
        ).then();
    }

    async convertRecommendationToAnnotation(recommendations: AnnotationWrapper[]) {
        const { dossierId, fileId } = this._state;
        const dialogRef = this._dialog.open<AcceptRecommendationDialogComponent, AcceptRecommendationData, AcceptRecommendationReturnType>(
            AcceptRecommendationDialogComponent,
            { ...defaultDialogConfig, autoFocus: true, data: { annotations: recommendations, dossierId } },
        );
        const dialogClosed = dialogRef.afterClosed().pipe(filter(value => !!value && !!value.annotations));
        await firstValueFrom(dialogClosed).then(({ annotations, comment: commentText }) => {
            if (isJustOne(annotations) && this._annotationManager.resizingAnnotationId === annotations[0].id) {
                this.cancelResize(annotations[0]).then();
            }
            const comment = commentText ? { text: commentText } : undefined;
            this.#processObsAndEmit(this._manualRedactionService.addRecommendation(annotations, dossierId, fileId, comment));
        });
    }

    async resize(annotationWrapper: AnnotationWrapper) {
        this._annotationManager.resizingAnnotationId = annotationWrapper.id;

        if (annotationWrapper.rectangle || annotationWrapper.imported || annotationWrapper.isImage) {
            this._annotationManager.delete(annotationWrapper);
            const rectangleAnnotation = this.#generateRectangle(annotationWrapper);
            await this._annotationManager.add(rectangleAnnotation);
            return;
        }

        const viewerAnnotation = this._annotationManager.get(annotationWrapper);
        viewerAnnotation.ReadOnly = false;
        viewerAnnotation.Hidden = false;
        viewerAnnotation.disableRotationControl();
        this._annotationManager.redraw(viewerAnnotation);
        this._annotationManager.select(viewerAnnotation);
    }

    async acceptResize(annotation: AnnotationWrapper): Promise<void> {
        const textAndPositions = await this.#extractTextAndPositions(annotation.id);
        if (annotation.isRecommendation) {
            const recommendation = {
                ...annotation,
                value: textAndPositions.text,
                positions: textAndPositions.positions,
            } as AnnotationWrapper;
            return this.convertRecommendationToAnnotation([recommendation]);
        }

        const isImageText = annotation.isImage ? 'Image' : textAndPositions.text;
        const text = annotation.rectangle ? annotation.value : isImageText;
        const data = { annotation, text };

        this._dialogService.openDialog('resizeAnnotation', data, (result: { comment: string; updateDictionary: boolean }) => {
            const resizeRequest: IResizeRequest = {
                annotationId: annotation.id,
                comment: result.comment,
                positions: textAndPositions.positions,
                value: text,
                updateDictionary: result.updateDictionary,
            };

            this.cancelResize(annotation).then();

            const { fileId, dossierId } = this._state;
            const request = this._manualRedactionService.resizeOrSuggestResize([resizeRequest], dossierId, fileId);
            this.#processObsAndEmit(request);
        });
    }

    async cancelResize(annotationWrapper: AnnotationWrapper) {
        this._annotationManager.resizingAnnotationId = undefined;
        this._annotationManager.delete(annotationWrapper);
        await this._annotationDrawService.draw([annotationWrapper], this._skippedService.hideSkipped(), this._state.dossierTemplateId);
        this._annotationManager.deselect();
        await this._fileDataService.annotationsChanged();
    }

    #generateRectangle(annotationWrapper: AnnotationWrapper) {
        const annotation = this._pdf.rectangle();
        const pageHeight = this._documentViewer.getHeight(annotationWrapper.pageNumber);
        const rectangle: IRectangle = annotationWrapper.positions[0];
        annotation.PageNumber = annotationWrapper.pageNumber;
        annotation.X = rectangle.topLeft.x;
        annotation.Y = pageHeight - (rectangle.topLeft.y + rectangle.height);
        annotation.Width = rectangle.width;
        annotation.Height = rectangle.height;
        annotation.ReadOnly = false;
        annotation.Hidden = false;
        annotation.disableRotationControl();
        annotation.FillColor = this._annotationDrawService.convertColor(annotationWrapper.color);
        annotation.StrokeColor = annotation.FillColor;
        annotation.Opacity = 0.6;
        annotation.StrokeThickness = 1;
        annotation.Id = annotationWrapper.id;
        return annotation;
    }

    #getHighlightOperationData(operation: EarmarkOperation, highlights: AnnotationWrapper[]) {
        return {
            dossierId: this._state.dossierId,
            fileId: this._state.fileId,
            operation,
            highlights,
            annotationsChanged: async () => {
                await this._fileDataService.annotationsChanged();
            },
        };
    }

    async #processObsAndEmit(obs: Observable<unknown>) {
        await firstValueFrom(obs)
            .then(() => this._fileDataService.annotationsChanged())
            .catch(() => this._fileDataService.annotationsChanged());
    }

    private _getFalsePositiveText(annotation: AnnotationWrapper) {
        if (!annotation.canBeMarkedAsFalsePositive) {
            return;
        }

        if (annotation.isRecommendation) {
            return annotation.value;
        }

        let text: string;
        if (annotation.hasTextAfter) {
            text = getFirstRelevantTextPart(annotation.textAfter, 'FORWARD');
            return text ? (annotation.value + text).trim() : annotation.value;
        }

        if (annotation.hasTextBefore) {
            text = getFirstRelevantTextPart(annotation.textBefore, 'BACKWARD');
            return text ? (text + annotation.value).trim() : annotation.value;
        }

        return annotation.value;
    }

    async #extractTextAndPositions(annotationId: string) {
        const viewerAnnotation = this._annotationManager.get(annotationId);

        const document = await this._documentViewer.PDFDoc;
        const page = await document.getPage(viewerAnnotation.getPageNumber());
        if (this._pdf.isTextHighlight(viewerAnnotation)) {
            const words = [];
            const rectangles: IRectangle[] = [];
            for (const quad of viewerAnnotation.Quads) {
                const rect = toPosition(
                    viewerAnnotation.getPageNumber(),
                    this._documentViewer.getHeight(viewerAnnotation.getPageNumber()),
                    this._pdf.translateQuad(viewerAnnotation.getPageNumber(), quad),
                );
                rectangles.push(rect);

                /**
                 * this is an educated guess for lines that are close together
                 * so that we do not extract text from line above/line below
                 */
                const percentHeightOffset = rect.height / 10;

                const pdfNetRect = new this._pdf.PDFNet.Rect(
                    rect.topLeft.x + 2,
                    rect.topLeft.y + percentHeightOffset,
                    rect.topLeft.x + rect.width - 2,
                    rect.topLeft.y + rect.height - percentHeightOffset,
                );
                const quadWords = await this._extractTextFromRect(page, pdfNetRect);
                words.push(...quadWords);
            }

            return {
                text: words.join(' '),
                positions: rectangles,
            };
        }

        const position = toPosition(
            viewerAnnotation.getPageNumber(),
            this._documentViewer.getHeight(viewerAnnotation.getPageNumber()),
            this._annotationDrawService.annotationToQuads(viewerAnnotation),
        );

        return {
            positions: [position],
            text: null,
        };
    }

    private async _extractTextFromRect(page: Core.PDFNet.Page, rect: Core.PDFNet.Rect) {
        const txt = await this._pdf.PDFNet.TextExtractor.create();
        await txt.begin(page, rect); // Read the page.

        const words: string[] = [];
        // Extract words one by one.
        let line = await txt.getFirstLine();
        for (; await line.isValid(); line = await line.getNextLine()) {
            for (let word = await line.getFirstWord(); await word.isValid(); word = await word.getNextWord()) {
                words.push(await word.getString());
            }
        }
        return words;
    }

    #setAsFalsePositive(redaction: AnnotationWrapper, comment: string) {
        const request = {
            sourceId: redaction.id,
            value: this._getFalsePositiveText(redaction),
            type: redaction.type,
            positions: redaction.positions,
            addToDictionary: true,
            reason: 'False Positive',
            dictionaryEntryType: redaction.isRecommendation
                ? DictionaryEntryTypes.FALSE_RECOMMENDATION
                : DictionaryEntryTypes.FALSE_POSITIVE,
            comment: comment ? { text: comment } : null,
        };
        const { dossierId, fileId } = this._state;

        this.#processObsAndEmit(this._manualRedactionService.addAnnotation([request], dossierId, fileId)).then();
    }

    #removeRedaction(redaction: AnnotationWrapper, comment: string, removeFromDictionary: boolean) {
        const body = {
            annotationId: redaction.id,
            comment: comment,
            removeFromDictionary,
        };
        const { dossierId, fileId } = this._state;
        this.#processObsAndEmit(
            this._manualRedactionService.removeOrSuggestRemove([body], dossierId, fileId, removeFromDictionary, redaction.isHint),
        ).then();
    }
}
