import { LocationEntityStore } from "../../campaign-entities/services/LocationEntityStore.js";
import { ConnectionStore } from "../../connections/services/ConnectionStore.js";
import { NexusLineageManager } from "../../nexus/services/NexusLineageManager.js";
import { NexusSceneFolderManager } from "../../nexus/services/NexusSceneFolderManager.js";
import { SiteRecordManager } from "./SiteRecordManager.js";

const MODULE_ID = "augur-nexus";
const SITES_FLAG = "sites";

/**
 * Transplants a modern Site record between parent Scenes without moving its
 * physical markers. This is the only place that should coordinate the Site's
 * composite identity change.
 */
export class SiteReparentService {
    static #activeSites = new Set();

    static inspect({ parentSceneId = null, siteId = null, newParentSceneId = null } = {}) {
        return this.#toReport(this.#buildPlan({ parentSceneId, siteId, newParentSceneId }));
    }

    static async reparent({
        parentSceneId = null,
        siteId = null,
        newParentSceneId = null,
        dryRun = false
    } = {}) {
        const plan = this.#buildPlan({ parentSceneId, siteId, newParentSceneId });
        const report = this.#toReport(plan);
        if (dryRun) return report;
        if (!game.user?.isGM) throw new Error("Only a GM can reparent a Site.");
        if (!plan.valid) throw new Error(`Site cannot be reparented: ${plan.errors.join(" ")}`);

        const operationKey = `${parentSceneId}:${siteId}`;
        if (this.#activeSites.has(operationKey)) throw new Error("This Site is already being reparented.");
        this.#activeSites.add(operationKey);

        const snapshot = this.#captureSnapshot(plan);
        try {
            await this.#apply(plan);
            this.#verify(plan);
            this.#notifyChanged("site-reparented", plan);
            return {
                ...this.#toReport(plan),
                completed: true,
                newTarget: {
                    kind: "nexus-site",
                    parentSceneId: plan.newParentScene.id,
                    siteId: plan.record.siteId
                }
            };
        } catch (error) {
            let rollbackError = null;
            try {
                await this.#rollback(snapshot);
                this.#notifyChanged("site-reparent-rollback", plan);
            } catch (caught) {
                rollbackError = caught;
                console.error("Augur: Nexus | Site reparent rollback failed.", caught);
            }

            if (rollbackError) {
                throw new AggregateError(
                    [error, rollbackError],
                    `Site reparenting failed and rollback was incomplete: ${error.message}`
                );
            }
            throw new Error(`Site reparenting failed and was rolled back: ${error.message}`, { cause: error });
        } finally {
            this.#activeSites.delete(operationKey);
        }
    }

    static #buildPlan({ parentSceneId = null, siteId = null, newParentSceneId = null } = {}) {
        const errors = [];
        const warnings = [];
        const sourceParentScene = parentSceneId ? game.scenes.get(parentSceneId) || null : null;
        const newParentScene = newParentSceneId ? game.scenes.get(newParentSceneId) || null : null;

        if (!sourceParentScene) errors.push("The current parent Scene could not be found.");
        if (!newParentScene) errors.push("The destination parent Scene could not be found.");
        if (!siteId) errors.push("No Site id was provided.");
        if (sourceParentScene && newParentScene && sourceParentScene.id === newParentScene.id) {
            errors.push("The Site already belongs to that Scene.");
        }

        const record = sourceParentScene && siteId
            ? SiteRecordManager.getSceneRecord(sourceParentScene, siteId)
            : null;
        if (!record) errors.push("The source Scene does not contain a modern Site record with that id.");
        if (record?.legacyJournalBacked) errors.push("Legacy journal-backed Sites cannot be reparented.");

        const destinationRecord = newParentScene && siteId
            ? SiteRecordManager.getSceneRecord(newParentScene, siteId)
            : null;
        if (destinationRecord) errors.push("The destination Scene already contains a Site with that id.");

        const linkedSceneId = record?.siteSceneId || record?.linkedSceneId || null;
        const linkedScene = linkedSceneId ? game.scenes.get(linkedSceneId) || null : null;
        if (linkedSceneId && !linkedScene) errors.push("The Site declares a linked Scene that no longer exists.");
        if (linkedScene && newParentScene) {
            const reparentValidation = NexusLineageManager.validateUserSceneReparent(linkedScene);
            if (!reparentValidation.valid) {
                errors.push(reparentValidation.message || "The linked Scene cannot be moved.");
            }
            const validation = NexusLineageManager.validateSceneParent(linkedScene, {
                parentSceneId: newParentScene.id
            });
            if (!validation.valid) errors.push(validation.message || "The destination would create invalid Scene lineage.");
        }

        const legacyPlaceables = sourceParentScene && siteId
            ? this.#findLegacyPlaceables(sourceParentScene, siteId)
            : [];
        if (legacyPlaceables.length) {
            errors.push("The Site still uses legacy Site placeables. Convert them to managed markers before reparenting.");
        }

        const markers = record
            ? this.#collectManagedMarkers({
                parentSceneId: sourceParentScene?.id,
                siteId: record.siteId,
                markerRefs: record.markerRefs
            })
            : { entries: [], staleRefs: [], untrackedMarkerCount: 0 };
        if (markers.staleRefs.length) warnings.push(`${markers.staleRefs.length} stale marker reference(s) will be discarded.`);
        if (markers.untrackedMarkerCount) {
            warnings.push(`${markers.untrackedMarkerCount} managed marker(s) missing from markerRefs will be recovered.`);
        }

        const childLocations = record
            ? LocationEntityStore.getLocations().filter(location => {
                const parent = location.hierarchy?.parent || null;
                return parent?.kind === "site"
                    && parent.parentSceneId === sourceParentScene?.id
                    && parent.siteId === record.siteId;
            }).map(location => ({
                entity: location,
                document: LocationEntityStore.resolveDocument(location.id)
            })).filter(entry => !!entry.document)
            : [];

        const connectionPlan = record && newParentScene
            ? this.#planConnectionMove(sourceParentScene.id, newParentScene.id, record.siteId)
            : { graph: null, oldNodeId: "", newNodeId: "", connectionCount: 0, conflict: false };
        if (connectionPlan.conflict) errors.push("Connections already contain a node for the destination Site identity.");

        const journalPage = record?.journalEntryId && record?.journalPageId
            ? game.journal.get(record.journalEntryId)?.pages.get(record.journalPageId) || null
            : null;
        if ((record?.journalEntryId || record?.journalPageId) && !journalPage) {
            warnings.push("The Site's optional Journal association is incomplete or missing.");
        }

        return {
            valid: errors.length === 0,
            errors,
            warnings,
            sourceParentScene,
            newParentScene,
            record,
            linkedScene,
            markers,
            legacyPlaceables,
            childLocations,
            connectionPlan,
            journalPage,
            nextRecord: null
        };
    }

    static #toReport(plan) {
        return {
            valid: plan.valid,
            errors: [...plan.errors],
            warnings: [...plan.warnings],
            source: plan.sourceParentScene ? {
                sceneId: plan.sourceParentScene.id,
                name: plan.sourceParentScene.name
            } : null,
            destination: plan.newParentScene ? {
                sceneId: plan.newParentScene.id,
                name: plan.newParentScene.name
            } : null,
            site: plan.record ? {
                siteId: plan.record.siteId,
                name: plan.record.siteName,
                linkedSceneId: plan.linkedScene?.id || null
            } : null,
            impact: {
                managedMarkers: plan.markers?.entries?.length || 0,
                staleMarkerRefs: plan.markers?.staleRefs?.length || 0,
                recoveredMarkers: plan.markers?.untrackedMarkerCount || 0,
                childLocations: plan.childLocations?.length || 0,
                connections: plan.connectionPlan?.connectionCount || 0,
                journalPage: !!plan.journalPage
            }
        };
    }

    static #findLegacyPlaceables(scene, siteId) {
        return [
            ...(scene?.tiles?.contents || []),
            ...(scene?.notes?.contents || [])
        ].filter(document => {
            const flags = document.flags?.[MODULE_ID] || {};
            return !!flags.site && flags.siteId === siteId && !flags.marker;
        });
    }

    static #collectManagedMarkers({ parentSceneId, siteId, markerRefs = [] } = {}) {
        const oldTarget = { kind: "nexus-site", parentSceneId, siteId };
        const entriesByMarkerId = new Map();
        let untrackedMarkerCount = 0;
        const trackedIds = new Set((markerRefs || []).map(ref => ref?.markerId).filter(Boolean));

        for (const scene of game.scenes.contents) {
            for (const tile of scene.tiles?.contents || []) {
                const marker = tile.flags?.[MODULE_ID]?.marker || null;
                if (!this.#targetMatches(marker?.target, oldTarget) || !marker?.markerId) continue;
                const label = this.#findMarkerLabel(scene, marker, tile);
                entriesByMarkerId.set(marker.markerId, { scene, tile, label, marker });
                if (!trackedIds.has(marker.markerId)) untrackedMarkerCount += 1;
            }
        }

        return {
            entries: [...entriesByMarkerId.values()],
            staleRefs: (markerRefs || []).filter(ref => !entriesByMarkerId.has(ref?.markerId)),
            untrackedMarkerCount
        };
    }

    static #findMarkerLabel(scene, marker, tile) {
        const labelId = marker?.label?.id || "";
        return (labelId ? scene.drawings?.get(labelId) || null : null)
            || scene.drawings?.contents?.find(drawing => {
                const flags = drawing.flags?.[MODULE_ID] || {};
                return flags.markerLabel === true
                    && (flags.markerId === marker?.markerId || flags.ownerTileId === tile?.id);
            })
            || null;
    }

    static #planConnectionMove(oldParentSceneId, newParentSceneId, siteId) {
        const graph = ConnectionStore.getGraph();
        const oldNodeId = `nexus-site:${oldParentSceneId}:${siteId}`;
        const newNodeId = `nexus-site:${newParentSceneId}:${siteId}`;
        const oldNode = graph.nodes?.[oldNodeId] || null;
        const conflict = !!graph.nodes?.[newNodeId];
        const connectionCount = oldNode
            ? Object.values(graph.edges || {}).filter(edge =>
                edge.sourceNodeId === oldNodeId || edge.targetNodeId === oldNodeId
            ).length
            : 0;
        return { graph, oldNodeId, newNodeId, oldNode, connectionCount, conflict };
    }

    static #captureSnapshot(plan) {
        return {
            sourceParentScene: plan.sourceParentScene,
            sourceSites: this.#clone(plan.sourceParentScene.getFlag(MODULE_ID, SITES_FLAG)),
            newParentScene: plan.newParentScene,
            destinationSites: this.#clone(plan.newParentScene.getFlag(MODULE_ID, SITES_FLAG)),
            linkedScene: plan.linkedScene,
            linkedSceneFlags: plan.linkedScene ? this.#clone(plan.linkedScene.flags?.[MODULE_ID]) : undefined,
            linkedSceneFolderId: plan.linkedScene ? (plan.linkedScene.folder?.id || plan.linkedScene.folder || null) : null,
            linkedSceneSort: plan.linkedScene?.sort ?? 0,
            markerDocuments: plan.markers.entries.map(entry => ({
                scene: entry.scene,
                tile: entry.tile,
                tileFlags: this.#clone(entry.tile.flags?.[MODULE_ID]),
                label: entry.label,
                labelFlags: entry.label ? this.#clone(entry.label.flags?.[MODULE_ID]) : undefined
            })),
            locationDocuments: plan.childLocations.map(entry => ({
                document: entry.document,
                entity: this.#clone(entry.document.getFlag(MODULE_ID, "campaignEntity"))
            })),
            journalPage: plan.journalPage,
            journalFlags: plan.journalPage ? this.#clone(plan.journalPage.flags?.[MODULE_ID]) : undefined,
            connectionGraph: this.#clone(plan.connectionPlan.graph)
        };
    }

    static async #apply(plan) {
        const oldTarget = {
            kind: "nexus-site",
            parentSceneId: plan.sourceParentScene.id,
            siteId: plan.record.siteId
        };
        const newTarget = {
            kind: "nexus-site",
            parentSceneId: plan.newParentScene.id,
            siteId: plan.record.siteId
        };
        const markerRefs = plan.markers.entries.map(({ scene, tile, label, marker }) => ({
            markerId: marker.markerId,
            sceneId: scene.id,
            documentName: tile.documentName || "Tile",
            documentId: tile.id,
            labelId: label?.id || "",
            markerKind: marker.markerKind || "reference",
            target: { ...(marker.target || {}), ...newTarget }
        }));

        plan.nextRecord = SiteRecordManager.normalizeRecord({
            ...plan.record,
            parentSceneId: plan.newParentScene.id,
            parentSceneName: plan.newParentScene.name,
            markerRefs
        }, { parentScene: plan.newParentScene });

        await this.#writeDestinationRecord(plan);
        await this.#retargetMarkers(plan, oldTarget, newTarget);
        await this.#updateChildLocations(plan);
        await this.#updateLinkedScene(plan);
        await this.#updateJournalPage(plan);
        await this.#moveConnections(plan);
        await this.#removeSourceRecord(plan);
    }

    static async #writeDestinationRecord(plan) {
        const sites = this.#clone(plan.newParentScene.getFlag(MODULE_ID, SITES_FLAG)) || {};
        sites.records = this.#clone(sites.records) || {};
        sites.records[plan.record.siteId] = this.#clone(plan.nextRecord);
        await plan.newParentScene.setFlag(MODULE_ID, SITES_FLAG, sites);
    }

    static async #retargetMarkers(plan, oldTarget, newTarget) {
        const tileUpdates = new Map();
        const drawingUpdates = new Map();

        for (const { scene, tile, label, marker } of plan.markers.entries) {
            if (!this.#targetMatches(marker.target, oldTarget)) {
                throw new Error(`Marker ${marker.markerId} changed after preflight.`);
            }
            const tileFlags = this.#clone(tile.flags?.[MODULE_ID]) || {};
            tileFlags.marker = {
                ...tileFlags.marker,
                target: { ...(tileFlags.marker?.target || {}), ...newTarget }
            };
            this.#pushDocumentUpdate(tileUpdates, scene, tile.documentName || "Tile", {
                _id: tile.id,
                [`flags.${MODULE_ID}`]: tileFlags
            });

            if (label) {
                const labelFlags = this.#clone(label.flags?.[MODULE_ID]) || {};
                labelFlags.target = { ...(labelFlags.target || {}), ...newTarget };
                this.#pushDocumentUpdate(drawingUpdates, scene, label.documentName || "Drawing", {
                    _id: label.id,
                    [`flags.${MODULE_ID}`]: labelFlags
                });
            }
        }

        await this.#applyEmbeddedUpdates(tileUpdates);
        await this.#applyEmbeddedUpdates(drawingUpdates);
    }

    static async #updateChildLocations(plan) {
        for (const { entity, document } of plan.childLocations) {
            const nextEntity = this.#clone(document.getFlag(MODULE_ID, "campaignEntity") || entity);
            nextEntity.hierarchy = {
                ...(nextEntity.hierarchy || {}),
                parent: {
                    kind: "site",
                    parentSceneId: plan.newParentScene.id,
                    siteId: plan.record.siteId
                }
            };
            await document.update({
                [`flags.${MODULE_ID}.campaignEntity`]: nextEntity
            });
        }
    }

    static async #updateLinkedScene(plan) {
        if (!plan.linkedScene) return;
        const markerOnDestination = plan.markers.entries.find(entry =>
            entry.scene.id === plan.newParentScene.id
        ) || null;
        const moduleFlags = this.#clone(plan.linkedScene.flags?.[MODULE_ID]) || {};
        moduleFlags.siteScene = true;
        moduleFlags.site = this.#clone(plan.nextRecord);
        moduleFlags.lineage = {
            parentSceneId: plan.newParentScene.id,
            parentSiteId: plan.record.siteId
        };
        moduleFlags.navigation = {
            parentSceneId: plan.newParentScene.id,
            sceneId: plan.newParentScene.id,
            parentSiteId: plan.record.siteId,
            transitionStyle: markerOnDestination ? "focus-note" : "scene-default",
            transitionContext: markerOnDestination ? {
                noteId: null,
                placeableId: markerOnDestination.tile.id,
                documentName: markerOnDestination.tile.documentName || "Tile",
                moduleId: MODULE_ID,
                flagKey: "marker.target.siteId",
                flagValue: plan.record.siteId
            } : {}
        };
        await plan.linkedScene.update({
            [`flags.${MODULE_ID}`]: moduleFlags
        });
        await NexusSceneFolderManager.placeExistingSceneInParentFolder(plan.newParentScene, plan.linkedScene, { autoSort: true });
    }

    static async #updateJournalPage(plan) {
        if (!plan.journalPage) return;
        const flags = this.#clone(plan.journalPage.flags?.[MODULE_ID]) || {};
        if (flags.siteId && flags.siteId !== plan.record.siteId) {
            throw new Error("The associated Journal page belongs to another Site.");
        }
        flags.parentSceneId = plan.newParentScene.id;
        flags.parentSceneName = plan.newParentScene.name;
        await plan.journalPage.update({
            [`flags.${MODULE_ID}`]: flags
        });
    }

    static async #moveConnections(plan) {
        const { graph, oldNodeId, newNodeId, oldNode } = plan.connectionPlan;
        if (!oldNode) return;
        const nextGraph = this.#clone(graph);
        nextGraph.nodes[newNodeId] = {
            ...nextGraph.nodes[oldNodeId],
            id: newNodeId,
            parentSceneId: plan.newParentScene.id,
            siteId: plan.record.siteId,
            updatedTime: Date.now()
        };
        delete nextGraph.nodes[oldNodeId];
        for (const edge of Object.values(nextGraph.edges || {})) {
            if (edge.sourceNodeId === oldNodeId) edge.sourceNodeId = newNodeId;
            if (edge.targetNodeId === oldNodeId) edge.targetNodeId = newNodeId;
            if (edge.views?.[oldNodeId]) {
                edge.views[newNodeId] = edge.views[oldNodeId];
                delete edge.views[oldNodeId];
            }
            edge.updatedTime = Date.now();
        }
        await ConnectionStore.setGraph(nextGraph, { reason: "reparentSite" });
    }

    static async #removeSourceRecord(plan) {
        const records = plan.sourceParentScene.getFlag(MODULE_ID, SITES_FLAG)?.records || {};
        if (!records[plan.record.siteId]) throw new Error("The source Site record disappeared during reparenting.");
        await plan.sourceParentScene.update({
            [`flags.${MODULE_ID}.${SITES_FLAG}.records.-=${plan.record.siteId}`]: null
        });
    }

    static #verify(plan) {
        if (SiteRecordManager.getSceneRecord(plan.sourceParentScene, plan.record.siteId)) {
            throw new Error("The old parent still contains the Site record.");
        }
        const movedRecord = SiteRecordManager.getSceneRecord(plan.newParentScene, plan.record.siteId);
        if (!movedRecord || movedRecord.parentSceneId !== plan.newParentScene.id) {
            throw new Error("The destination Site record could not be verified.");
        }
        for (const { tile, label } of plan.markers.entries) {
            const target = tile.flags?.[MODULE_ID]?.marker?.target || {};
            if (target.parentSceneId !== plan.newParentScene.id || target.siteId !== plan.record.siteId) {
                throw new Error(`Marker ${tile.id} was not retargeted.`);
            }
            if (label) {
                const labelTarget = label.flags?.[MODULE_ID]?.target || {};
                if (labelTarget.parentSceneId !== plan.newParentScene.id || labelTarget.siteId !== plan.record.siteId) {
                    throw new Error(`Marker label ${label.id} was not retargeted.`);
                }
            }
        }
        if (plan.linkedScene) {
            const lineage = plan.linkedScene.getFlag(MODULE_ID, "lineage") || {};
            if (lineage.parentSceneId !== plan.newParentScene.id || lineage.parentSiteId !== plan.record.siteId) {
                throw new Error("The linked Scene lineage was not updated.");
            }
        }
        for (const { document } of plan.childLocations) {
            const parent = document.getFlag(MODULE_ID, "campaignEntity")?.hierarchy?.parent || {};
            if (parent.parentSceneId !== plan.newParentScene.id || parent.siteId !== plan.record.siteId) {
                throw new Error(`Child Location ${document.name} was not updated.`);
            }
        }
        const graph = ConnectionStore.getGraph();
        if (graph.nodes?.[plan.connectionPlan.oldNodeId]) throw new Error("The old Connections node still exists.");
        if (plan.connectionPlan.oldNode && !graph.nodes?.[plan.connectionPlan.newNodeId]) {
            throw new Error("The new Connections node was not created.");
        }
    }

    static async #rollback(snapshot) {
        await this.#restoreFlag(snapshot.sourceParentScene, SITES_FLAG, snapshot.sourceSites);
        await this.#restoreFlag(snapshot.newParentScene, SITES_FLAG, snapshot.destinationSites);
        for (const entry of snapshot.markerDocuments) {
            await entry.scene.updateEmbeddedDocuments(entry.tile.documentName || "Tile", [{
                _id: entry.tile.id,
                [`flags.${MODULE_ID}`]: entry.tileFlags || {}
            }], { diff: false, [MODULE_ID]: { visualOnly: true } });
            if (entry.label) {
                await entry.scene.updateEmbeddedDocuments(entry.label.documentName || "Drawing", [{
                    _id: entry.label.id,
                    [`flags.${MODULE_ID}`]: entry.labelFlags || {}
                }], { diff: false, [MODULE_ID]: { visualOnly: true } });
            }
        }
        if (snapshot.linkedScene) {
            await snapshot.linkedScene.update({
                [`flags.${MODULE_ID}`]: snapshot.linkedSceneFlags || {},
                folder: snapshot.linkedSceneFolderId,
                sort: snapshot.linkedSceneSort
            }, { diff: false });
        }
        for (const { document, entity } of snapshot.locationDocuments) {
            await document.update({
                [`flags.${MODULE_ID}.campaignEntity`]: entity
            }, { diff: false });
        }
        if (snapshot.journalPage) {
            await snapshot.journalPage.update({
                [`flags.${MODULE_ID}`]: snapshot.journalFlags || {}
            }, { diff: false });
        }
        if (snapshot.connectionGraph) {
            await ConnectionStore.setGraph(snapshot.connectionGraph, { reason: "reparentSiteRollback" });
        }
    }

    static async #restoreFlag(document, key, value) {
        await document.unsetFlag(MODULE_ID, key);
        if (value !== undefined) await document.setFlag(MODULE_ID, key, this.#clone(value));
    }

    static #pushDocumentUpdate(map, scene, documentName, update) {
        const key = `${scene.id}:${documentName}`;
        const bucket = map.get(key) || { scene, documentName, updates: [] };
        bucket.updates.push(update);
        map.set(key, bucket);
    }

    static async #applyEmbeddedUpdates(groups) {
        for (const { scene, documentName, updates } of groups.values()) {
            if (updates.length) {
                await scene.updateEmbeddedDocuments(documentName, updates, {
                    diff: false,
                    [MODULE_ID]: { visualOnly: true }
                });
            }
        }
    }

    static #targetMatches(left = {}, right = {}) {
        return left?.kind === "nexus-site"
            && right?.kind === "nexus-site"
            && left.parentSceneId === right.parentSceneId
            && left.siteId === right.siteId;
    }

    static #notifyChanged(reason, plan) {
        NexusLineageManager.invalidateCache(reason);
        Hooks.callAll("augurNexusLineageChanged", {
            reason,
            siteId: plan.record?.siteId || null,
            oldParentSceneId: plan.sourceParentScene?.id || null,
            newParentSceneId: plan.newParentScene?.id || null
        });
        Hooks.callAll("augurNexusCampaignEntitiesChanged", {
            reason,
            siteId: plan.record?.siteId || null
        });
    }

    static #clone(value) {
        return value === undefined ? undefined : foundry.utils.deepClone(value);
    }
}
