import { AttributeType } from "@triliumnext/commons";

import type FNote from "../entities/fnote.js";
import froca from "./froca.js";
import type { AttributeRow } from "./load_results.js";
import server from "./server.js";

async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
    await server.put(`notes/${noteId}/attribute`, {
        type: "label",
        name,
        value,
        isInheritable
    });
}

export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false, componentId?: string) {
    await server.put(`notes/${noteId}/set-attribute`, {
        type: "label",
        name,
        value,
        isInheritable,
    }, componentId);
}

export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) {
    await server.put(`notes/${noteId}/set-attribute`, {
        type: "relation",
        name,
        value,
        isInheritable
    });
}

/**
 * Sets a boolean label on the given note, taking inheritance into account. If the desired value matches the inherited
 * value, any owned label will be removed to allow the inherited value to take effect. If the desired value differs
 * from the inherited value, an owned label will be created or updated to reflect the desired value.
 *
 * When checking if the boolean value is set, don't use `note.hasLabel`; instead use `note.isLabelTruthy`.
 *
 * @param note the note on which to set the boolean label.
 * @param labelName the name of the label to set.
 * @param value the boolean value to set for the label.
 */
export async function setBooleanWithInheritance(note: FNote, labelName: string, value: boolean) {
    const actualValue = note.isLabelTruthy(labelName);
    if (actualValue === value) return;
    const hasInheritedValue = !note.hasOwnedLabel(labelName) && note.hasLabel(labelName);

    if (hasInheritedValue) {
        if (value) {
            setLabel(note.noteId, labelName, "");
        } else {
            // Label is inherited - override to false.
            setLabel(note.noteId, labelName, "false");
        }
    } else if (value) {
        setLabel(note.noteId, labelName, "");
    } else {
        removeOwnedLabelByName(note, labelName);
    }
}

async function removeAttributeById(noteId: string, attributeId: string) {
    await server.remove(`notes/${noteId}/attributes/${attributeId}`);
}

export async function removeOwnedAttributesByNameOrType(note: FNote, type: AttributeType, name: string) {
    for (const attr of note.getOwnedAttributes()) {
        if (attr.type === type && attr.name === name) {
            await server.remove(`notes/${note.noteId}/attributes/${attr.attributeId}`);
        }
    }
}

/**
 * Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e.
 * it will not remove inherited attributes.
 *
 * @param note the note from which to remove the label.
 * @param labelName the name of the label to remove.
 * @returns `true` if an attribute was identified and removed, `false` otherwise.
 */
function removeOwnedLabelByName(note: FNote, labelName: string) {
    const label = note.getOwnedLabel(labelName);
    if (label) {
        removeAttributeById(note.noteId, label.attributeId);
        return true;
    }
    return false;
}

/**
 * Removes a relation identified by its name from the given note, if it exists. Note that the relation must be owned, i.e.
 * it will not remove inherited attributes.
 *
 * @param note the note from which to remove the relation.
 * @param relationName the name of the relation to remove.
 * @returns `true` if an attribute was identified and removed, `false` otherwise.
 */
function removeOwnedRelationByName(note: FNote, relationName: string) {
    const relation = note.getOwnedRelation(relationName);
    if (relation) {
        removeAttributeById(note.noteId, relation.attributeId);
        return true;
    }
    return false;
}

/**
 * Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy.
 * For an attribute with an empty value, pass an empty string instead.
 *
 * @param note the note to set the attribute to.
 * @param type the type of attribute (label or relation).
 * @param name the name of the attribute to set.
 * @param value the value of the attribute to set.
 */
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined, componentId?: string) {
    if (value !== null && value !== undefined) {
        // Create or update the attribute.
        await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }, componentId);
    } else {
        // Remove the attribute if it exists on the server but we don't define a value for it.
        const attributeId = note.getAttribute(type, name)?.attributeId;
        if (attributeId) {
            await server.remove(`notes/${note.noteId}/attributes/${attributeId}`, componentId);
        }
    }
}

/**
 * @returns - returns true if this attribute has the potential to influence the note in the argument.
 *         That can happen in multiple ways:
 *         1. attribute is owned by the note
 *         2. attribute is owned by the template of the note
 *         3. attribute is owned by some note's ancestor and is inheritable
 */
function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefined) {
    if (!affectedNote || !attrRow) {
        return false;
    }

    const attrNote = attrRow.noteId && froca.notes[attrRow.noteId];

    if (!attrNote) {
        // the note (owner of the attribute) is not even loaded into the cache, so it should not affect anything else
        return false;
    }

    const owningNotes = [affectedNote, ...affectedNote.getNotesToInheritAttributesFrom()];

    for (const owningNote of owningNotes) {
        if (owningNote.noteId === attrNote.noteId) {
            return true;
        }
    }

    if (attrRow.isInheritable) {
        for (const owningNote of owningNotes) {
            if (owningNote.hasAncestor(attrNote.noteId, true)) {
                return true;
            }
        }
    }

    return false;
}

/**
 * Toggles whether a dangerous attribute is enabled or not. When an attribute is disabled, its name is prefixed with `disabled:`.
 *
 * Note that this work for non-dangerous attributes as well.
 *
 * If there are multiple attributes with the same name, all of them will be toggled at the same time.
 *
 * @param note the note whose attribute to change.
 * @param type the type of dangerous attribute (label or relation).
 * @param name the name of the dangerous attribute.
 * @param willEnable whether to enable or disable the attribute.
 * @returns a promise that will resolve when the request to the server completes.
 */
async function toggleDangerousAttribute(note: FNote, type: "label" | "relation", name: string, willEnable: boolean) {
    const attrs = [
        ...note.getOwnedAttributes(type, name),
        ...note.getOwnedAttributes(type, `disabled:${name}`)
    ];

    for (const attr of attrs) {
        const baseName = getNameWithoutDangerousPrefix(attr.name);
        const newName = willEnable ? baseName : `disabled:${baseName}`;
        if (newName === attr.name) continue;

        // We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically.
        if (attr.type === "label") {
            await setLabel(note.noteId, newName, attr.value);
        } else {
            await setRelation(note.noteId, newName, attr.value);
        }
        await removeAttributeById(note.noteId, attr.attributeId);
    }
}

/**
 * Returns the name of an attribute without the `disabled:` prefix, or the same name if it's not disabled.
 * @param name the name of an attribute.
 * @returns the name without the `disabled:` prefix.
 */
function getNameWithoutDangerousPrefix(name: string) {
    return name.startsWith("disabled:") ? name.substring(9) : name;
}

export default {
    addLabel,
    setLabel,
    setRelation,
    setAttribute,
    setBooleanWithInheritance,
    removeAttributeById,
    removeOwnedLabelByName,
    removeOwnedRelationByName,
    isAffecting,
    toggleDangerousAttribute,
    getNameWithoutDangerousPrefix
};
