import { Injectable } from '@angular/core';
import { IqserDialog } from '@common-ui/dialog/iqser-dialog.service';
import { getConfig, Toaster } from '@iqser/common-ui';
import { List, log } from '@iqser/common-ui/lib/utils';
import { AnnotationPermissions } from '@models/file/annotation.permissions';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Core } from '@pdftron/webviewer';
import {
    DictionaryEntryTypes,
    EarmarkOperation,
    ILegalBasisChangeRequest,
    IRecategorizationRequest,
    IRectangle,
    IResizeRequest,
} from '@red/domain';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { PermissionsService } from '@services/permissions.service';
import { firstValueFrom, Observable, zip } from 'rxjs';
import { getFirstRelevantTextPart } from '../../../utils';
import { AnnotationDrawService } from '../../pdf-viewer/services/annotation-draw.service';
import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service';
import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service';
import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service';
import { EditAnnotationDialogComponent } from '../dialogs/docu-mine/edit-annotation-dialog/edit-annotation-dialog.component';
import { RemoveAnnotationDialogComponent } from '../dialogs/docu-mine/remove-annotation-dialog/remove-annotation-dialog.component';
import { ResizeAnnotationDialogComponent } from '../dialogs/docu-mine/resize-annotation-dialog/resize-annotation-dialog.component';
import { EditRedactionDialogComponent } from '../dialogs/edit-redaction-dialog/edit-redaction-dialog.component';
import { RedactRecommendationDialogComponent } from '../dialogs/redact-recommendation-dialog/redact-recommendation-dialog.component';
import { RemoveRedactionDialogComponent } from '../dialogs/remove-redaction-dialog/remove-redaction-dialog.component';
import { ResizeRedactionDialogComponent } from '../dialogs/resize-redaction-dialog/resize-redaction-dialog.component';
import { RemoveRedactionOptions } from '../utils/dialog-options';
import {
    EditRedactionData,
    RemoveRedactionData,
    RemoveRedactionPermissions,
    RemoveRedactionResult,
    ResizeRedactionData,
} from '../utils/dialog-types';
import { toPosition } from '../utils/pdf-calculation.utils';
import { FileDataService } from './file-data.service';
import { FilePreviewDialogService } from './file-preview-dialog.service';
import { FilePreviewStateService } from './file-preview-state.service';
import { ManualRedactionService } from './manual-redaction.service';
import { SkippedService } from './skipped.service';

@Injectable()
export class AnnotationActionsService {
    readonly #isDocumine = getConfig().IS_DOCUMINE;

    constructor(
        private readonly _manualRedactionService: ManualRedactionService,
        private readonly _dialogService: FilePreviewDialogService,
        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,
        private readonly _permissionsService: PermissionsService,
        private readonly _toaster: Toaster,
    ) {}

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

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

    async editRedaction(annotations: AnnotationWrapper[]) {
        const { dossierId, dossierTemplateId, fileId } = this._state;
        const dossierTemplate = this._dossierTemplatesService.find(dossierTemplateId);
        const data = {
            annotations,
            dossierId,
            applyToAllDossiers: dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault,
        };

        const result = await this.#getEditRedactionDialog(data).result();

        const requests: Observable<unknown>[] = [];

        if (!result) {
            return;
        }

        if (
            !this.#isDocumine &&
            (!annotations.every(annotation => annotation.legalBasis === result.legalBasis) ||
                !annotations.every(annotation => annotation.section === result.section))
        ) {
            const changeLegalBasisBody = annotations.map(annotation => ({
                annotationId: annotation.id,
                legalBasis: result.legalBasis,
                section: result.section ?? annotation.section,
                value: result.value ?? annotation.value,
            }));
            requests.push(this._manualRedactionService.changeLegalBasis(changeLegalBasisBody, dossierId, fileId));
        }
        if (result.type && !annotations.every(annotation => annotation.type === result.type)) {
            const recategorizeBody: List<IRecategorizationRequest> = annotations.map(annotation => ({
                annotationId: annotation.id,
                type: result.type ?? annotation.type,
            }));
            requests.push(this._manualRedactionService.recategorizeRedactions(recategorizeBody, dossierId, fileId));
        }

        if (result.comment) {
            try {
                for (const a of annotations) {
                    await this._manualRedactionService.addComment(result.comment, a.id, dossierId, fileId);
                }
            } catch (error) {
                this._toaster.rawError(error.error.message);
            }
        }

        if (!requests.length) {
            return;
        }
        await this.#processObsAndEmit(zip(requests).pipe(log()));
    }

    async removeRedaction(redactions: AnnotationWrapper[], permissions: AnnotationPermissions) {
        const removePermissions: RemoveRedactionPermissions = {
            canRemoveOnlyHere: permissions.canRemoveOnlyHere,
            canRemoveFromDictionary: permissions.canRemoveFromDictionary,
            canMarkAsFalsePositive: permissions.canMarkAsFalsePositive,
        };
        const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId);
        const isApprover = this._permissionsService.isApprover(this._state.dossier());

        const data = {
            redactions,
            dossier: this._state.dossier(),
            falsePositiveContext: redactions.map(r => this.#getFalsePositiveText(r)),
            permissions: removePermissions,
            applyToAllDossiers: isApprover ? dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault : false,
            isApprover,
        };

        const result = await this.#getRemoveRedactionDialog(data).result();

        if (!result) {
            return;
        }

        if (
            result.option.value === RemoveRedactionOptions.FALSE_POSITIVE ||
            result.option.value === RemoveRedactionOptions.DO_NOT_RECOMMEND
        ) {
            this.#setAsFalsePositive(redactions, result);
        } else {
            this.#removeRedaction(redactions, result);
        }
    }

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

    async convertRecommendationToAnnotation(recommendations: AnnotationWrapper[]) {
        const { dossierId, fileId } = this._state;
        const data = this.#getRedactRecommendationDialogData(recommendations);
        const dialog = this._iqserDialog.openDefault(RedactRecommendationDialogComponent, { data });
        const result = await dialog.result();
        if (!result) {
            return;
        }

        if (!result.isMulti && this._annotationManager.resizingAnnotationId === recommendations[0].id) {
            this.cancelResize(recommendations[0]).then();
        }

        const request$ = this._manualRedactionService.addRecommendation(recommendations, result.redaction, dossierId, fileId);
        return this.#processObsAndEmit(request$);
    }

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

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

        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 dossier = this._state.dossier();

        const isImageText = annotation.isImage ? 'Image' : textAndPositions.text;
        const text = annotation.rectangle ? annotation.value : isImageText;
        const isApprover = this._permissionsService.isApprover(dossier);
        const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId);

        const data = {
            redaction: annotation,
            text,
            applyToAllDossiers: isApprover && dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault,
            isApprover,
            dossierId: dossier.dossierId,
        };

        const result = await this.#getResizeRedactionDialog(data).result();

        if (!result) {
            return;
        }

        const resizeRequest: IResizeRequest = {
            annotationId: annotation.id,
            comment: result.comment,
            positions: textAndPositions.positions,
            value: text,
            updateDictionary: result.updateDictionary,
            addToAllDossiers: result.addToAllDossiers,
        };

        await this.cancelResize(annotation);

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

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

    #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).finally(() => this._fileDataService.annotationsChanged());
    }

    #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,
        };
    }

    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(redactions: AnnotationWrapper[], dialogResult: RemoveRedactionResult) {
        const requests = redactions.map(redaction => ({
            sourceId: redaction.id,
            value: this.#getFalsePositiveText(redaction),
            type: redaction.type,
            positions: redaction.positions,
            addToDictionary: true,
            addToAllDossiers: !!dialogResult.option.extraOption?.checked || !!dialogResult.applyToAllDossiers,
            reason: 'False Positive',
            dictionaryEntryType: redaction.isRecommendation
                ? DictionaryEntryTypes.FALSE_RECOMMENDATION
                : DictionaryEntryTypes.FALSE_POSITIVE,
            comment: dialogResult.comment ? { text: dialogResult.comment } : null,
        }));
        const { dossierId, fileId } = this._state;

        this.#processObsAndEmit(this._manualRedactionService.addAnnotation(requests, dossierId, fileId)).then();
    }

    #removeRedaction(redactions: AnnotationWrapper[], dialogResult: RemoveRedactionResult) {
        const removeFromDictionary = dialogResult.option.value === RemoveRedactionOptions.IN_DOSSIER;
        const body = redactions.map(redaction => ({
            annotationId: redaction.id,
            comment: dialogResult.comment,
            removeFromDictionary,
            removeFromAllDossiers: !!dialogResult.option.extraOption?.checked || !!dialogResult.applyToAllDossiers,
        }));
        // todo: might not be correct, probably shouldn't get to this point if they are not all the same
        const isHint = redactions.every(r => r.isHint);
        const { dossierId, fileId } = this._state;
        this.#processObsAndEmit(this._manualRedactionService.removeRedaction(body, dossierId, fileId, removeFromDictionary, isHint)).then();
    }

    #getRemoveRedactionDialog(data: RemoveRedactionData) {
        if (this.#isDocumine) {
            return this._iqserDialog.openDefault(RemoveAnnotationDialogComponent, { data });
        }
        return this._iqserDialog.openDefault(RemoveRedactionDialogComponent, { data });
    }

    #getResizeRedactionDialog(data: ResizeRedactionData) {
        if (this.#isDocumine) {
            return this._iqserDialog.openDefault(ResizeAnnotationDialogComponent, { data });
        }
        return this._iqserDialog.openDefault(ResizeRedactionDialogComponent, { data });
    }

    #getEditRedactionDialog(data: EditRedactionData) {
        if (this.#isDocumine) {
            return this._iqserDialog.openDefault(EditAnnotationDialogComponent, { data });
        }
        return this._iqserDialog.openDefault(EditRedactionDialogComponent, { data });
    }

    #getRedactRecommendationDialogData(annotations: AnnotationWrapper[]) {
        const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId);
        const isApprover = this._permissionsService.isApprover(this._state.dossier());
        const applyDictionaryUpdatesToAllDossiersByDefault = dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault;

        return {
            annotations,
            dossierId: this._state.dossierId,
            applyToAllDossiers: isApprover ? applyDictionaryUpdatesToAllDossiersByDefault : false,
            isApprover,
        };
    }
}
