const API_VERSION = 1;
const ENTITY_TYPES = new Set(["npc", "ship", "faction", "location"]);
const JOURNAL_MODES = new Set(["append-section", "prepend-section", "replace-section", "replace-page"]);
const FIELD_OPERATIONS = new Set(["add", "update", "rename", "remove", "reorder"]);
const MAX_JOURNAL_HTML_LENGTH = 250_000;

function clone(value) {
    return foundry.utils.deepClone(value);
}

function text(value) {
    return String(value ?? "").trim();
}

function assertGm() {
    if (!game.user?.isGM) throw new Error("Only a GM can change Nexus dossier content.");
}

function assertEntityType(entityType) {
    const type = text(entityType).toLocaleLowerCase();
    if (!ENTITY_TYPES.has(type)) throw new Error(`Unsupported Nexus entity type: ${type || "missing"}.`);
    return type;
}

function canonical(value) {
    if (Array.isArray(value)) return value.map(canonical);
    if (!value || typeof value !== "object") return value;
    return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
}

function fingerprint(value) {
    const source = JSON.stringify(canonical(value));
    let hash = 0x811c9dc5;
    for (let index = 0; index < source.length; index++) {
        hash ^= source.charCodeAt(index);
        hash = Math.imul(hash, 0x01000193);
    }
    return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}:${source.length}`;
}

function slug(value) {
    return text(value)
        .toLocaleLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-+|-+$/g, "") || "section";
}

function sectionMarker(sectionTitle) {
    return `nexus-section-${slug(sectionTitle)}`;
}

function sanitizeHtml(rawHtml) {
    const raw = String(rawHtml ?? "");
    if (raw.length > MAX_JOURNAL_HTML_LENGTH) throw new Error(`Journal HTML exceeds the ${MAX_JOURNAL_HTML_LENGTH}-character limit.`);
    if (/<\/?(?:script|iframe|object|embed|form|input|button|style|link|meta)\b|\son[a-z]+\s*=|javascript\s*:/i.test(raw)) {
        throw new Error("Journal HTML contains executable or unsupported markup.");
    }
    const cleaned = foundry.utils.cleanHTML(raw).trim();
    if (raw.trim() && !cleaned) throw new Error("Journal HTML contained no supported Foundry content.");
    return cleaned;
}

function buildManagedSection(sectionTitle, bodyHtml) {
    const title = text(sectionTitle);
    if (!title) throw new Error("Journal section operations require a section title.");
    if (/<\/?section\b/i.test(bodyHtml)) throw new Error("Journal section bodies cannot contain nested section elements.");
    const escapedTitle = foundry.utils.escapeHTML(title);
    return `<section id="${sectionMarker(title)}" data-nexus-section="${slug(title)}"><h2>${escapedTitle}</h2>${bodyHtml}</section>`;
}

function composeJournalHtml(currentHtml, { mode, sectionTitle = "", html = "" } = {}) {
    const operation = text(mode);
    if (!JOURNAL_MODES.has(operation)) throw new Error(`Unsupported Journal operation: ${operation || "missing"}.`);
    const existing = String(currentHtml || "").trim();
    const cleaned = sanitizeHtml(html);
    if (operation === "replace-page") return cleaned;

    const section = buildManagedSection(sectionTitle, cleaned);
    if (operation === "append-section") return [existing, section].filter(Boolean).join("\n");
    if (operation === "prepend-section") return [section, existing].filter(Boolean).join("\n");

    const marker = sectionMarker(sectionTitle);
    const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const pattern = new RegExp(`<section\\b[^>]*\\bid=["']${escaped}["'][^>]*>[\\s\\S]*?<\\/section>`, "i");
    if (!pattern.test(existing)) throw new Error(`The Journal does not contain the managed section '${text(sectionTitle)}'. Append it before attempting a section replacement.`);
    return existing.replace(pattern, section);
}

function entityProjection(entity) {
    const keys = ["id", "type", "uuid", "schemaVersion", "sourceModule", "createdTime", "updatedTime", "display", "identity", "role", "personality", "ideology", "flavor", "profile", "currentThread", "hierarchy", "scenes", "access", "dossier", "tags", "generation"];
    return Object.fromEntries(keys.filter(key => Object.hasOwn(entity || {}, key)).map(key => [key, clone(entity[key])]));
}

function fieldCollection(entityType, entity) {
    const type = assertEntityType(entityType);
    const key = type === "location" ? "fields" : "customFields";
    return { key, fields: clone(Array.isArray(entity?.profile?.[key]) ? entity.profile[key] : []) };
}

function normalizeFieldId(value, label) {
    const raw = text(value || label)
        .toLocaleLowerCase()
        .replace(/[^a-z0-9.:-]+/g, "-")
        .replace(/^-+|-+$/g, "");
    return raw.startsWith("field.") ? raw : `field.${raw || foundry.utils.randomID()}`;
}

function planFieldOperations(entityType, currentFields, operations = []) {
    if (!Array.isArray(operations) || !operations.length) throw new Error("A custom-field plan requires at least one operation.");
    const type = assertEntityType(entityType);
    const next = clone(currentFields);
    const effects = [];

    for (const [index, requested] of operations.entries()) {
        const operation = text(requested?.operation);
        if (!FIELD_OPERATIONS.has(operation)) throw new Error(`Unsupported custom-field operation at index ${index}: ${operation || "missing"}.`);
        const fieldId = text(requested?.fieldId);
        const existingIndex = fieldId ? next.findIndex(field => field.id === fieldId) : -1;

        if (operation === "add") {
            const label = text(requested.label);
            if (!label) throw new Error(`Custom-field add operation ${index} requires a label.`);
            const id = normalizeFieldId(fieldId, label);
            if (next.some(field => field.id === id)) throw new Error(`Custom field already exists: ${id}.`);
            const field = { id, label, value: text(requested.value) };
            if (type === "location") field.type = requested.type === "multiline" ? "multiline" : "text";
            const at = Number.isInteger(requested.index) ? Math.max(0, Math.min(next.length, requested.index)) : next.length;
            next.splice(at, 0, field);
            effects.push({ operation, fieldId: id, label, index: at });
            continue;
        }

        if (existingIndex < 0) throw new Error(`Custom field could not be found: ${fieldId || "missing field ID"}.`);
        const current = next[existingIndex];
        if (operation === "update") {
            current.value = text(requested.value);
            if (type === "location" && requested.type) current.type = requested.type === "multiline" ? "multiline" : "text";
            effects.push({ operation, fieldId, label: current.label });
        } else if (operation === "rename") {
            const label = text(requested.label);
            if (!label) throw new Error(`Custom-field rename operation ${index} requires a label.`);
            effects.push({ operation, fieldId, from: current.label, to: label });
            current.label = label;
        } else if (operation === "remove") {
            next.splice(existingIndex, 1);
            effects.push({ operation, fieldId, label: current.label });
        } else if (operation === "reorder") {
            if (!Number.isInteger(requested.index)) throw new Error(`Custom-field reorder operation ${index} requires an integer index.`);
            const [field] = next.splice(existingIndex, 1);
            const at = Math.max(0, Math.min(next.length, requested.index));
            next.splice(at, 0, field);
            effects.push({ operation, fieldId, from: existingIndex, to: at });
        }
    }
    return { fields: next, effects };
}

async function entityApi(entityType) {
    const type = assertEntityType(entityType);
    if (type === "npc") {
        const api = await import("./npcs.js");
        return { get: api.getNpc, update: api.updateNpc };
    }
    if (type === "ship") {
        const api = await import("./ships.js");
        return { get: api.getShip, update: api.updateShip };
    }
    if (type === "faction") {
        const api = await import("./factions.js");
        return { get: api.getFaction, update: api.updateFaction };
    }
    const api = await import("./locations.js");
    return { get: api.getLocation, update: api.updateLocation };
}

async function resolveEntity(entityType, entityId) {
    const type = assertEntityType(entityType);
    const id = text(entityId);
    const api = await entityApi(type);
    const entity = id ? api.get(id) : null;
    if (!entity || entity.type !== type) throw new Error(`Nexus ${type} could not be found: ${id || "missing ID"}.`);
    return { type, id, api, entity };
}

async function journalStore(entityType) {
    const type = assertEntityType(entityType);
    if (type === "location") {
        const { LocationEntityStore } = await import("../features/campaign-entities/services/LocationEntityStore.js");
        return { get: id => LocationEntityStore.getJournalPage(id), getOrCreate: id => LocationEntityStore.getOrCreateJournalPage(id) };
    }
    const { CampaignEntityJournalStore } = await import("../features/campaign-entities/services/CampaignEntityJournalStore.js");
    return { get: id => CampaignEntityJournalStore.getJournalPage(id), getOrCreate: id => CampaignEntityJournalStore.getOrCreateJournalPage(id) };
}

function planId(prefix) {
    return `${prefix}.${foundry.utils.randomID()}`;
}

export async function inspectEntityContent(entityType, entityId) {
    const resolved = await resolveEntity(entityType, entityId);
    const { key, fields } = fieldCollection(resolved.type, resolved.entity);
    const store = await journalStore(resolved.type);
    const page = store.get(resolved.id);
    const html = String(page?.text?.content || "");
    return {
        apiVersion: API_VERSION,
        entity: entityProjection(resolved.entity),
        customFields: { key, fields, fingerprint: fingerprint({ updatedTime: resolved.entity.updatedTime, fields }) },
        journal: {
            exists: !!page,
            pageId: String(page?.id || ""),
            pageUuid: String(page?.uuid || ""),
            html,
            fingerprint: fingerprint({ pageId: String(page?.id || ""), html })
        }
    };
}

export async function planEntityCustomFieldChanges({ entityType, entityId, operations = [] } = {}) {
    assertGm();
    const inspection = await inspectEntityContent(entityType, entityId);
    const planned = planFieldOperations(inspection.entity.type, inspection.customFields.fields, operations);
    return {
        apiVersion: API_VERSION,
        planId: planId("nexus-fields"),
        kind: "entity-custom-fields",
        operation: "update",
        target: { entityType: inspection.entity.type, entityId: inspection.entity.id, name: inspection.entity.display?.name || inspection.entity.id },
        sourceFingerprint: inspection.customFields.fingerprint,
        summary: `${planned.effects.length} custom-field change${planned.effects.length === 1 ? "" : "s"} for ${inspection.entity.display?.name || inspection.entity.id}`,
        effects: planned.effects,
        payload: { fieldKey: inspection.customFields.key, fields: planned.fields }
    };
}

export async function applyEntityCustomFieldPlan(plan = {}) {
    assertGm();
    if (plan?.apiVersion !== API_VERSION || plan?.kind !== "entity-custom-fields") throw new Error("Unsupported Nexus custom-field plan.");
    const inspection = await inspectEntityContent(plan.target?.entityType, plan.target?.entityId);
    if (inspection.customFields.fingerprint !== plan.sourceFingerprint) throw new Error("The Nexus profile changed after this custom-field plan was prepared. Refresh the plan before applying it.");
    if (plan.payload?.fieldKey !== inspection.customFields.key || !Array.isArray(plan.payload?.fields)) throw new Error("The custom-field plan payload is invalid for this entity type.");
    const resolved = await resolveEntity(plan.target.entityType, plan.target.entityId);
    const entity = await resolved.api.update(resolved.id, { profile: { [inspection.customFields.key]: clone(plan.payload.fields) } });
    return { ok: true, planId: plan.planId, entityType: resolved.type, entityId: resolved.id, name: entity?.display?.name || plan.target.name, fieldCount: plan.payload.fields.length };
}

export async function planEntityJournalWrite({ entityType, entityId, mode = "append-section", sectionTitle = "", html = "" } = {}) {
    assertGm();
    const inspection = await inspectEntityContent(entityType, entityId);
    const nextHtml = composeJournalHtml(inspection.journal.html, { mode, sectionTitle, html });
    const operation = text(mode);
    return {
        apiVersion: API_VERSION,
        planId: planId("nexus-journal"),
        kind: "entity-journal",
        operation,
        target: { entityType: inspection.entity.type, entityId: inspection.entity.id, name: inspection.entity.display?.name || inspection.entity.id, pageId: inspection.journal.pageId },
        sourceFingerprint: inspection.journal.fingerprint,
        summary: `${operation.replaceAll("-", " ")} for ${inspection.entity.display?.name || inspection.entity.id}`,
        effects: [{ operation, sectionTitle: text(sectionTitle), createsPage: !inspection.journal.exists, replacesPopulatedPage: operation === "replace-page" && !!inspection.journal.html.trim() }],
        payload: { html: nextHtml, format: 1 }
    };
}

export async function applyEntityJournalPlan(plan = {}) {
    assertGm();
    if (plan?.apiVersion !== API_VERSION || plan?.kind !== "entity-journal" || !JOURNAL_MODES.has(plan.operation)) throw new Error("Unsupported Nexus Journal plan.");
    const inspection = await inspectEntityContent(plan.target?.entityType, plan.target?.entityId);
    if (inspection.journal.fingerprint !== plan.sourceFingerprint) throw new Error("The Nexus Journal changed after this plan was prepared. Refresh the plan before applying it.");
    const safeHtml = sanitizeHtml(plan.payload?.html);
    const store = await journalStore(inspection.entity.type);
    const page = await store.getOrCreate(inspection.entity.id);
    if (!page?.id) throw new Error("Nexus could not create or resolve the writable dossier Journal page.");
    await page.update({ text: { content: safeHtml, format: 1 } });
    return { ok: true, planId: plan.planId, entityType: inspection.entity.type, entityId: inspection.entity.id, name: inspection.entity.display?.name || inspection.entity.id, pageId: String(page.id), pageUuid: String(page.uuid || ""), fingerprint: fingerprint({ pageId: String(page.id), html: safeHtml }) };
}

export function getEntityContentCapability() {
    return {
        apiVersion: API_VERSION,
        id: "augur-nexus.entity-content",
        subjects: [...ENTITY_TYPES],
        operations: {
            inspect: ["entity-content"],
            plan: ["custom-fields", ...JOURNAL_MODES],
            apply: ["custom-fields", "journal"]
        },
        journal: { modes: [...JOURNAL_MODES], maxHtmlLength: MAX_JOURNAL_HTML_LENGTH, format: 1 },
        customFields: { operations: [...FIELD_OPERATIONS] }
    };
}

export const __entityContentTestUtils = Object.freeze({ fingerprint, composeJournalHtml, planFieldOperations, sectionMarker });
