import { ConnectionGraphModel } from "../models/ConnectionGraphModel.js";
import { CampaignEntityFolderManager } from "../../campaign-entities/services/CampaignEntityFolderManager.js";

const MODULE_ID = "augur-nexus";
const LEGACY_SETTING_KEY = "connectionsGraph";
const POINTER_SETTING_KEY = "connectionDatabaseJournalEntryId";
const DATABASE_FLAG = "connectionDatabase";
const DATABASE_NAME = "Augur Connections Database";
const SCHEMA_VERSION = 1;

export class ConnectionDatabaseService {
    static #resolvedDocument = null;
    static #resolutionComplete = false;
    static #stalePointerWarned = false;

    static async initialize() {
        this.invalidate();

        const pointerId = this.#getPointerId();
        if (pointerId) {
            const pointedDocument = game.journal?.get(pointerId) || null;
            if (this.isDatabaseDocument(pointedDocument)) {
                this.#useDocument(pointedDocument);
                return pointedDocument;
            }
            this.#warnStalePointer(pointerId);
            this.#resolutionComplete = true;
            return null;
        }

        const candidates = this.#findDatabaseDocuments();
        if (candidates.length > 1) {
            console.error(`${MODULE_ID} | Multiple connection database Journals were found; Nexus will not guess which one is authoritative.`);
            ui.notifications?.error?.("Multiple Augur Connections Databases were found. Connections are read-only until the duplicate is resolved.");
            this.#resolutionComplete = true;
            return null;
        }

        const candidate = candidates[0] || null;
        if (!this.#isResponsibleActiveGm()) {
            if (candidate) this.#useDocument(candidate);
            else this.#resolutionComplete = true;
            return candidate;
        }

        try {
            if (candidate) {
                await this.#adoptExistingDatabase(candidate);
                return candidate;
            }
            return await this.#createAndMigrateDatabase();
        } catch (error) {
            console.error(`${MODULE_ID} | Failed to initialize the connection database. The legacy connection graph was left untouched.`, error);
            ui.notifications?.error?.("Augur Connections Database initialization failed. Existing connection data was not changed.");
            this.invalidate();
            this.#resolutionComplete = true;
            return null;
        }
    }

    static invalidate() {
        this.#resolvedDocument = null;
        this.#resolutionComplete = false;
    }

    static getDocument() {
        if (this.#resolvedDocument && game.journal?.get(this.#resolvedDocument.id) === this.#resolvedDocument) {
            return this.#resolvedDocument;
        }
        if (this.#resolutionComplete) return null;

        const pointerId = this.#getPointerId();
        if (pointerId) {
            const document = game.journal?.get(pointerId) || null;
            if (this.isDatabaseDocument(document)) {
                this.#useDocument(document);
                return document;
            }
            this.#warnStalePointer(pointerId);
            this.#resolutionComplete = true;
            return null;
        }

        const candidates = this.#findDatabaseDocuments();
        if (candidates.length === 1) {
            this.#useDocument(candidates[0]);
            return candidates[0];
        }
        this.#resolutionComplete = true;
        return null;
    }

    static getGraph() {
        const document = this.getDocument();
        if (document) return this.#databaseToGraph(document.getFlag(MODULE_ID, DATABASE_FLAG));

        // The setting is a migration source and a safe read-only fallback if initialization fails.
        return game.settings.get(MODULE_ID, LEGACY_SETTING_KEY) || this.#emptyGraph();
    }

    static getChangeMetadata(document = this.getDocument()) {
        if (document) {
            const database = this.#normalizeDatabase(document.getFlag(MODULE_ID, DATABASE_FLAG));
            return {
                revision: database.revision,
                updateId: database.updateId
            };
        }
        const legacy = game.settings.get(MODULE_ID, LEGACY_SETTING_KEY) || {};
        return {
            updatedTime: Number(legacy.updatedTime || 0),
            updateId: String(legacy.updateId || "")
        };
    }

    static async applyChanges({
        nodeUpserts = new Map(),
        nodeDeletes = new Set(),
        categoryUpserts = new Map(),
        categoryDeletes = new Set(),
        edgeUpserts = new Map(),
        edgeDeletes = new Set()
    } = {}, { reason = "update" } = {}) {
        const document = this.getDocument();
        if (!document) {
            throw new Error("The Augur Connections Database is unavailable. Existing legacy data remains read-only.");
        }
        if (!game.user?.isGM) throw new Error("Only a GM can update Nexus connections.");

        const current = this.#normalizeDatabase(document.getFlag(MODULE_ID, DATABASE_FLAG));
        const revision = current.revision + 1;
        const updateId = foundry.utils.randomID();
        const update = {
            [`flags.${MODULE_ID}.${DATABASE_FLAG}.revision`]: revision,
            [`flags.${MODULE_ID}.${DATABASE_FLAG}.updateId`]: updateId
        };

        // Records are complete snapshots; force replacement so normalized-away fields do not survive flag merges.
        for (const [nodeId, node] of nodeUpserts) {
            update[`flags.${MODULE_ID}.${DATABASE_FLAG}.nodes.==${this.#encodeKey(nodeId)}`] = node;
        }
        for (const nodeId of nodeDeletes) {
            update[`flags.${MODULE_ID}.${DATABASE_FLAG}.nodes.-=${this.#encodeKey(nodeId)}`] = null;
        }
        for (const [categoryId, category] of categoryUpserts) {
            update[`flags.${MODULE_ID}.${DATABASE_FLAG}.customCategories.==${this.#encodeKey(categoryId)}`] = category;
        }
        for (const categoryId of categoryDeletes) {
            update[`flags.${MODULE_ID}.${DATABASE_FLAG}.customCategories.-=${this.#encodeKey(categoryId)}`] = null;
        }

        for (const [edgeId, edge] of edgeUpserts) {
            update[`flags.${MODULE_ID}.${DATABASE_FLAG}.edges.==${this.#encodeKey(edgeId)}`] = this.#encodeEdge(edge);
        }
        for (const edgeId of edgeDeletes) {
            update[`flags.${MODULE_ID}.${DATABASE_FLAG}.edges.-=${this.#encodeKey(edgeId)}`] = null;
        }

        await document.update(update, {
            [MODULE_ID]: {
                connectionDatabase: true,
                reason
            }
        });
        return { revision, updateId };
    }

    static isDatabaseDocument(document) {
        const database = document?.getFlag?.(MODULE_ID, DATABASE_FLAG);
        return !!database
            && Number(database.schemaVersion) === SCHEMA_VERSION
            && database.customCategories && typeof database.customCategories === "object"
            && database.nodes && typeof database.nodes === "object"
            && database.edges && typeof database.edges === "object";
    }

    static isDeletionAllowed(options = {}) {
        return options?.[MODULE_ID]?.allowConnectionDatabaseDeletion === true;
    }

    static async #adoptExistingDatabase(document) {
        const legacyGraph = ConnectionGraphModel.normalizeGraph(game.settings.get(MODULE_ID, LEGACY_SETTING_KEY) || {});
        const databaseGraph = ConnectionGraphModel.normalizeGraph(
            this.#databaseToGraph(document.getFlag(MODULE_ID, DATABASE_FLAG))
        );
        if (this.#hasContent(legacyGraph) && !this.#graphsEqual(legacyGraph, databaseGraph)) {
            throw new Error("An unclaimed connection database does not match the legacy graph.");
        }

        await game.settings.set(MODULE_ID, POINTER_SETTING_KEY, document.id);
        this.#useDocument(document);
        if (this.#hasContent(legacyGraph)) await this.#clearLegacyGraphAfterMigration();
    }

    static async #createAndMigrateDatabase() {
        const legacyGraph = ConnectionGraphModel.normalizeGraph(game.settings.get(MODULE_ID, LEGACY_SETTING_KEY) || {});
        const folder = await CampaignEntityFolderManager.getOrCreateSystemDataFolder();
        const database = this.#graphToDatabase(legacyGraph);
        let document = null;

        try {
            document = await JournalEntry.create({
                name: DATABASE_NAME,
                folder: folder?.id || null,
                ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE },
                flags: {
                    [MODULE_ID]: {
                        [DATABASE_FLAG]: database
                    }
                }
            }, {
                [MODULE_ID]: {
                    connectionDatabase: true,
                    initializing: true
                }
            });
            if (!document) throw new Error("Foundry did not create the connection database Journal.");
            await this.#createWarningPage(document);

            const readBack = game.journal?.get(document.id) || null;
            if (!this.isDatabaseDocument(readBack)) throw new Error("The created connection database could not be read back.");
            const migratedGraph = ConnectionGraphModel.normalizeGraph(
                this.#databaseToGraph(readBack.getFlag(MODULE_ID, DATABASE_FLAG))
            );
            if (!this.#graphsEqual(legacyGraph, migratedGraph)) {
                throw new Error("Connection database verification did not match the legacy graph.");
            }

            await game.settings.set(MODULE_ID, POINTER_SETTING_KEY, readBack.id);
            this.#useDocument(readBack);
            await this.#clearLegacyGraphAfterMigration();
            return readBack;
        } catch (error) {
            if (document && !this.#getPointerId()) {
                try {
                    await document.delete({
                        [MODULE_ID]: {
                            allowConnectionDatabaseDeletion: true,
                            failedInitialization: true
                        }
                    });
                } catch (cleanupError) {
                    console.warn(`${MODULE_ID} | Failed to remove an unverified connection database Journal.`, cleanupError);
                }
            }
            throw error;
        }
    }

    static async #clearLegacyGraph() {
        await game.settings.set(MODULE_ID, LEGACY_SETTING_KEY, this.#emptyGraph());
    }

    static async #createWarningPage(document) {
        const [page] = await document.createEmbeddedDocuments("JournalEntryPage", [{
            name: "Managed Nexus Data — Do Not Delete",
            type: "text",
            sort: CONST.SORT_INTEGER_DENSITY,
            text: {
                format: 1,
                content: `
                    <h1>Managed Nexus Data</h1>
                    <p><strong>Do not delete or manually edit this Journal.</strong></p>
                    <p>This document is the authoritative database for Nexus connections in this world. Nexus maintains it automatically.</p>
                    <p>Deleting or changing its stored data can break connections between scenes, sites, locations, people, organizations, ships, shops, and other linked content. Use the Nexus connection tools to create, edit, or remove connections.</p>
                `
            }
        }]);
        if (!page) throw new Error("The connection database warning page could not be created.");
    }

    static async #clearLegacyGraphAfterMigration() {
        try {
            await this.#clearLegacyGraph();
        } catch (error) {
            // The verified Journal and pointer are already authoritative; retaining the inert source is safer than rollback.
            console.warn(`${MODULE_ID} | The legacy connection setting could not be cleared after migration.`, error);
        }
    }

    static #graphToDatabase(graph = {}) {
        const normalized = ConnectionGraphModel.normalizeGraph(graph);
        return {
            schemaVersion: SCHEMA_VERSION,
            revision: 1,
            updateId: foundry.utils.randomID(),
            customCategories: this.#encodeRecordMap(normalized.customCategories),
            nodes: this.#encodeRecordMap(normalized.nodes),
            edges: this.#encodeEdgeMap(normalized.edges)
        };
    }

    static #databaseToGraph(database = {}) {
        const normalized = this.#normalizeDatabase(database);
        return {
            version: normalized.schemaVersion,
            schemaVersion: normalized.schemaVersion,
            revision: normalized.revision,
            updateId: normalized.updateId,
            customCategories: this.#decodeRecordMap(normalized.customCategories),
            nodes: this.#decodeRecordMap(normalized.nodes),
            edges: this.#decodeEdgeMap(normalized.edges)
        };
    }

    static #normalizeDatabase(database = {}) {
        return {
            schemaVersion: Number(database?.schemaVersion || SCHEMA_VERSION),
            revision: Number(database?.revision || 0),
            updateId: String(database?.updateId || ""),
            customCategories: database?.customCategories && typeof database.customCategories === "object"
                ? database.customCategories
                : {},
            nodes: database?.nodes && typeof database.nodes === "object" ? database.nodes : {},
            edges: database?.edges && typeof database.edges === "object" ? database.edges : {}
        };
    }

    static #encodeRecordMap(records = {}) {
        return Object.fromEntries(
            Object.entries(records || {}).map(([id, value]) => [this.#encodeKey(id), value])
        );
    }

    static #decodeRecordMap(records = {}) {
        const decoded = {};
        for (const [storedId, value] of Object.entries(records || {})) {
            decoded[this.#decodeKey(storedId)] = value;
        }
        return decoded;
    }

    static #encodeEdgeMap(edges = {}) {
        return Object.fromEntries(
            Object.entries(edges || {}).map(([id, edge]) => [this.#encodeKey(id), this.#encodeEdge(edge)])
        );
    }

    static #decodeEdgeMap(edges = {}) {
        return Object.fromEntries(
            Object.entries(edges || {}).map(([storedId, edge]) => [this.#decodeKey(storedId), this.#decodeEdge(edge)])
        );
    }

    static #encodeEdge(edge = {}) {
        const encoded = { ...edge };
        if (edge?.views) encoded.views = this.#encodeRecordMap(edge.views);
        if (edge?.opinions?.byObserver) {
            encoded.opinions = {
                ...edge.opinions,
                byObserver: this.#encodeRecordMap(edge.opinions.byObserver)
            };
        }
        return encoded;
    }

    static #decodeEdge(edge = {}) {
        const decoded = { ...edge };
        if (edge?.views) decoded.views = this.#decodeRecordMap(edge.views);
        if (edge?.opinions?.byObserver) {
            decoded.opinions = {
                ...edge.opinions,
                byObserver: this.#decodeRecordMap(edge.opinions.byObserver)
            };
        }
        return decoded;
    }

    static #encodeKey(value) {
        return encodeURIComponent(String(value || "")).replaceAll(".", "%2E");
    }

    static #decodeKey(value) {
        try {
            return decodeURIComponent(String(value || ""));
        } catch {
            return String(value || "");
        }
    }

    static #findDatabaseDocuments() {
        return (game.journal?.contents || []).filter(document => this.isDatabaseDocument(document));
    }

    static #getPointerId() {
        return String(game.settings.get(MODULE_ID, POINTER_SETTING_KEY) || "").trim();
    }

    static #useDocument(document) {
        this.#resolvedDocument = document;
        this.#resolutionComplete = true;
        this.#stalePointerWarned = false;
    }

    static #warnStalePointer(pointerId) {
        if (this.#stalePointerWarned) return;
        this.#stalePointerWarned = true;
        console.error(`${MODULE_ID} | Connection database pointer ${pointerId} does not resolve to a valid database Journal.`);
        ui.notifications?.error?.("The Augur Connections Database is missing. Nexus will not create an empty replacement automatically.");
    }

    static #isResponsibleActiveGm() {
        if (!game.user?.isGM) return false;
        const activeGm = game.users?.activeGM
            || game.users?.find?.(user => user.active && user.isGM)
            || null;
        return !activeGm || activeGm.id === game.user.id;
    }

    static #hasContent(graph = {}) {
        return !!(
            Object.keys(graph?.customCategories || {}).length
            || Object.keys(graph?.nodes || {}).length
            || Object.keys(graph?.edges || {}).length
        );
    }

    static #graphsEqual(left = {}, right = {}) {
        const normalize = graph => {
            const normalized = ConnectionGraphModel.normalizeGraph(graph);
            return {
                customCategories: normalized.customCategories,
                nodes: normalized.nodes,
                edges: normalized.edges
            };
        };
        return this.#stableSerialize(normalize(left)) === this.#stableSerialize(normalize(right));
    }

    static #stableSerialize(value, key = "") {
        if (["updatedTime", "updateId", "revision", "schemaVersion", "version"].includes(key)) return "";
        if (Array.isArray(value)) return `[${value.map(entry => this.#stableSerialize(entry)).join(",")}]`;
        if (value && typeof value === "object") {
            return `{${Object.keys(value)
                .filter(entryKey => !["updatedTime", "updateId", "revision", "schemaVersion", "version"].includes(entryKey))
                .sort()
                .map(entryKey => `${JSON.stringify(entryKey)}:${this.#stableSerialize(value[entryKey], entryKey)}`)
                .join(",")}}`;
        }
        return JSON.stringify(value);
    }

    static #emptyGraph() {
        return {
            version: 1,
            customCategories: {},
            nodes: {},
            edges: {}
        };
    }
}
