import { TextGranularity, type Annotation, type SavedAnnotation } from "./types/Annotation";
import type { Source } from "./types/Source";
import type { Settings } from "./types/Settings";

// TODO: Add a typedef for the Annotorious client (anno)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
/**
 * Annotorious plugin to use W3C protocol storage
 */
class AnnotationServerStorage {
    anno;

    annotationCount: number;

    settings: Settings;

    /**
     * Instantiate the storage plugin.
     *
     * @param {any} anno Instance of the Annotorious client.
     * @param {Settings} settings Settings object for the storage plugin.
     */
    constructor(
        anno: any, // eslint-disable-line @typescript-eslint/no-explicit-any
        settings: Settings,
    ) {
        this.anno = anno;
        this.settings = settings;
        this.annotationCount = 0;

        // bind event handlers
        this.anno.on(
            "createAnnotation",
            this.handleCreateAnnotation.bind(this),
        );
        this.anno.on(
            "updateAnnotation",
            this.handleUpdateAnnotation.bind(this),
        );
        this.anno.on(
            "deleteAnnotation",
            this.handleDeleteAnnotation.bind(this),
        );

        // load annotations from the server and signal for display
        this.loadAnnotations();
    }

    /**
     * Helper function to load annotations asynchronously once the plugin
     * is initialized.
     */
    async loadAnnotations() {
        try {
            const annotations: void | SavedAnnotation[] = await this.search(
                this.settings.target,
            );
            if (this.settings.lineMode) {
                // in line-by-line editing mode, only render line-level annotations in annotorious
                await this.anno.setAnnotations(
                    annotations?.filter((a) => a.textGranularity === TextGranularity.LINE),
                );
            } else {
                // otherwise render block-level annotations
                await this.anno.setAnnotations(annotations);
            }
            if (annotations instanceof Array) {
                this.annotationCount = annotations.length;
            }
            document.dispatchEvent(
                new CustomEvent("annotations-loaded", {
                    detail: {
                        // include target with event to match canvases
                        target: this.settings.target,
                        // include annotations here (annotorious might be briefly out of sync)
                        annotations,
                    },
                }),
            );
            return annotations;
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
        } catch (err: any) {
            this.alert(err.message, "error");
        }
    }

    /**
     * Event handler for the createAnnotation event; adjusts the source if
     * needed, saves the annotation to the store and Annotorious, then returns
     * the stored annotation retrieved from storage in a Promise.
     *
     * @param {Annotation} annotation V3 (W3C) annotation
     */
    async handleCreateAnnotation(
        annotation: Annotation,
    ): Promise<Annotation | void> {
        try {
            annotation.target.source = this.adjustTargetSource(
                annotation.target.source,
            );
            // save source URI to dc:source attribute on annotation
            if (this.settings.sourceUri) {
                annotation["dc:source"] = this.settings.sourceUri;
            }

            // save primary and secondary (if applicable) motivation on annotation
            annotation.motivation = this.settings.secondaryMotivation
                ? ["sc:supplementing", this.settings.secondaryMotivation]
                : "sc:supplementing";

            // increment annotation count and set position attribute
            this.setAnnotationCount(this.annotationCount + 1);
            if (!annotation["schema:position"]) {
                annotation["schema:position"] = this.annotationCount;
            }

            // wait for adapter to return saved annotation from storage
            const newAnnotation: Annotation = await this.create(annotation);
            // remove the annotation with the provisional ID from Annotorious display
            this.anno.removeAnnotation(annotation.id);
            // add the saved annotation returned by storage to Annotorious display
            this.anno.addAnnotation(newAnnotation);

            // reload annotations
            // TODO: Avoid extra network request here
            await this.loadAnnotations();
            this.alert("Annotation created", "success");
            return await Promise.resolve(newAnnotation);
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
        } catch (err: any) {
            this.alert(err.message, "error");
        }
    }

    /**
     * Event handler for the updateAnnotation event; adjusts the source if
     * needed, then updates the annotation in the store and in Annotorious,
     *
     * @param {SavedAnnotation} annotation Updated annotation.
     * @param {SavedAnnotation} previous Previously saved annotation.
     */
    async handleUpdateAnnotation(
        annotation: SavedAnnotation,
        previous: SavedAnnotation,
    ): Promise<Annotation | void> {
        // The posted annotation should have an @id which exists in the store
        // we want to keep the same id, so we update the new annotation with
        // the previous id before saving.
        annotation.id = previous.id;
        // target needs to be updated if the image selection has changed
        annotation.target.source = this.adjustTargetSource(
            annotation.target.source,
        );
        try {
            const updatedAnnotation: SavedAnnotation = await this.update(
                annotation,
            );
            // redisplay the updated annotation in annotorious
            this.anno.addAnnotation(updatedAnnotation);
            // reload annotations from storage (for post-save effects e.g. html sanitization)
            await this.loadAnnotations();
            this.alert("Annotation saved", "success");
            return await Promise.resolve(updatedAnnotation);
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
        } catch (err: any) {
            // in case of an error, ensure annotation gets re-selected while editor still open
            this.anno.selectAnnotation(annotation);
            this.alert(err.message, "error");
        }
    }

    /**
     * Update the annotation in the store only (i.e. when image annotation editing is disabled).
     *
     * @param {SavedAnnotation} annotation Updated annotation.
     */
    async handleUpdateAnnotationInStore(
        annotation: SavedAnnotation,
    ): Promise<Annotation | void> {
        try {
            const updatedAnnotation: SavedAnnotation = await this.update(
                annotation,
            );
            // reload annotations from storage (for post-save effects e.g. html sanitization)
            await this.loadAnnotations();
            this.alert("Annotation saved", "success");
            return await Promise.resolve(updatedAnnotation);
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
        } catch (err: any) {
            this.alert(err.message, "error");
        }
    }

    /**
     * Event handler for the deleteAnnotation event; deletes the annotation
     * from the store.
     *
     * @param {SavedAnnotation} annotation Annotation to delete; must have an
     * id property that matches its id property in the store.
     */
    async handleDeleteAnnotation(annotation: SavedAnnotation): Promise<void> {
        try {
            await this.delete(annotation);
            this.setAnnotationCount(this.annotationCount - 1);
            await this.loadAnnotations();
            this.alert("Annotation deleted", "success");
            return await Promise.resolve();
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
        } catch (err: any) {
            // in case of an error, ensure annotation gets re-selected while editor still open
            this.anno.selectAnnotation(annotation);
            this.alert(err.message, "error");
        }
    }

    /**
     * Set the annotation count, for use in calculating position
     *
     * @param {number} count The new count
     */
    setAnnotationCount(count: number) {
        this.annotationCount = count;
    }

    /**
     * Save a new annotation to storage.
     *
     * @param {Annotation} annotation V3 (W3C) annotation to save.
     */
    async create(annotation: Annotation): Promise<SavedAnnotation> {
        const res = await fetch(`${this.settings.annotationEndpoint}`, {
            body: JSON.stringify(annotation),
            headers: {
                Accept: "application/json",
                "Content-Type": "application/json",
                "X-CSRFToken": this.settings.csrf_token,
            },
            method: "POST",
        });
        // fetch won't automatically throw error on bad HTTP code, so check for ok
        if (res.ok) {
            return res.json();
        } else {
            throw new Error(
                `Error creating annotation: ${res.status} ${res.statusText}`,
            );
        }
    }

    /**
     * Update an existing annotation in storage.
     *
     * @param {SavedAnnotation} annotation V3 (W3C) annotation to update.
     */
    async update(annotation: SavedAnnotation): Promise<SavedAnnotation> {
        // post the revised annotation to its URI
        const res = await fetch(annotation.id, {
            body: JSON.stringify(annotation),
            headers: {
                Accept: "application/json",
                "Content-Type": "application/json",
                "X-CSRFToken": this.settings.csrf_token,
                // ensure match with the previously fetched ETag
                "If-Match": annotation.etag,
            },
            method: "POST",
        });
        if (res.ok) {
            return res.json();
        } else if (res.status === 412) {
            throw new Error(
                `Error: Annotation was modified by another user while you were working.
                Refresh the page to get the latest version, then make your changes.`,
            );
        } else {
            throw new Error(
                `Error updating annotation: ${res.status} ${res.statusText}`,
            );
        }
    }

    /**
     *
     * Delete an existing annotation from storage.
     *
     * @param {SavedAnnotation} annotation to delete
     */
    async delete(annotation: SavedAnnotation) {
        const res = await fetch(annotation.id, {
            headers: {
                Accept: "application/json",
                "Content-Type": "application/json",
                "X-CSRFToken": this.settings.csrf_token,
                // ensure match with the previously fetched ETag
                "If-Match": annotation.etag,
            },
            method: "DELETE",
        });
        if (res.ok) {
            return res;
        } else if (res.status === 412) {
            throw new Error(
                `Error: Annotation was modified by another user.
                Refresh the page to get the latest version, then delete it.`,
            );
        } else {
            throw new Error(
                `Error deleting annotation: ${res.status} ${res.statusText}`,
            );
        }
    }

    /**
     *
     * Search for annotations on the specified target, ordered by schema:position attribute.
     *
     * @param {string} targetUri URI of the target to search for
     */
    async search(targetUri: string): Promise<void | SavedAnnotation[]> {
        const { annotationEndpoint, sourceUri, manifest, secondaryMotivation } =
            this.settings;
        const sourceQ = sourceUri ? `&source=${sourceUri}` : "";
        const manifestQ = manifest ? `&manifest=${manifest}` : "";
        const motivationQ = secondaryMotivation
            ? `&motivation=${secondaryMotivation}`
            : "";
        const res = await fetch(
            `${annotationEndpoint}search/?uri=${targetUri}${sourceQ}${manifestQ}${motivationQ}`,
            {
                headers: {
                    Accept: "application/json",
                    "Content-Type": "application/json",
                    "X-CSRFToken": this.settings.csrf_token,
                },
            },
        );
        if (res.ok) {
            const data = await res.json();
            return <SavedAnnotation[]>data.resources;
        } else {
            throw new Error(
                `Error retrieving annotations: ${res.status} ${res.statusText}`,
            );
        }
    }

    /**
     * Utility function to change a source string (Annotorious output) into a
     * Source object, in order to to add canvas/manifest info.
     *
     * @param {Source|string} source Source to be adjusted
     * @returns {Source} Source object with set target and manifest
     */
    adjustTargetSource(source: Source | string): Source {
        if (typeof source == "string") {
            // add manifest id to annotation
            source = {
                // use the configured target (should be canvas id)
                id: this.settings.target,
                // link to containing manifest
                partOf: {
                    id: this.settings.manifest,
                    type: "Manifest",
                },
                type: "Canvas",
            };
        }
        return source;
    }

    /**
     * Raises a custom event, tahqiq-alert, with passed message/status and the
     * target (i.e. canvas) with which this instance is associated.
     *
     * @param {string} message Message for the alert.
     * @param {string} status Optional alert status.
     */
    alert(message: string, status?: string) {
        document.dispatchEvent(
            new CustomEvent("tahqiq-alert", {
                detail: {
                    message,
                    status: status || "info",
                    target: this.settings.target,
                },
            }),
        );
    }
}

export default AnnotationServerStorage;
