import { ConnectionCategories } from "../services/ConnectionCategories.js";
import { FactionMembershipModel } from "./FactionMembershipModel.js";
import { ConnectionOpinionModel } from "./ConnectionOpinionModel.js";
import { FactionSeatModel } from "./FactionSeatModel.js";
import { LocationLeadershipModel } from "./LocationLeadershipModel.js";
import { ShipCaptainModel } from "./ShipCaptainModel.js";
import { ShipLocationModel } from "./ShipLocationModel.js";
import { ShipOwnershipModel } from "./ShipOwnershipModel.js";

export class ConnectionGraphModel {
    static normalizeGraph(graph = {}) {
        const customCategories = ConnectionCategories.normalizeCustomCategories(graph?.customCategories || {});
        const nodes = {};
        const edges = {};

        for (const [key, node] of Object.entries(graph?.nodes || {})) {
            const normalized = this.normalizeNode({ id: key, ...node }, customCategories);
            if (normalized) nodes[normalized.id] = normalized;
        }

        for (const [key, edge] of Object.entries(graph?.edges || {})) {
            const normalized = this.normalizeEdge({ id: key, ...edge }, nodes, customCategories);
            if (normalized) edges[normalized.id] = normalized;
        }

        return {
            ...graph,
            version: Number(graph?.version || 1),
            customCategories,
            nodes,
            edges
        };
    }

    static normalizeNode(node = {}, customCategories = {}) {
        const id = String(node.id || "").trim();
        const kind = ["nexus-site", "nexus-scene", "nexus-entity"].includes(node.kind) ? node.kind : "foundry-document";
        if (!id) return null;

        if (kind === "nexus-site" && (!node.parentSceneId || !node.siteId)) return null;
        if (kind === "nexus-scene" && !node.sceneId) return null;
        if (kind === "nexus-entity" && (!node.entityId || !node.entityType)) return null;
        if (kind === "foundry-document" && !node.uuid) return null;

        const normalized = {
            ...node,
            id,
            kind,
            uuid: ["foundry-document", "nexus-entity"].includes(kind) ? String(node.uuid || "") : "",
            documentType: kind === "foundry-document" ? String(node.documentType || "") : "",
            parentSceneId: kind === "nexus-site" ? String(node.parentSceneId || "") : null,
            siteId: kind === "nexus-site" ? String(node.siteId || "") : null,
            sceneId: kind === "nexus-scene" ? String(node.sceneId || "") : null,
            entityId: kind === "nexus-entity" ? String(node.entityId || "") : null,
            entityType: kind === "nexus-entity" ? String(node.entityType || "") : null,
            name: String(node.name || "Unknown"),
            img: String(node.img || ""),
            category: ConnectionCategories.normalize(node.category, kind === "nexus-site" || kind === "nexus-scene" ? "place" : "npc", customCategories),
            groupOrder: this.#normalizeGroupOrder(node.groupOrder, customCategories),
            hiddenCategoryIds: this.#normalizeGroupOrder(node.hiddenCategoryIds, customCategories),
            createdTime: Number(node.createdTime || Date.now()),
            updatedTime: Number(node.updatedTime || node.createdTime || Date.now())
        };

        delete normalized.sourceCoverSrc;
        delete normalized.sourceCoverLabel;
        delete normalized.suppressActions;
        delete normalized.actionMode;
        delete normalized.providerId;
        if (!normalized.groupOrder.length) delete normalized.groupOrder;
        if (!normalized.hiddenCategoryIds.length) delete normalized.hiddenCategoryIds;
        return normalized;
    }

    static normalizeEdge(edge = {}, nodes = {}, customCategories = {}) {
        const id = String(edge.id || "").trim();
        const sourceNodeId = String(edge.sourceNodeId || "").trim();
        const targetNodeId = String(edge.targetNodeId || "").trim();
        if (!id || !sourceNodeId || !targetNodeId || sourceNodeId === targetNodeId) return null;
        if (nodes && (!nodes[sourceNodeId] || !nodes[targetNodeId])) return null;

        const category = ConnectionCategories.normalize(edge.category, nodes?.[targetNodeId]?.category || "npc", customCategories);
        const normalized = {
            ...edge,
            id,
            sourceNodeId,
            targetNodeId,
            relationType: this.#normalizeRelationType(edge.relationType),
            resourceFlows: this.#normalizeResourceFlows(edge.resourceFlows, { sourceNodeId, targetNodeId }),
            category,
            role: String(edge.role || ConnectionCategories.roleFor(category)),
            note: String(edge.note || ""),
            playerVisibility: ["inherit", "show", "hide"].includes(edge.playerVisibility) ? edge.playerVisibility : "inherit",
            sort: Number(edge.sort || 100),
            createdTime: Number(edge.createdTime || Date.now()),
            updatedTime: Number(edge.updatedTime || edge.createdTime || Date.now())
        };
        normalized.views = this.#normalizeEdgeViews(edge.views, { sourceNodeId, targetNodeId, category }, customCategories);
        normalized.opinions = ConnectionOpinionModel.canTrack(nodes?.[sourceNodeId], nodes?.[targetNodeId])
            ? ConnectionOpinionModel.normalize(edge.opinions, { sourceNodeId, targetNodeId })
            : null;
        normalized.factionMembership = FactionMembershipModel.normalize(edge.factionMembership, {
            sourceNode: nodes?.[sourceNodeId],
            targetNode: nodes?.[targetNodeId]
        });
        normalized.factionSeat = FactionSeatModel.normalize(edge.factionSeat, {
            sourceNode: nodes?.[sourceNodeId],
            targetNode: nodes?.[targetNodeId],
            relationType: normalized.relationType
        });
        normalized.locationLeadership = LocationLeadershipModel.normalize(edge.locationLeadership, {
            sourceNode: nodes?.[sourceNodeId],
            targetNode: nodes?.[targetNodeId]
        });
        normalized.shipCaptain = ShipCaptainModel.normalize(edge.shipCaptain, {
            sourceNode: nodes?.[sourceNodeId],
            targetNode: nodes?.[targetNodeId]
        });
        normalized.shipOwnership = ShipOwnershipModel.normalize(edge.shipOwnership, {
            sourceNode: nodes?.[sourceNodeId],
            targetNode: nodes?.[targetNodeId]
        });
        normalized.shipLocation = ShipLocationModel.normalize(edge.shipLocation, {
            sourceNode: nodes?.[sourceNodeId],
            targetNode: nodes?.[targetNodeId]
        });
        if (!Object.keys(normalized.views).length) delete normalized.views;
        if (!normalized.resourceFlows.length) delete normalized.resourceFlows;
        if (!normalized.opinions) delete normalized.opinions;
        if (!normalized.factionMembership) delete normalized.factionMembership;
        if (!normalized.factionSeat) delete normalized.factionSeat;
        if (!normalized.locationLeadership) delete normalized.locationLeadership;
        if (!normalized.shipCaptain) delete normalized.shipCaptain;
        if (!normalized.shipOwnership) delete normalized.shipOwnership;
        if (!normalized.shipLocation) delete normalized.shipLocation;
        return normalized;
    }

    static #normalizeResourceFlows(flows = [], { sourceNodeId = "", targetNodeId = "" } = {}) {
        const endpointIds = new Set([sourceNodeId, targetNodeId]);
        const normalized = [];
        const seen = new Set();
        for (const flow of Array.isArray(flows) ? flows : []) {
            const providerId = String(flow?.providerId || "").trim();
            const resourceId = String(flow?.resourceId || "").trim();
            const fromNodeId = String(flow?.fromNodeId || "").trim();
            const toNodeId = String(flow?.toNodeId || "").trim();
            if (!providerId || !resourceId || fromNodeId === toNodeId) continue;
            if (!endpointIds.has(fromNodeId) || !endpointIds.has(toNodeId)) continue;
            const key = `${providerId}:${resourceId}:${fromNodeId}:${toNodeId}`;
            if (seen.has(key)) continue;
            seen.add(key);
            // Labels and images are fallback presentation snapshots; the provider/resource pair remains authoritative.
            normalized.push({
                providerId,
                resourceId,
                fromNodeId,
                toNodeId,
                label: String(flow?.label || resourceId).trim() || resourceId,
                imageSrc: String(flow?.imageSrc || "").trim()
            });
        }
        return normalized;
    }

    static #normalizeEdgeViews(views = {}, edge = {}, customCategories = {}) {
        const normalized = {};
        const allowedNodeIds = new Set([edge.sourceNodeId, edge.targetNodeId].filter(Boolean));
        for (const [nodeId, view] of Object.entries(views || {})) {
            if (!allowedNodeIds.has(nodeId) || !view || typeof view !== "object") continue;
            const normalizedView = this.#normalizeEdgeView(view, edge.category, customCategories);
            if (Object.keys(normalizedView).length) normalized[nodeId] = normalizedView;
        }
        return normalized;
    }

    static #normalizeEdgeView(view = {}, fallbackCategory = "npc", customCategories = {}) {
        const normalized = {};
        if (Object.hasOwn(view, "category")) {
            normalized.category = ConnectionCategories.normalize(view.category, fallbackCategory, customCategories);
        }
        if (Object.hasOwn(view, "role")) normalized.role = String(view.role || "");
        if (Object.hasOwn(view, "note")) normalized.note = String(view.note || "");
        if (Object.hasOwn(view, "sort")) {
            const sort = Number(view.sort);
            if (Number.isFinite(sort)) normalized.sort = sort;
        }
        return normalized;
    }

    static #normalizeRelationType(value) {
        return String(value || "")
            .trim()
            .toLocaleLowerCase()
            .replace(/[^a-z0-9:.-]+/g, "-")
            .replace(/^-+|-+$/g, "");
    }

    static #normalizeGroupOrder(groupOrder = [], customCategories = {}) {
        const knownCategoryIds = new Set(ConnectionCategories.editable(customCategories).map(category => category.id));
        const normalized = [];
        for (const categoryId of Array.isArray(groupOrder) ? groupOrder : []) {
            const id = String(categoryId || "").trim();
            if (!id || !knownCategoryIds.has(id) || normalized.includes(id)) continue;
            normalized.push(id);
        }
        return normalized;
    }
}
