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 { RegionPopulationRunStore } from "./RegionPopulationRunStore.js";

const INCLUDED_ENTITY_TYPES = new Set(["location", "faction", "npc"]);

export class RegionPopulationSurveyService {
    static build(scene, snapshot = {}, { includedFactionIds = [] } = {}) {
        if (!scene?.id) throw new Error("A Scene is required to survey regional development.");
        const placements = this.#normalizePlacements(snapshot.placements || []);
        const graphContext = ConnectionStore.getGraphContext();
        const includedIds = new Set();
        const locationIds = new Set(placements.filter(row => row.entityType === "location").map(row => row.entityId));
        const factionIds = new Set(placements.filter(row => row.entityType === "faction").map(row => row.entityId));
        const discoveredFactionIds = new Set(factionIds);
        const explicitFactionIds = new Set(
            (Array.isArray(includedFactionIds) ? includedFactionIds : []).map(String).filter(Boolean)
        );
        const npcIds = new Set(placements.filter(row => row.entityType === "npc").map(row => row.entityId));
        const warnings = [];
        const errors = [];

        for (const id of locationIds) includedIds.add(id);
        for (const id of factionIds) includedIds.add(id);
        for (const id of explicitFactionIds) {
            factionIds.add(id);
            includedIds.add(id);
        }
        for (const id of npcIds) includedIds.add(id);

        const locationsById = this.#entityMap(locationIds, id => LocationEntityStore.getLocation(id), "Location", errors);
        const factionsById = this.#entityMap(factionIds, id => CampaignEntityJournalStore.getFaction(id), "Faction", warnings);
        const npcsById = this.#entityMap(npcIds, id => CampaignEntityJournalStore.getNpc(id), "NPC", warnings);

        this.#validateLocationPlacements(placements, locationsById, errors);

        const includeNode = node => {
            if (node?.kind !== "nexus-entity" || !INCLUDED_ENTITY_TYPES.has(node.entityType) || !node.entityId) return false;
            includedIds.add(node.entityId);
            if (node.entityType === "faction") factionIds.add(node.entityId);
            if (node.entityType === "npc") npcIds.add(node.entityId);
            return true;
        };

        // On-map Locations define the regional boundary. Their owners, rulers, and directly
        // connected people are part of the material state even when they have no marker.
        for (const locationId of locationIds) {
            const nodeId = ConnectionTargetResolver.getEntityNodeId(locationId);
            for (const { edge, node } of graphContext.adjacency.get(nodeId) || []) {
                if (node?.entityType === "faction" && this.#isLocationFactionEdge(edge, nodeId)) {
                    discoveredFactionIds.add(node.entityId);
                    includeNode(node);
                }
                if (node?.entityType === "npc") includeNode(node);
            }
        }

        // Once a faction is relevant, its members are the bounded social cast for development.
        for (const factionId of [...factionIds]) {
            const nodeId = ConnectionTargetResolver.getEntityNodeId(factionId);
            for (const { edge, node } of graphContext.adjacency.get(nodeId) || []) {
                if (node?.entityType === "npc" && edge?.factionMembership) includeNode(node);
            }
        }

        for (const id of factionIds) {
            const entity = CampaignEntityJournalStore.getFaction(id);
            if (entity) factionsById.set(id, entity);
            else warnings.push(`Organization ${id} is referenced by the regional map but could not be found.`);
        }
        for (const id of npcIds) {
            const entity = CampaignEntityJournalStore.getNpc(id);
            if (entity) npcsById.set(id, entity);
            else warnings.push(`Person ${id} is referenced by the regional map but could not be found.`);
        }

        const includedNodeIds = new Set([...includedIds].map(id => ConnectionTargetResolver.getEntityNodeId(id)));
        const connections = Object.values(graphContext.graph.edges || {})
            .filter(edge => includedNodeIds.has(edge.sourceNodeId) && includedNodeIds.has(edge.targetNodeId))
            .map(edge => foundry.utils.deepClone(edge));
        const factionSeats = Object.values(graphContext.graph.edges || {})
            .filter(edge => edge?.factionSeat?.factionNodeId && edge?.factionSeat?.placeNodeId)
            .map(edge => ({
                factionId: this.#entityIdFromNodeId(edge.factionSeat.factionNodeId),
                locationId: this.#entityIdFromNodeId(edge.factionSeat.placeNodeId)
            }))
            .filter(row => factionIds.has(row.factionId));
        const byCell = this.#placementsByCell(placements);
        const run = RegionPopulationRunStore.get(scene);
        const survey = {
            schemaVersion: 1,
            sceneId: scene.id,
            providerId: String(snapshot.providerId || ""),
            clock: {
                developedThroughYear: Number(run?.currentYear ?? run?.summary?.years ?? 0),
                chronicleJournalEntryId: String(run?.chronicleJournalEntryId || ""),
                previousSummary: foundry.utils.deepClone(run?.summary || {})
            },
            placements,
            placementsByCell: Object.fromEntries([...byCell.entries()]),
            locations: [...locationsById.values()].map(entity => this.#entityRow(entity, placements)),
            factions: [...factionsById.values()].map(entity => ({
                ...this.#entityRow(entity, placements),
                explicitlyIncluded: explicitFactionIds.has(entity.id),
                discoveredInRegion: discoveredFactionIds.has(entity.id)
            })),
            npcs: [...npcsById.values()].map(entity => this.#entityRow(entity, placements)),
            connections,
            relationships: { factionSeats },
            roads: { edges: [...new Set((snapshot.roadEdges || []).map(String).filter(Boolean))] },
            overlays: foundry.utils.deepClone(snapshot.populationOverlays || []),
            graphSignature: String(graphContext.signature || ""),
            warnings: [...new Set(warnings)],
            errors: [...new Set(errors)]
        };
        survey.signature = this.#signature(survey);
        return survey;
    }

    static assertUsable(survey = {}) {
        if (survey.errors?.length) throw new Error(survey.errors.join("\n"));
        return survey;
    }

    static #normalizePlacements(placements) {
        return placements.map(row => ({
            markerId: String(row?.markerId || ""),
            tileId: String(row?.tileId || ""),
            entityId: String(row?.entityId || ""),
            entityType: String(row?.entityType || ""),
            cellId: String(row?.cellId || ""),
            center: {
                x: Number(row?.center?.x || 0),
                y: Number(row?.center?.y || 0)
            }
        })).filter(row => row.entityId && INCLUDED_ENTITY_TYPES.has(row.entityType) && row.cellId);
    }

    static #entityMap(ids, resolver, label, issues) {
        const result = new Map();
        for (const id of ids) {
            const entity = resolver(id);
            if (entity) result.set(id, entity);
            else issues.push(`${label} marker ${id} does not resolve to an existing Nexus entity.`);
        }
        return result;
    }

    static #validateLocationPlacements(placements, locationsById, errors) {
        const locationRows = placements.filter(row => row.entityType === "location");
        const markersByLocation = new Map();
        const locationsByCell = new Map();
        for (const row of locationRows) {
            const entityRows = markersByLocation.get(row.entityId) || [];
            entityRows.push(row);
            markersByLocation.set(row.entityId, entityRows);
            const cellRows = locationsByCell.get(row.cellId) || [];
            cellRows.push(row);
            locationsByCell.set(row.cellId, cellRows);
        }
        for (const [entityId, rows] of markersByLocation) {
            if (rows.length > 1) {
                const name = locationsById.get(entityId)?.display?.name || entityId;
                errors.push(`${name} is placed more than once on this hex map. Keep one marker before developing the region.`);
            }
        }
        for (const [cellId, rows] of locationsByCell) {
            const distinct = new Set(rows.map(row => row.entityId));
            if (distinct.size > 1) errors.push(`Hex ${cellId} contains more than one Location. Move them to separate hexes before developing the region.`);
        }
    }

    static #isLocationFactionEdge(edge, locationNodeId) {
        return (edge?.relationType === "ownership" && edge.targetNodeId === locationNodeId)
            || edge?.factionSeat?.placeNodeId === locationNodeId;
    }

    static #placementsByCell(placements) {
        const result = new Map();
        for (const placement of placements) {
            const rows = result.get(placement.cellId) || [];
            rows.push(foundry.utils.deepClone(placement));
            result.set(placement.cellId, rows);
        }
        return result;
    }

    static #entityRow(entity, placements) {
        return {
            key: `${entity.type}:${entity.id}`,
            entityId: entity.id,
            entityType: entity.type,
            entity: foundry.utils.deepClone(entity),
            placementCellIds: placements
                .filter(row => row.entityId === entity.id && row.entityType === entity.type)
                .map(row => row.cellId)
        };
    }

    static #signature(survey) {
        const material = {
            sceneId: survey.sceneId,
            clock: survey.clock,
            placements: survey.placements,
            entities: [...survey.locations, ...survey.factions, ...survey.npcs].map(row => row.entity),
            connections: survey.connections,
            relationships: survey.relationships,
            roads: survey.roads,
            overlays: survey.overlays,
            graphSignature: survey.graphSignature
        };
        const text = this.#stableSerialize(material);
        let hash = 2166136261;
        for (let index = 0; index < text.length; index++) {
            hash ^= text.charCodeAt(index);
            hash = Math.imul(hash, 16777619);
        }
        return (hash >>> 0).toString(36);
    }

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

    static #entityIdFromNodeId(nodeId) {
        return String(nodeId || "").replace(/^nexus-entity:/, "");
    }
}
