import { CampaignEntityJournalStore } from "../../campaign-entities/services/CampaignEntityJournalStore.js";
import { ConnectionStore } from "../../connections/services/ConnectionStore.js";

const ENTITY_TYPES = new Set(["npc", "ship", "faction"]);

const ADAPTERS = {
    npc: {
        label: "Person",
        plural: "People",
        get: id => CampaignEntityJournalStore.getNpc(id)
    },
    ship: {
        label: "Ship",
        plural: "Ships",
        get: id => CampaignEntityJournalStore.getShip(id)
    },
    faction: {
        label: "Organization",
        plural: "Organizations",
        get: id => CampaignEntityJournalStore.getFaction(id)
    }
};

function uniqueReasons(reasons = []) {
    return [...new Set(reasons.filter(Boolean))];
}

function getRelatedNodeId(edge, nodeId) {
    if (edge?.sourceNodeId === nodeId) return edge.targetNodeId || "";
    if (edge?.targetNodeId === nodeId) return edge.sourceNodeId || "";
    return "";
}

function isSupportedEntityNode(node) {
    return node?.kind === "nexus-entity" && ENTITY_TYPES.has(node.entityType);
}

function displayName(node, entity) {
    return entity?.display?.name || node?.name || ADAPTERS[node?.entityType]?.label || "Unknown Entity";
}

export class BranchLocalEntityDeletionPolicy {
    static analyze({ anchorNodeIds = [], sceneIds = [] } = {}) {
        const anchors = new Set(anchorNodeIds.filter(Boolean));
        const deletedSceneIds = new Set(sceneIds.filter(Boolean));
        if (!anchors.size) return { candidates: [], preserved: [], components: [] };

        const context = ConnectionStore.getGraphContext();
        const graph = context.graph || {};
        const nodes = graph.nodes || {};
        const incidentEdges = new Map();

        const addIncidentEdge = (nodeId, edge) => {
            if (!nodeId) return;
            const bucket = incidentEdges.get(nodeId) || [];
            bucket.push(edge);
            incidentEdges.set(nodeId, bucket);
        };

        for (const edge of Object.values(graph.edges || {})) {
            addIncidentEdge(edge?.sourceNodeId, edge);
            if (edge?.targetNodeId !== edge?.sourceNodeId) addIncidentEdge(edge?.targetNodeId, edge);
        }

        const reachableNodeIds = new Set();
        const queue = [...anchors];
        const visited = new Set(queue);

        while (queue.length) {
            const nodeId = queue.shift();
            for (const edge of incidentEdges.get(nodeId) || []) {
                const relatedNodeId = getRelatedNodeId(edge, nodeId);
                if (!relatedNodeId || visited.has(relatedNodeId)) continue;
                const relatedNode = nodes[relatedNodeId] || null;
                if (!isSupportedEntityNode(relatedNode)) continue;
                visited.add(relatedNodeId);
                reachableNodeIds.add(relatedNodeId);
                queue.push(relatedNodeId);
            }
        }

        const records = new Map();
        const blockedReasons = new Map();

        for (const nodeId of reachableNodeIds) {
            const node = nodes[nodeId] || null;
            const adapter = ADAPTERS[node?.entityType] || null;
            const entity = adapter && node?.entityId ? adapter.get(node.entityId) : null;
            const reasons = [];
            const edges = incidentEdges.get(nodeId) || [];

            if (!node || !adapter || !entity) reasons.push("entity or Connection record could not be resolved");
            if (!edges.length) reasons.push("Connection state could not be resolved");
            if (entity?.projections?.actorUuid) reasons.push("has an Actor projection");
            if ((entity?.markerRefs || []).some(ref => !ref?.sceneId || !deletedSceneIds.has(ref.sceneId))) {
                reasons.push("has a marker outside the deletion scope");
            }

            for (const edge of edges) {
                const relatedNodeId = getRelatedNodeId(edge, nodeId);
                const relatedNode = relatedNodeId ? nodes[relatedNodeId] || null : null;
                if (!relatedNodeId || !relatedNode) {
                    reasons.push("has an unresolved Connection");
                    continue;
                }
                if (anchors.has(relatedNodeId) || reachableNodeIds.has(relatedNodeId)) continue;
                reasons.push(`connected to surviving ${relatedNode.name || "content"}`);
            }

            const row = {
                id: entity?.id || node?.entityId || nodeId,
                nodeId,
                entityType: node?.entityType || "",
                typeLabel: adapter?.label || "Entity",
                pluralLabel: adapter?.plural || "Entities",
                name: displayName(node, entity),
                entity,
                reasons: uniqueReasons(reasons),
                componentId: ""
            };
            records.set(nodeId, row);
            if (row.reasons.length) blockedReasons.set(nodeId, row.reasons);
        }

        const activeNodeIds = new Set(
            [...reachableNodeIds].filter(nodeId => !blockedReasons.has(nodeId))
        );

        let changed = true;
        while (changed) {
            changed = false;
            for (const nodeId of [...activeNodeIds]) {
                const blockingNames = [];
                for (const edge of incidentEdges.get(nodeId) || []) {
                    const relatedNodeId = getRelatedNodeId(edge, nodeId);
                    if (!relatedNodeId || anchors.has(relatedNodeId) || activeNodeIds.has(relatedNodeId)) continue;
                    if (!reachableNodeIds.has(relatedNodeId)) continue;
                    blockingNames.push(records.get(relatedNodeId)?.name || nodes[relatedNodeId]?.name || "preserved entity");
                }
                if (!blockingNames.length) continue;
                activeNodeIds.delete(nodeId);
                blockedReasons.set(nodeId, uniqueReasons([
                    ...(blockedReasons.get(nodeId) || []),
                    ...blockingNames.map(name => `connected to preserved ${name}`)
                ]));
                changed = true;
            }
        }

        const components = [];
        const componentByNodeId = new Map();
        const unassigned = new Set(activeNodeIds);

        while (unassigned.size) {
            const first = unassigned.values().next().value;
            const memberNodeIds = [];
            const componentQueue = [first];
            unassigned.delete(first);

            while (componentQueue.length) {
                const nodeId = componentQueue.shift();
                memberNodeIds.push(nodeId);
                for (const edge of incidentEdges.get(nodeId) || []) {
                    const relatedNodeId = getRelatedNodeId(edge, nodeId);
                    if (!unassigned.has(relatedNodeId)) continue;
                    unassigned.delete(relatedNodeId);
                    componentQueue.push(relatedNodeId);
                }
            }

            memberNodeIds.sort();
            const componentId = memberNodeIds[0];
            const entityIds = memberNodeIds.map(nodeId => records.get(nodeId)?.id).filter(Boolean);
            const component = { id: componentId, nodeIds: memberNodeIds, entityIds };
            components.push(component);
            for (const nodeId of memberNodeIds) componentByNodeId.set(nodeId, componentId);
        }

        const candidates = [...activeNodeIds].map(nodeId => ({
            ...records.get(nodeId),
            reasons: [],
            componentId: componentByNodeId.get(nodeId) || nodeId
        }));
        const preserved = [...reachableNodeIds]
            .filter(nodeId => !activeNodeIds.has(nodeId))
            .map(nodeId => ({
                ...records.get(nodeId),
                reasons: uniqueReasons(blockedReasons.get(nodeId) || records.get(nodeId)?.reasons)
            }));
        const sortByTypeAndName = (left, right) =>
            left.entityType.localeCompare(right.entityType)
            || left.name.localeCompare(right.name, game.i18n.lang);

        candidates.sort(sortByTypeAndName);
        preserved.sort(sortByTypeAndName);
        components.sort((left, right) => left.id.localeCompare(right.id));
        return { candidates, preserved, components };
    }

    static normalizeSelection(impact = {}, selectedEntityIds = []) {
        const requested = new Set(selectedEntityIds.filter(Boolean));
        const selected = new Set();

        for (const component of impact.components || []) {
            if (!(component.entityIds || []).every(id => requested.has(id))) continue;
            for (const id of component.entityIds) selected.add(id);
        }
        return selected;
    }
}
