export class ChronicleNarrativeModel {
    static create(text = "", participants = []) {
        const source = String(text || "");
        if (!source) return [];

        const references = (participants || [])
            .map(participant => ({
                entityId: String(participant?.entityId || participant?.id || "").trim(),
                fallback: String(participant?.name || "").trim()
            }))
            .filter(reference => reference.entityId && reference.fallback)
            .sort((left, right) => right.fallback.length - left.fallback.length);
        if (!references.length) return [{ type: "text", text: source }];

        const segments = [];
        let cursor = 0;
        while (cursor < source.length) {
            const match = this.#nextReference(source, cursor, references);
            if (!match) {
                this.#appendText(segments, source.slice(cursor));
                break;
            }
            this.#appendText(segments, source.slice(cursor, match.index));
            segments.push({
                type: "entity",
                entityId: match.reference.entityId,
                fallback: match.reference.fallback
            });
            cursor = match.index + match.reference.fallback.length;
        }
        return segments;
    }

    static normalize(narrative = []) {
        const segments = [];
        for (const segment of Array.isArray(narrative) ? narrative : []) {
            if (segment?.type === "entity") {
                const entityId = String(segment.entityId || "").trim();
                const fallback = String(segment.fallback || "").trim();
                if (entityId && fallback) segments.push({ type: "entity", entityId, fallback });
                continue;
            }
            this.#appendText(segments, String(segment?.text || ""));
        }
        return segments;
    }

    static #nextReference(source, cursor, references) {
        let next = null;
        for (const reference of references) {
            const index = this.#findReference(source, reference.fallback, cursor);
            if (index < 0) continue;
            if (!next || index < next.index || (index === next.index && reference.fallback.length > next.reference.fallback.length)) {
                next = { index, reference };
            }
        }
        return next;
    }

    static #findReference(source, name, cursor) {
        let index = source.indexOf(name, cursor);
        while (index >= 0) {
            const before = source[index - 1] || "";
            const after = source[index + name.length] || "";
            const beginsWithWord = /[\p{L}\p{N}]/u.test(name[0] || "");
            const endsWithWord = /[\p{L}\p{N}]/u.test(name.at(-1) || "");
            const validStart = !beginsWithWord || !/[\p{L}\p{N}]/u.test(before);
            const validEnd = !endsWithWord || !/[\p{L}\p{N}]/u.test(after);
            if (validStart && validEnd) return index;
            index = source.indexOf(name, index + 1);
        }
        return -1;
    }

    static #appendText(segments, text) {
        if (!text) return;
        const previous = segments.at(-1);
        if (previous?.type === "text") previous.text += text;
        else segments.push({ type: "text", text });
    }
}
