import { LocationParentReference } from "../../campaign-entities/models/LocationParentReference.js";
import { EntityPlacementCommitService } from "../../campaign-entities/services/EntityPlacementCommitService.js";
import { CampaignEntityJournalStore } from "../../campaign-entities/services/CampaignEntityJournalStore.js";
import { LocationEntityStore } from "../../campaign-entities/services/LocationEntityStore.js";
import { ConnectionStore } from "../../connections/services/ConnectionStore.js";
import { ConnectionTargetResolver } from "../../connections/services/ConnectionTargetResolver.js";
import { OwnershipService } from "../../connections/services/OwnershipService.js";
import { TradingCenterService } from "../../connections/services/TradingCenterService.js";
import { FactionMembershipService } from "../../connections/services/FactionMembershipService.js";
import { FactionSeatService } from "../../connections/services/FactionSeatService.js";
import { LocationLeadershipService } from "../../connections/services/LocationLeadershipService.js";
import { ConnectionOpinionService } from "../../connections/services/ConnectionOpinionService.js";
import { RegionChronicleService } from "./RegionChronicleService.js";
import { RegionPopulationRunStore } from "./RegionPopulationRunStore.js";
import { RegionPopulationSurveyService } from "./RegionPopulationSurveyService.js";

const TRADE_CATEGORY = {
    id: "trade-routes",
    label: "Trade Routes",
    icon: "fas fa-route",
    color: "#c99a45"
};

export class RegionPopulationProgressiveService {
    static async run({
        scene,
        regionProvider,
        populationProvider,
        settings = {},
        onProgress = null,
        onEvent = null,
        signal = null
    } = {}) {
        if (!scene?.id || !regionProvider || !populationProvider || typeof populationProvider.runDevelopment !== "function") {
            throw new Error("The progressive region population contract is incomplete.");
        }

        const runId = foundry.utils.randomID();
        const startedAt = Date.now();
        const roadContributionId = `${scene.id}:${runId}`;
        const overlayContributionId = `${scene.id}:${runId}:overlays`;
        const snapshot = await regionProvider.getSnapshot(scene);
        const survey = RegionPopulationSurveyService.assertUsable(RegionPopulationSurveyService.build(scene, snapshot, {
            includedFactionIds: settings.regionalPowerEntityIds || []
        }));
        const startYear = Number(survey.clock?.developedThroughYear || 0);
        const entitiesByKey = new Map();
        for (const row of [...survey.locations, ...survey.factions, ...survey.npcs]) {
            entitiesByKey.set(row.key, foundry.utils.deepClone(row.entity));
        }
        const committedEvents = [];
        let committedCounts = {};
        let lastCompletedYear = startYear;
        let activeStep = null;
        const { NexusMarkerService } = await import("../../markers/services/NexusMarkerService.js");

        const trackManifest = async (type, id) => {
            const value = String(id || "").trim();
            if (!value) return;
            const field = { location: "locationIds", npc: "npcIds", faction: "factionIds", connection: "connectionIds" }[type];
            if (!field) return;
            if (activeStep) {
                activeStep[field].add(value);
                return;
            }
            if (type === "location") await RegionPopulationRunStore.appendLocation(scene, value);
            else if (type === "connection") await RegionPopulationRunStore.appendConnection(scene, value);
            else await RegionPopulationRunStore.appendEntity(scene, type, value);
        };
        const untrackManifest = (type, id) => {
            const field = { location: "locationIds", npc: "npcIds", faction: "factionIds", connection: "connectionIds" }[type];
            if (field && activeStep) activeStep[field].delete(String(id || ""));
        };

        await ConnectionStore.upsertCustomCategory(TRADE_CATEGORY);
        await RegionPopulationRunStore.start(scene, {
            runId,
            regionProviderId: regionProvider.id,
            populationProviderId: populationProvider.id,
            seed: settings.seed || "",
            settings,
            startYear,
            roadContributionId,
            overlayContributionId
        });

        const operations = {
            runDevelopmentStep: async (step = {}, callback) => {
                if (typeof callback !== "function") return undefined;
                if (activeStep) return callback();
                activeStep = {
                    locationIds: new Set(),
                    npcIds: new Set(),
                    factionIds: new Set(),
                    connectionIds: new Set(),
                    completedYear: lastCompletedYear
                };
                let committed = false;
                try {
                    const result = await ConnectionStore.runBatch(callback, {
                        reason: `regionDevelopmentStep:${Number(step.startYear || 0)}-${Number(step.endYear || 0)}`
                    });
                    committed = true;
                    await RegionPopulationRunStore.checkpoint(scene, {
                        locationIds: [...activeStep.locationIds],
                        npcIds: [...activeStep.npcIds],
                        factionIds: [...activeStep.factionIds],
                        connectionIds: [...activeStep.connectionIds],
                        currentYear: activeStep.completedYear
                    });
                    return result;
                } catch (error) {
                    try {
                        await RegionPopulationRunStore.checkpoint(scene, {
                            locationIds: [...activeStep.locationIds],
                            npcIds: [...activeStep.npcIds],
                            factionIds: [...activeStep.factionIds],
                            connectionIds: committed ? [...activeStep.connectionIds] : [],
                            currentYear: lastCompletedYear
                        });
                    } catch (checkpointError) {
                        console.error("Augur: Nexus | Failed to preserve an interrupted development step manifest.", checkpointError);
                    }
                    throw error;
                } finally {
                    activeStep = null;
                }
            },
            createLocation: definition => this.#createLocation({
                scene,
                snapshot,
                definition,
                entitiesByKey,
                markerService: NexusMarkerService,
                trackManifest
            }),
            updateLocation: definition => this.#updateLocation({
                definition,
                entitiesByKey,
                markerService: NexusMarkerService
            }),
            updateFaction: definition => this.#updateFaction({ definition, entitiesByKey }),
            createNpc: definition => this.#createCampaignEntity({
                scene,
                entityType: "npc",
                definition,
                entitiesByKey,
                trackManifest,
                untrackManifest
            }),
            createFaction: definition => this.#createCampaignEntity({
                scene,
                entityType: "faction",
                definition,
                entitiesByKey,
                trackManifest,
                untrackManifest
            }),
            connect: definition => this.#connect(definition, entitiesByKey, scene, trackManifest),
            adjustOpinion: definition => this.#adjustOpinion(definition, entitiesByKey),
            setOwner: definition => this.#setOwner(definition, entitiesByKey),
            setFactionSeat: definition => this.#setFactionSeat(definition, entitiesByKey),
            setLocationLeader: definition => this.#setLocationLeader(definition, entitiesByKey),
            canSetTradingCenter: definition => this.#canSetTradingCenter(definition, entitiesByKey),
            setTradingCenter: definition => this.#setTradingCenter(definition, entitiesByKey),
            appendRoads: routes => this.#appendRoads({
                scene,
                regionProvider,
                roadContributionId,
                routes
            }),
            appendCellOverlays: definitions => this.#appendCellOverlays({
                scene,
                regionProvider,
                overlayContributionId,
                definitions,
                entitiesByKey
            }),
            getEntity: key => foundry.utils.deepClone(entitiesByKey.get(String(key || "")) || null)
        };

        let chronicle = null;
        try {
            const result = await populationProvider.runDevelopment({
                scene,
                snapshot,
                survey,
                settings: foundry.utils.deepClone(settings),
                operations,
                signal,
                onProgress,
                onYearComplete: async year => {
                    lastCompletedYear = Math.max(lastCompletedYear, Number(year || 0));
                    if (activeStep) {
                        activeStep.completedYear = lastCompletedYear;
                        return;
                    }
                    await RegionPopulationRunStore.markStatus(scene, "generating", {
                        currentYear: lastCompletedYear,
                        endYear: lastCompletedYear
                    });
                },
                onEvent: (event, counts) => {
                    committedEvents.push(foundry.utils.deepClone(event));
                    if (counts) committedCounts = foundry.utils.deepClone(counts);
                    onEvent?.(event, counts);
                }
            });
            if (signal?.aborted) throw new DOMException("Generation cancelled.", "AbortError");
            await NexusMarkerService.syncOwnershipLabelsForScene(scene);

            if (result?.chronicle) {
                chronicle = await RegionChronicleService.append(scene, {
                    runId,
                    data: result.chronicle
                });
            }
            const run = await RegionPopulationRunStore.markStatus(scene, "complete", {
                completedAt: Date.now(),
                elapsedMs: Date.now() - startedAt,
                summary: foundry.utils.deepClone(result?.summary || {}),
                currentYear: Number(result?.summary?.years || startYear + Number(settings.years || 0)),
                endYear: Number(result?.summary?.years || startYear + Number(settings.years || 0)),
                chronicleJournalEntryId: chronicle?.entry?.id || ""
            });
            RegionChronicleService.refreshAvailability(scene);
            return { run, result };
        } catch (error) {
            if (committedEvents.length) {
                try {
                    const partialChronicle = {
                        data: this.#buildChronicleData({
                            scene,
                            runId,
                            populationProvider,
                            settings,
                            events: committedEvents,
                            counts: committedCounts,
                            partial: true
                        })
                    };
                    partialChronicle.data.years = Math.max(Number(partialChronicle.data.years || 0), lastCompletedYear);
                    partialChronicle.data.summary.years = partialChronicle.data.years;
                    chronicle = chronicle
                        ? await RegionChronicleService.update(chronicle, partialChronicle)
                        : await RegionChronicleService.append(scene, { runId, ...partialChronicle });
                } catch (journalError) {
                    console.warn("Augur: Nexus | Failed to preserve the partial regional chronicle.", journalError);
                }
            }
            await RegionPopulationRunStore.markStatus(scene, "incomplete", {
                elapsedMs: Date.now() - startedAt,
                failureMessage: error?.name === "AbortError"
                    ? "Generation was cancelled."
                    : String(error?.message || "Generation stopped unexpectedly."),
                chronicleJournalEntryId: chronicle?.entry?.id || "",
                currentYear: lastCompletedYear,
                endYear: lastCompletedYear
            });
            RegionChronicleService.refreshAvailability(scene);
            throw error;
        }
    }

    static async #createLocation({ scene, snapshot, definition, entitiesByKey, markerService, trackManifest }) {
        const key = String(definition?.key || "").trim();
        if (!key) throw new Error("Population location key is missing.");
        if (entitiesByKey.has(key)) throw new Error(`Population location key ${key} is already in use.`);
        const candidate = foundry.utils.deepClone(definition.candidate || {});
        candidate.hierarchy = {
            ...(candidate.hierarchy || {}),
            parent: LocationParentReference.forScene(scene)
        };
        const position = this.#cellPosition(snapshot, definition.cellId);
        const result = await EntityPlacementCommitService.commit({
            draft: {
                type: "location",
                candidate,
                composition: foundry.utils.deepClone(definition.composition || { members: [] })
            },
            createMarker: entity => markerService.createEntityMarker(entity, position, {
                scene,
                iconSize: Number(definition.iconSize || 128),
                showLabel: true,
                labelFontSize: definition.labelFontSize,
                labelColor: definition.labelColor,
                snapToGrid: false
            })
        });
        entitiesByKey.set(key, result.root);
        try {
            await trackManifest("location", result.root.id);
        } catch (error) {
            try {
                await LocationEntityStore.deleteLocation(result.root.id);
            } catch (rollbackError) {
                console.error("Augur: Nexus | Failed to roll back an untracked generated Location.", rollbackError);
            }
            throw error;
        }
        return foundry.utils.deepClone(result.root);
    }

    static async #updateLocation({ definition, entitiesByKey, markerService }) {
        const key = String(definition?.key || "").trim();
        const current = entitiesByKey.get(key);
        if (!current) throw new Error(`Population location ${key || "missing"} could not be updated.`);
        const updated = await LocationEntityStore.updateLocation(
            current.id,
            foundry.utils.deepClone(definition.patch || {}),
            {
                moduleId: String(definition.moduleId || current.sourceModule || ""),
                syncMarkers: false
            }
        );
        entitiesByKey.set(key, updated);
        if (definition.syncMarkers !== false) await markerService.syncMarkersForTarget(updated, { iconSize: definition.iconSize });
        return foundry.utils.deepClone(updated);
    }

    static async #updateFaction({ definition, entitiesByKey }) {
        const key = String(definition?.key || "").trim();
        const current = entitiesByKey.get(key);
        if (!current || current.type !== "faction") throw new Error(`Population faction ${key || "missing"} could not be updated.`);
        const updated = await CampaignEntityJournalStore.updateFaction(
            current.id,
            foundry.utils.deepClone(definition.patch || {}),
            { moduleId: String(definition.moduleId || current.sourceModule || "") }
        );
        entitiesByKey.set(key, updated);
        return foundry.utils.deepClone(updated);
    }

    static async #createCampaignEntity({ scene, entityType, definition, entitiesByKey, trackManifest, untrackManifest }) {
        const key = String(definition?.key || "").trim();
        const connections = Array.isArray(definition?.connections) ? definition.connections : [];
        if (!key || entitiesByKey.has(key)) throw new Error(`Population ${entityType} key ${key || "missing"} is invalid.`);
        if (!connections.length) throw new Error(`A generated ${entityType} requires an immediate regional Connection.`);

        let entity = null;
        try {
            const candidate = foundry.utils.deepClone(definition.candidate || {});
            entity = entityType === "npc"
                ? await CampaignEntityJournalStore.createNpc(candidate)
                : await CampaignEntityJournalStore.createFaction(candidate);
            entitiesByKey.set(key, entity);
            await trackManifest(entityType, entity.id);

            for (const connection of connections) {
                const next = foundry.utils.deepClone(connection || {});
                next.sourceKey ||= key;
                if (next.sourceKey !== key && next.targetKey !== key) {
                    throw new Error(`A generated ${entityType} Connection must include its own key.`);
                }
                await this.#connect(next, entitiesByKey, scene, trackManifest);
            }
            return foundry.utils.deepClone(entity);
        } catch (error) {
            entitiesByKey.delete(key);
            if (entity?.id) untrackManifest(entityType, entity.id);
            if (entity?.id) {
                try {
                    if (entityType === "npc") await CampaignEntityJournalStore.deleteNpc(entity.id);
                    else await CampaignEntityJournalStore.deleteFaction(entity.id);
                } catch (rollbackError) {
                    console.error(`Augur: Nexus | Failed to roll back a generated ${entityType}.`, rollbackError);
                }
            }
            throw error;
        }
    }

    static async #connect(definition, entitiesByKey, scene, trackManifest) {
        const source = entitiesByKey.get(String(definition?.sourceKey || ""));
        const target = entitiesByKey.get(String(definition?.targetKey || ""));
        if (!source || !target) throw new Error("A regional connection endpoint could not be resolved.");
        const sourceTarget = ConnectionTargetResolver.fromCampaignEntity(source);
        const targetTarget = ConnectionTargetResolver.fromCampaignEntity(target);
        if (!sourceTarget || !targetTarget) throw new Error("A regional connection target could not be resolved.");

        const existingEdge = (ConnectionStore.getGraphContext().adjacency.get(sourceTarget.id) || [])
            .find(({ node }) => node.id === targetTarget.id)?.edge || null;
        const result = await ConnectionStore.runBatch(async () => {
            const sourceView = foundry.utils.deepClone(definition.sourceView || {});
            const targetView = foundry.utils.deepClone(definition.targetView || {});
            const resourceFlows = (Array.isArray(definition.resourceFlows) ? definition.resourceFlows : []).map(flow => {
                const fromEntity = entitiesByKey.get(String(flow?.fromKey || ""));
                const toEntity = entitiesByKey.get(String(flow?.toKey || ""));
                return {
                    ...foundry.utils.deepClone(flow),
                    fromNodeId: ConnectionTargetResolver.getNodeId(ConnectionTargetResolver.fromCampaignEntity(fromEntity)),
                    toNodeId: ConnectionTargetResolver.getNodeId(ConnectionTargetResolver.fromCampaignEntity(toEntity))
                };
            });
            const edge = await ConnectionStore.addConnection(sourceTarget, targetTarget, {
                sourceView,
                targetView,
                relationType: definition.relationType || "",
                resourceFlows
            });
            if (!edge) throw new Error("A regional connection could not be created.");
            const edgePatch = {};
            if (Object.hasOwn(definition, "relationType")) edgePatch.relationType = definition.relationType || "";
            if (Array.isArray(definition.resourceFlows)) edgePatch.resourceFlows = resourceFlows;
            if (Object.keys(edgePatch).length) await ConnectionStore.updateConnection(edge.id, edgePatch);
            await ConnectionStore.setConnectionViewForNode(edge.id, sourceTarget.id, sourceView);
            await ConnectionStore.setConnectionViewForNode(edge.id, targetTarget.id, targetView);
            if (definition.membershipTier === "leader") {
                await FactionMembershipService.setLeader(sourceTarget, targetTarget, {
                    edgeId: edge.id,
                    rank: definition.membershipRank
                });
            } else if (definition.membershipTier === "member") {
                await FactionMembershipService.setMember(sourceTarget, targetTarget, {
                    edgeId: edge.id,
                    rank: definition.membershipRank
                });
            }
            return ConnectionStore.getConnectionEdge(edge.id);
        }, { reason: "regionPopulationConnection" });
        if (!existingEdge && result?.id) await trackManifest("connection", result.id);
        return result;
    }

    static async #adjustOpinion(definition, entitiesByKey) {
        const observer = entitiesByKey.get(String(definition?.observerKey || definition?.sourceKey || ""));
        const subject = entitiesByKey.get(String(definition?.subjectKey || definition?.targetKey || ""));
        if (!observer || !subject) throw new Error("A regional opinion endpoint could not be resolved.");
        const observerTarget = ConnectionTargetResolver.fromCampaignEntity(observer);
        const subjectTarget = ConnectionTargetResolver.fromCampaignEntity(subject);
        if (!observerTarget || !subjectTarget) throw new Error("A regional opinion target could not be resolved.");
        const edge = ConnectionStore.getConnectionsForNode(observerTarget.id)
            .find(connection => connection.node?.id === subjectTarget.id)?.edge || null;
        if (!edge) throw new Error("A regional opinion requires an existing social Connection.");
        if (!ConnectionOpinionService.isTracked(edge)) await ConnectionOpinionService.enable(edge.id);
        return ConnectionOpinionService.addAdjustment(edge.id, observerTarget.id, {
            id: String(definition?.id || "").trim() || foundry.utils.randomID(),
            label: String(definition?.label || "Shared history").trim() || "Shared history",
            value: Number(definition?.value || 0),
            source: foundry.utils.deepClone(definition?.source || { kind: "region-development", id: null })
        }, { bidirectional: definition?.bidirectional === true });
    }

    static async #setOwner(definition, entitiesByKey) {
        const asset = entitiesByKey.get(String(definition?.assetKey || ""));
        const owner = entitiesByKey.get(String(definition?.ownerKey || ""));
        if (!asset || !owner) throw new Error("A regional ownership endpoint could not be resolved.");
        const assetTarget = ConnectionTargetResolver.fromCampaignEntity(asset);
        const ownerTarget = ConnectionTargetResolver.fromCampaignEntity(owner);
        if (!assetTarget || !ownerTarget) throw new Error("A regional ownership target could not be resolved.");
        return OwnershipService.setOwner(assetTarget, ownerTarget);
    }

    static async #setFactionSeat(definition, entitiesByKey) {
        const faction = entitiesByKey.get(String(definition?.factionKey || ""));
        const location = entitiesByKey.get(String(definition?.locationKey || ""));
        if (!faction || !location) throw new Error("A regional faction-seat endpoint could not be resolved.");
        const factionTarget = ConnectionTargetResolver.fromCampaignEntity(faction);
        const locationTarget = ConnectionTargetResolver.fromCampaignEntity(location);
        if (!factionTarget || !locationTarget) throw new Error("A regional faction-seat target could not be resolved.");
        return FactionSeatService.setSeat(factionTarget, locationTarget);
    }

    static async #setLocationLeader(definition, entitiesByKey) {
        const location = entitiesByKey.get(String(definition?.locationKey || ""));
        const leader = entitiesByKey.get(String(definition?.leaderKey || ""));
        if (!location || !leader) throw new Error("A regional Location-leadership endpoint could not be resolved.");
        const locationTarget = ConnectionTargetResolver.fromCampaignEntity(location);
        const leaderTarget = ConnectionTargetResolver.fromCampaignEntity(leader);
        if (!locationTarget || !leaderTarget) throw new Error("A regional Location-leadership target could not be resolved.");
        return LocationLeadershipService.setLeader(locationTarget, leaderTarget, {
            role: String(definition?.role || "Ruler")
        });
    }

    static async #setTradingCenter(definition, entitiesByKey) {
        const location = entitiesByKey.get(String(definition?.locationKey || ""));
        const center = entitiesByKey.get(String(definition?.centerKey || ""));
        if (!location || !center) throw new Error("A regional trading-center endpoint could not be resolved.");
        const locationTarget = ConnectionTargetResolver.fromCampaignEntity(location);
        const centerTarget = ConnectionTargetResolver.fromCampaignEntity(center);
        if (!locationTarget || !centerTarget) throw new Error("A regional trading-center target could not be resolved.");
        return TradingCenterService.setTradingCenter(locationTarget, centerTarget);
    }

    static #canSetTradingCenter(definition, entitiesByKey) {
        const location = entitiesByKey.get(String(definition?.locationKey || ""));
        const center = entitiesByKey.get(String(definition?.centerKey || ""));
        if (!location || !center) return false;
        const locationTarget = ConnectionTargetResolver.fromCampaignEntity(location);
        const centerTarget = ConnectionTargetResolver.fromCampaignEntity(center);
        return !!locationTarget && !!centerTarget && TradingCenterService.canSetTradingCenter(locationTarget, centerTarget);
    }

    static async #appendRoads({ scene, regionProvider, roadContributionId, routes }) {
        if (!Array.isArray(routes) || !routes.length || typeof regionProvider.applyRoads !== "function") return null;
        return regionProvider.applyRoads(scene, routes, { contributionId: roadContributionId });
    }

    static async #appendCellOverlays({ scene, regionProvider, overlayContributionId, definitions, entitiesByKey }) {
        if (!Array.isArray(definitions) || !definitions.length || typeof regionProvider.applyCellOverlays !== "function") return null;
        const overlays = definitions.map(definition => ({
            ...foundry.utils.deepClone(definition),
            ownerEntityId: entitiesByKey.get(String(definition?.ownerKey || ""))?.id || ""
        }));
        return regionProvider.applyCellOverlays(scene, overlays, { contributionId: overlayContributionId });
    }

    static #cellPosition(snapshot = {}, cellId = "") {
        const cell = (snapshot.cells || []).find(candidate => candidate.id === cellId);
        if (!cell?.center) throw new Error(`Population cell ${cellId || "unknown"} could not be resolved.`);
        return { x: Number(cell.center.x || 0), y: Number(cell.center.y || 0) };
    }

    static #buildChronicleData({ scene, runId, populationProvider, settings = {}, events = [], counts = {}, partial = false } = {}) {
        const configuredYears = Number(settings.years || 0);
        const lastRecordedYear = Math.max(0, ...(events || []).map(event => Number(event?.year || 0)));
        const years = partial ? lastRecordedYear : configuredYears;
        const summary = {
            years,
            roots: Math.max(Number(settings.rootCount || 0), Number(counts?.factions || 0)),
            ...foundry.utils.deepClone(counts || {})
        };
        return {
            title: String(populationProvider?.chroniclePresentation?.title || "Regional Chronicle"),
            theme: String(populationProvider?.chroniclePresentation?.theme || "medieval"),
            sourceModuleId: String(populationProvider?.chroniclePresentation?.sourceModuleId || ""),
            sourceProviderId: String(populationProvider?.id || ""),
            sceneId: String(scene?.id || ""),
            sceneName: String(scene?.name || "Unnamed Region"),
            runId: String(runId || ""),
            seed: String(settings.seed || ""),
            years,
            partial: partial === true,
            summary,
            events: foundry.utils.deepClone(events || [])
        };
    }
}
