import { openActionMenu, promptTextInput } from "../../../api/ui.js";
import { ConnectionCategoryPicker } from "../applications/ConnectionCategoryPicker.js";
import { ConnectionOpinionEditor } from "../applications/ConnectionOpinionEditor.js";
import { ConnectionCategories } from "../services/ConnectionCategories.js";
import { ConnectionCategoryPresets } from "../services/ConnectionCategoryPresets.js";
import { ConnectionDropResolver } from "../services/ConnectionDropResolver.js";
import { ConnectionStore } from "../services/ConnectionStore.js";
import { ConnectionTargetResolver } from "../services/ConnectionTargetResolver.js";
import { PlayerConnectionVisibilityDialog } from "../../nexus/applications/PlayerConnectionVisibilityDialog.js";
import { PlayerVisibilityInfoPanel } from "../../nexus/applications/PlayerVisibilityInfoPanel.js";
import { OwnershipService } from "../services/OwnershipService.js";
import { ConnectionResourceFlowService } from "../services/ConnectionResourceFlowService.js";
import { TradingCenterService } from "../services/TradingCenterService.js";
import { ConnectionOpinionService } from "../services/ConnectionOpinionService.js";
import { FactionMembershipService } from "../services/FactionMembershipService.js";
import { FactionSeatService } from "../services/FactionSeatService.js";
import { LocationLeadershipService } from "../services/LocationLeadershipService.js";

export class ConnectionsBoard {
    #target = null;
    #isGM = false;
    #activeCategory = "all";

    constructor({ target, isGM = game.user.isGM, activeCategory = "all" } = {}) {
        this.#target = target;
        this.#isGM = isGM;
        this.#activeCategory = ConnectionCategories.normalize(activeCategory);
    }

    get activeCategory() {
        return this.#activeCategory;
    }

    setTarget(target) {
        this.#target = target;
    }

    setCategory(categoryId) {
        this.#activeCategory = ConnectionCategories.normalize(categoryId, "all", ConnectionStore.getCustomCategories());
    }

    getContext() {
        const target = this.#target;
        const nodeId = ConnectionTargetResolver.getNodeId(target);
        const customCategories = ConnectionStore.getCustomCategories();
        // Trading-center edges are infrastructure for the dedicated trade tools. Flattening
        // them into ordinary connection cards obscures the route hierarchy and duplicates
        // the Regional Trade Network interface.
        const connections = ConnectionStore.getConnectionsForNode(nodeId)
            .filter(({ edge }) => edge?.relationType !== TradingCenterService.RELATION_TYPE)
            .filter(({ edge }) => !Object.values(edge?.views || {})
                .some(view => view?.category === "trade-routes"));
        const allCards = connections.map(({ edge, node }) => this.#buildCard(edge, node, nodeId, customCategories))
            .sort((left, right) => Number(right.isFactionLeader) - Number(left.isFactionLeader)
                || Number(right.isFactionSeat) - Number(left.isFactionSeat)
                || Number(right.isLocationLeader) - Number(left.isLocationLeader));

        const editableCategories = this.#orderCategories(ConnectionCategories.editable(customCategories), nodeId, target);
        const allGroups = editableCategories
            .map(category => {
                const groupCards = allCards.filter(card => card.categoryId === category.id);
                const hiddenForPlayers = ConnectionStore.isCategoryGroupHiddenForNode(nodeId, category.id);
                const playerVisible = ConnectionStore.canUserSeeCategoryGroup(nodeId, category.id, game.user);
                return {
                    ...category,
                    hiddenForPlayers,
                    playerVisible,
                    cards: groupCards
                };
            })
            .filter(group => group.cards.length > 0)
            .filter(group => this.#isGM || group.playerVisible);

        const categoryIdsWithCards = new Set(allGroups.map(group => group.id));
        if (this.#activeCategory !== "all" && !categoryIdsWithCards.has(this.#activeCategory)) {
            this.#activeCategory = "all";
        }

        const visibleGroups = allGroups.filter(group => this.#activeCategory === "all" || group.id === this.#activeCategory);
        const visibleCards = visibleGroups.flatMap(group => group.cards);
        const orderedCategories = [
            ConnectionCategories.get("all", customCategories),
            ...editableCategories
        ];
        const categories = orderedCategories
            .filter(category => category.id === "all" || categoryIdsWithCards.has(category.id))
            .map(category => ({
                ...category,
                active: category.id === this.#activeCategory,
                count: category.id === "all"
                    ? allGroups.reduce((sum, group) => sum + group.cards.length, 0)
                    : allGroups.find(group => group.id === category.id)?.cards.length || 0
            }));
        const activeCategory = categories.find(category => category.active) || categories[0] || ConnectionCategories.get("all", customCategories);
        const visibleCategoryIds = visibleGroups.map(group => group.id);

        return {
            target: this.#buildTargetContext(target),
            categories,
            activeCategory: this.#activeCategory,
            activeCategoryIcon: activeCategory?.icon || "fas fa-share-nodes",
            hasConnections: visibleCards.length > 0,
            isGM: this.#isGM,
            groups: visibleGroups.map(group => {
                const visibleIndex = visibleCategoryIds.indexOf(group.id);
                return {
                    ...group,
                    visible: true,
                    canMoveUp: visibleIndex > 0,
                    canMoveDown: visibleIndex >= 0 && visibleIndex < visibleCategoryIds.length - 1
                };
            })
        };
    }

    activateListeners(rootElement, hostApplication) {
        const board = rootElement?.querySelector?.("[data-connections-board]");
        if (!board) {
            if (this.#isGM) this.#activateExternalDropSurfaces(rootElement, hostApplication);
            return;
        }

        board.querySelectorAll("[data-connection-category]").forEach(button => {
            button.addEventListener("click", event => {
                event.preventDefault();
                this.#activeCategory = ConnectionCategories.normalize(button.dataset.connectionCategory, "all", ConnectionStore.getCustomCategories());
                hostApplication.render({ parts: ["content"] });
            });
        });

        board.querySelectorAll("[data-connection-category-select]").forEach(select => {
            select.addEventListener("change", event => {
                this.#activeCategory = ConnectionCategories.normalize(event.currentTarget.value, "all", ConnectionStore.getCustomCategories());
                hostApplication.render({ parts: ["content"] });
            });
        });

        board.querySelectorAll("[data-connection-group-actions]").forEach(button => {
            button.addEventListener("click", event => {
                event.preventDefault();
                event.stopPropagation();
                this.#openGroupActions(button, hostApplication);
            });
        });

        board.querySelectorAll("[data-connection-open]").forEach(button => {
            button.addEventListener("click", async event => {
                event.preventDefault();
                const node = ConnectionStore.getNode(button.dataset.connectionNodeId);
                await ConnectionTargetResolver.openNode(node);
            });
        });
        board.querySelectorAll("[data-connection-opinion]").forEach(button => {
            button.addEventListener("click", event => {
                event.preventDefault();
                event.stopPropagation();
                this.openOpinionSummary(button, hostApplication);
            });
        });
        board.querySelectorAll("[data-connection-actions]").forEach(button => {
            button.addEventListener("click", event => {
                event.preventDefault();
                this.#openCardActions(button, hostApplication);
            });
        });

        board.querySelectorAll("[data-connection-group-move]").forEach(button => {
            button.addEventListener("click", async event => {
                event.preventDefault();
                event.stopPropagation();
                button.blur();
                const categoryId = button.dataset.connectionCategoryId || "";
                const direction = Number(button.dataset.connectionGroupMove || 0);
                const categoryIds = [...board.querySelectorAll(".connections-group[data-connection-category-id]")]
                    .map(group => group.dataset.connectionCategoryId || "")
                    .filter(Boolean);
                const moved = await ConnectionStore.moveGroupForNode(ConnectionTargetResolver.getNodeId(this.#target), categoryId, direction, { categoryIds });
                if (moved) hostApplication.render({ parts: ["content"] });
            });
        });

        board.querySelectorAll("[data-connection-drag]").forEach(card => {
            card.addEventListener("dragstart", event => {
                this.#setConnectionRowDragData(event, card);
                const node = ConnectionStore.getNode(card.dataset.connectionNodeId || "");
                ConnectionTargetResolver.setDragData(event.dataTransfer, node, { effectAllowed: "copyMove" });
            });
        });

        if (!this.#isGM) return;
        board.querySelectorAll(".connection-card").forEach(card => {
            if (!card.hasAttribute("data-connection-drag")) {
                card.addEventListener("dragstart", event => this.#setConnectionRowDragData(event, card));
            }

            card.addEventListener("dragover", event => {
                event.preventDefault();
                event.stopPropagation();
                card.classList.add("is-drop-before");
            });

            card.addEventListener("dragleave", event => {
                if (card.contains(event.relatedTarget)) return;
                card.classList.remove("is-drop-before");
            });

            card.addEventListener("drop", async event => {
                event.preventDefault();
                event.stopPropagation();
                card.classList.remove("is-drop-before");
                await this.#handleDrop(event, hostApplication, card);
            });
        });

        board.querySelectorAll("[data-connection-category-id]").forEach(group => {
            group.addEventListener("dragover", event => {
                event.preventDefault();
                event.stopPropagation();
                group.classList.add("is-category-drop-target");
            });

            group.addEventListener("dragleave", event => {
                if (group.contains(event.relatedTarget)) return;
                group.classList.remove("is-category-drop-target");
            });

            group.addEventListener("drop", async event => {
                event.preventDefault();
                event.stopPropagation();
                group.classList.remove("is-category-drop-target");
                await this.#handleDrop(event, hostApplication, group);
            });
        });

        board.addEventListener("dragover", event => {
            event.preventDefault();
            board.classList.add("is-drop-target");
        });
        board.addEventListener("dragleave", event => {
            if (board.contains(event.relatedTarget)) return;
            board.classList.remove("is-drop-target");
        });
        board.addEventListener("drop", async event => {
            event.preventDefault();
            board.classList.remove("is-drop-target");
            await this.#handleDrop(event, hostApplication);
        });

        this.#activateExternalDropSurfaces(rootElement, hostApplication, board);
    }

    #activateExternalDropSurfaces(rootElement, hostApplication, board = null) {
        rootElement?.querySelectorAll?.("[data-connections-drop-surface]").forEach(surface => {
            if (surface === board || surface.dataset.connectionsDropBound) return;
            surface.dataset.connectionsDropBound = "true";

            surface.addEventListener("dragover", event => {
                event.preventDefault();
                surface.classList.add("is-connection-drop-target");
            });

            surface.addEventListener("dragleave", event => {
                if (surface.contains(event.relatedTarget)) return;
                surface.classList.remove("is-connection-drop-target");
            });

            surface.addEventListener("drop", async event => {
                event.preventDefault();
                surface.classList.remove("is-connection-drop-target");
                const targetElement = event.target instanceof Element ? event.target : event.target?.parentElement;
                if (targetElement?.closest("[data-connections-board]")) return;
                await this.#handleDrop(event, hostApplication);
            });
        });
    }

    #buildTargetContext(target) {
        return {
            ...target,
            color: target?.siteColor || "#ffffff",
            icon: target?.img || "",
            name: target?.name || "Site",
            subtitle: target?.subtitle || ""
        };
    }

    #orderCategories(categories = [], nodeId = "", target = null) {
        const categoryIds = new Set(categories.map(category => category.id));
        const order = ConnectionStore.getGroupOrderForNode(nodeId).filter(id => categoryIds.has(id));
        return ConnectionCategoryPresets.orderCategories(categories, { target, order });
    }

    #buildCard(edge, node, currentNodeId = "", customCategories = {}) {
        const displayNode = ConnectionTargetResolver.resolveDisplayNodeSync(node) || { id: "", name: "Missing connection", missing: true };
        const view = ConnectionStore.getConnectionView(edge, currentNodeId, customCategories);
        const categoryId = ConnectionCategories.normalize(view.category || displayNode.category, "npc", customCategories);
        const category = ConnectionCategories.get(categoryId, customCategories);
        const resourceFlow = ConnectionResourceFlowService.getPerspective(edge, currentNodeId);
        const resourceFlowRows = [
            resourceFlow.sends.length ? { label: "Sends", icon: "fas fa-arrow-up-right-dots", resources: resourceFlow.sends } : null,
            resourceFlow.receives.length ? { label: "Receives", icon: "fas fa-arrow-down-left", resources: resourceFlow.receives } : null
        ].filter(Boolean);
        const opinion = ConnectionOpinionService.resolvePerspective(edge, currentNodeId);
        const membership = FactionMembershipService.getMembership(edge);
        const leadershipPresentation = LocationLeadershipService.getPresentationForTargets(this.#target, displayNode);
        const isLocationLeader = LocationLeadershipService.isLeadershipEdge(edge);
        const leadershipSubtitle = edge.locationLeadership?.leaderNodeId === currentNodeId
            ? `${leadershipPresentation.relationshipRole} ${displayNode.name || "Location"}`
            : leadershipPresentation.title;
        return {
            edgeId: edge.id,
            nodeId: displayNode.id,
            name: displayNode.name || "Unknown",
            subtitle: resourceFlow.hasFlows ? "" : isLocationLeader
                ? leadershipSubtitle
                : (displayNode.embeddedLabel || view.role || category.singular),
            img: displayNode.img || "",
            icon: category.icon,
            categoryId,
            color: category.color,
            customCategory: category.custom === true,
            draggable: !!ConnectionTargetResolver.getDragData(displayNode),
            dragType: "node",
            uuid: displayNode.uuid || "",
            note: String(view.note || "").trim(),
            missing: !!displayNode.missing,
            playerVisibilityLabel: ConnectionStore.getConnectionPlayerVisibilityLabel(edge),
            playerVisibilityIcon: ConnectionStore.getConnectionPlayerVisibilityIcon(edge),
            isPlayerHidden: !ConnectionStore.isConnectionVisibleToPlayers(edge),
            isOwnership: edge.relationType === OwnershipService.RELATION_TYPE,
            ownershipLabel: edge.targetNodeId === currentNodeId ? "Owner" : "Holding",
            hasResourceFlows: resourceFlow.hasFlows,
            resourceFlowRows,
            isFactionMember: !!membership,
            isFactionLeader: membership?.isLeader === true,
            factionMembershipLabel: membership?.isLeader ? "Faction Leader" : "Faction Member",
            isFactionSeat: FactionSeatService.isSeatEdge(edge),
            factionSeatLabel: edge.factionSeat?.factionNodeId === currentNodeId ? "Seat of Power" : "Faction Seat",
            isLocationLeader,
            locationLeadershipLabel: edge.locationLeadership?.leaderNodeId === currentNodeId
                ? leadershipPresentation.activeLabel
                : leadershipPresentation.npcRole,
            hasOpinion: !!opinion,
            opinion: opinion ? {
                scoreLabel: opinion.scoreLabel,
                tierLabel: opinion.tier.label,
                tierColor: opinion.tier.color
            } : null
        };
    }

    openOpinionSummary(button, hostApplication) {
        const edgeId = button.dataset.connectionEdgeId || "";
        const currentNodeId = ConnectionTargetResolver.getNodeId(this.#target);
        const opinion = ConnectionOpinionService.resolvePerspective(edgeId, currentNodeId);
        if (!opinion) return;

        const items = [{
            id: "opinion-summary-heading",
            label: `${opinion.observer.name} on ${opinion.subject.name}`,
            status: `${opinion.scoreLabel} ${opinion.tier.label}`,
            icon: "fas fa-scale-balanced",
            className: "connection-opinion-summary-heading",
            unavailable: true
        }, {
            id: "opinion-summary-base",
            label: "Base Opinion",
            status: this.#formatOpinionValue(opinion.base),
            icon: "fas fa-anchor",
            className: "connection-opinion-summary-contribution",
            unavailable: true
        }];

        if (opinion.adjustments.length) {
            items.push({
                id: "opinion-summary-adjustments",
                label: "Persistent Adjustments",
                icon: "fas fa-clock-rotate-left",
                className: "connection-opinion-summary-group",
                unavailable: true
            });
            for (const adjustment of opinion.adjustments) {
                items.push({
                    id: `opinion-adjustment-${adjustment.id}`,
                    label: adjustment.label,
                    status: this.#formatOpinionValue(adjustment.value),
                    icon: "fas fa-circle",
                    className: "connection-opinion-summary-contribution is-detail",
                    unavailable: true
                });
            }
        }

        for (const influence of opinion.automaticInfluences) {
            items.push({
                id: `opinion-influence-${influence.id}`,
                label: influence.label,
                status: this.#formatOpinionValue(influence.value),
                icon: "fas fa-wand-magic-sparkles",
                className: "connection-opinion-summary-group",
                unavailable: true
            });
            const detailTotal = influence.details.reduce((sum, detail) => sum + detail.value, 0);
            for (const detail of influence.details) {
                items.push({
                    id: `opinion-influence-detail-${influence.id}-${detail.id}`,
                    label: detail.label,
                    status: this.#formatOpinionValue(detail.value),
                    icon: "fas fa-circle",
                    className: "connection-opinion-summary-contribution is-detail",
                    unavailable: true
                });
            }
            const compatibilityLimit = influence.value - detailTotal;
            if (compatibilityLimit) {
                items.push({
                    id: `opinion-influence-limit-${influence.id}`,
                    label: "Compatibility limit",
                    status: this.#formatOpinionValue(compatibilityLimit),
                    icon: "fas fa-gauge-high",
                    className: "connection-opinion-summary-contribution is-detail",
                    unavailable: true
                });
            }
        }

        const rawTotal = opinion.base + opinion.adjustmentTotal + opinion.automaticTotal;
        const scoreLimit = opinion.score - rawTotal;
        if (scoreLimit) {
            items.push({
                id: "opinion-score-limit",
                label: "Opinion limit",
                status: this.#formatOpinionValue(scoreLimit),
                icon: "fas fa-gauge-high",
                className: "connection-opinion-summary-contribution",
                unavailable: true
            });
        }
        items.push({
            id: "opinion-summary-total",
            label: "Total Opinion",
            status: opinion.scoreLabel,
            icon: "fas fa-equals",
            className: "connection-opinion-summary-total",
            unavailable: true
        });

        if (this.#isGM) {
            items.push({
                id: "manage-opinions",
                label: "Manage Opinions...",
                icon: "fas fa-sliders",
                className: "connection-opinion-summary-manage",
                onSelect: () => ConnectionOpinionEditor.show({
                    edgeId,
                    observerNodeId: currentNodeId,
                    onChange: () => hostApplication.render({ parts: ["content"] })
                })
            });
        }

        openActionMenu({
            anchor: button,
            className: "connection-opinion-summary-menu",
            cssVariables: { "--opinion-tier-color": opinion.tier.color },
            items
        });
    }

    #formatOpinionValue(value) {
        const number = Number(value || 0);
        return number > 0 ? `+${number}` : String(number);
    }

    #setConnectionRowDragData(event, card) {
        const edgeId = card.dataset.connectionEdgeId || "";
        if (!edgeId || !event.dataTransfer) return;
        event.dataTransfer.setData("application/x-augur-connection-row", JSON.stringify({
            edgeId,
            sourceNodeId: ConnectionTargetResolver.getNodeId(this.#target),
            categoryId: card.dataset.connectionCategoryId || ""
        }));
    }

    async connectFromDrop(event, { categoryId = "" } = {}) {
        const relatedTarget = await ConnectionDropResolver.fromEvent(event);
        if (!relatedTarget) {
            ui.notifications.warn("Drop an Actor, Item, Journal, Journal Page, or Nexus place to connect it.");
            return null;
        }
        const customCategories = ConnectionStore.getCustomCategories();
        const resolvedCategoryId = categoryId
            || (this.#activeCategory !== "all" ? this.#activeCategory : "")
            || ConnectionCategoryPresets.defaultCategoryForConnection(this.#target, relatedTarget, customCategories);
        const category = resolvedCategoryId ? ConnectionCategories.get(resolvedCategoryId, customCategories) : null;
        return ConnectionStore.addConnection(this.#target, relatedTarget, category ? {
            category: category.id,
            role: category.singular
        } : {});
    }

    async #handleDrop(event, hostApplication, targetElement = null) {
        const handledInternalDrop = await this.#handleInternalConnectionDrop(event, hostApplication, targetElement);
        if (handledInternalDrop) return;

        const targetCategoryId = targetElement?.closest?.("[data-connection-category-id]")?.dataset?.connectionCategoryId || "";
        const edge = await this.connectFromDrop(event, { categoryId: targetCategoryId });
        if (!edge) return;
        hostApplication.render({ parts: ["content"] });
    }

    async #handleInternalConnectionDrop(event, hostApplication, targetElement = null) {
        const raw = event.dataTransfer?.getData("application/x-augur-connection-row") || "";
        if (!raw) return false;

        try {
            const payload = JSON.parse(raw);
            if (!payload?.edgeId) return true;

            const currentNodeId = ConnectionTargetResolver.getNodeId(this.#target);
            if (payload.sourceNodeId && payload.sourceNodeId !== currentNodeId) return false;

            const targetCategoryId = targetElement?.closest?.("[data-connection-category-id]")?.dataset?.connectionCategoryId || "";
            if (targetCategoryId && targetCategoryId !== payload.categoryId) {
                const category = ConnectionCategories.get(targetCategoryId, ConnectionStore.getCustomCategories());
                await ConnectionStore.setConnectionViewForNode(payload.edgeId, currentNodeId, { category: category.id });
            }

            const beforeEdgeId = targetElement?.closest?.(".connection-card")?.dataset?.connectionEdgeId || null;
            const sourceNodeId = payload.sourceNodeId || currentNodeId;
            if (sourceNodeId) await ConnectionStore.reorderConnectionForNode(payload.edgeId, sourceNodeId, beforeEdgeId);
            hostApplication.render({ parts: ["content"] });
        } catch (_err) {
            ui.notifications.warn("Could not rearrange that connection.");
        }

        return true;
    }

    #openCardActions(button, hostApplication) {
        if (!this.#isGM) return;
        const edgeId = button.dataset.connectionEdgeId;
        if (!edgeId) return;
        const currentNodeId = ConnectionTargetResolver.getNodeId(this.#target);
        const edge = ConnectionStore.getConnectionEdge(edgeId);
        const hasNote = !!String(ConnectionStore.getConnectionNoteForNode(edge, currentNodeId) || "").trim();
        const relatedNodeId = edge?.sourceNodeId === currentNodeId ? edge.targetNodeId : edge?.sourceNodeId;
        const relatedNode = ConnectionStore.getNode(relatedNodeId);
        const canManageOwnership = OwnershipService.canSetOwner(this.#target, relatedNode);
        const isCurrentOwner = OwnershipService.isOwnershipEdge(edge, this.#target);
        const canManageTradingCenter = TradingCenterService.canSetTradingCenter(this.#target, relatedNode);
        const isCurrentTradingCenter = TradingCenterService.isTradingCenterEdge(edge, this.#target);
        const canTrackOpinions = ConnectionOpinionService.canTrack(edge);
        const tracksOpinions = ConnectionOpinionService.isTracked(edge);
        const canManageMembership = FactionMembershipService.canSetMembership(this.#target, relatedNode);
        const membership = FactionMembershipService.getMembership(edge);
        const canManageSeat = FactionSeatService.canSetSeat(this.#target, relatedNode);
        const isCurrentSeat = FactionSeatService.isSeatEdge(edge);
        const canManageLeadership = LocationLeadershipService.canSetLeader(this.#target, relatedNode);
        const isCurrentLocationLeader = LocationLeadershipService.isLeadershipEdge(edge);
        const leadershipPresentation = LocationLeadershipService.getPresentationForTargets(this.#target, relatedNode);

        openActionMenu({
            anchor: button,
            className: "connections-card-actions-menu",
            items: [
                {
                    id: "player-visibility",
                    label: `Visible To Players: ${ConnectionStore.getConnectionPlayerVisibilityLabel(edge)}`,
                    icon: ConnectionStore.getConnectionPlayerVisibilityIcon(edge),
                    onSelect: () => this.#openPlayerVisibilityMenu(button, edgeId, hostApplication)
                },
                {
                    id: "edit-note",
                    label: hasNote ? "Edit Note..." : "Add Note...",
                    icon: "fas fa-note-sticky",
                    onSelect: () => this.#editNote(edgeId, currentNodeId, hostApplication)
                },
                {
                    id: "change-role",
                    label: "Edit Role...",
                    icon: "fas fa-pen-to-square",
                    onSelect: () => this.#editRole(edgeId, currentNodeId, hostApplication)
                },
                {
                    id: "change-category",
                    label: "Change Category...",
                    icon: "fas fa-tags",
                    onSelect: () => this.#openCategoryPicker(edgeId, currentNodeId, hostApplication)
                },
                ...(canManageMembership ? [{
                    id: membership?.isLeader ? "change-to-faction-member" : membership ? "make-faction-leader" : "set-faction-member",
                    label: membership?.isLeader ? "Change to Faction Member" : membership ? "Make Faction Leader" : "Set as Faction Member",
                    icon: membership?.isLeader ? "fas fa-user-shield" : membership ? "fas fa-crown" : "fas fa-user-shield",
                    onSelect: () => this.#setConnectionMembership({
                        edgeId,
                        relatedNode,
                        tier: membership?.isLeader ? "member" : membership ? "leader" : "member",
                        hostApplication
                    })
                }, ...(!membership ? [{
                    id: "set-faction-leader",
                    label: "Set as Faction Leader",
                    icon: "fas fa-crown",
                    onSelect: () => this.#setConnectionMembership({
                        edgeId,
                        relatedNode,
                        tier: "leader",
                        hostApplication
                    })
                }] : []), ...(membership ? [{
                    id: "remove-faction-membership",
                    label: "Remove Faction Membership",
                    icon: "fas fa-user-slash",
                    danger: true,
                    onSelect: () => this.#setConnectionMembership({
                        edgeId,
                        relatedNode,
                        clear: true,
                        hostApplication
                    })
                }] : [])] : []),
                ...(canManageSeat ? [{
                    id: isCurrentSeat ? "clear-faction-seat" : "set-faction-seat",
                    label: isCurrentSeat ? "Remove as Faction Seat" : "Make Faction Seat",
                    icon: "fas fa-landmark-dome",
                    onSelect: () => this.#setConnectionSeat({
                        edgeId, relatedNode, clear: isCurrentSeat, hostApplication
                    })
                }] : []),
                ...(canManageLeadership ? [{
                    id: isCurrentLocationLeader ? "clear-location-leader" : "set-location-leader",
                    label: isCurrentLocationLeader ? leadershipPresentation.removeLabel : leadershipPresentation.appointLabel,
                    icon: "fas fa-key",
                    onSelect: () => this.#setConnectionLeadership({
                        edgeId, relatedNode, clear: isCurrentLocationLeader, hostApplication
                    })
                }] : []),
                ...(canTrackOpinions ? [{
                    id: tracksOpinions ? "manage-opinions" : "track-opinions",
                    label: tracksOpinions ? "Manage Opinions..." : "Track Opinions",
                    icon: tracksOpinions ? "fas fa-scale-balanced" : "fas fa-eye",
                    onSelect: async () => {
                        if (!tracksOpinions) {
                            await ConnectionOpinionService.enable(edgeId);
                            hostApplication.render({ parts: ["content"] });
                            return;
                        }
                        ConnectionOpinionEditor.show({
                            edgeId,
                            observerNodeId: currentNodeId,
                            onChange: () => hostApplication.render({ parts: ["content"] })
                        });
                    }
                }, ...(tracksOpinions ? [{
                    id: "stop-tracking-opinions",
                    label: "Stop Tracking Opinions",
                    icon: "fas fa-eye-slash",
                    danger: true,
                    onSelect: async () => {
                        await ConnectionOpinionService.disable(edgeId);
                        hostApplication.render({ parts: ["content"] });
                    }
                }] : [])] : []),
                ...(canManageOwnership ? [{
                    id: isCurrentOwner ? "clear-owner" : "set-owner",
                    label: isCurrentOwner ? "Remove as Owner" : "Set as Owner",
                    icon: "fas fa-crown",
                    onSelect: () => this.#setConnectionOwnership({
                        edgeId,
                        relatedNode,
                        clear: isCurrentOwner,
                        hostApplication
                    })
                }] : []),
                ...(canManageTradingCenter ? [{
                    id: isCurrentTradingCenter ? "clear-trading-center" : "set-trading-center",
                    label: isCurrentTradingCenter ? "Remove Trading Center" : "Set as Trading Center",
                    icon: "fas fa-signs-post",
                    onSelect: () => this.#setConnectionTradingCenter({
                        edgeId,
                        relatedNode,
                        clear: isCurrentTradingCenter,
                        hostApplication
                    })
                }] : []),
                {
                    id: "remove-connection",
                    label: "Remove Connection",
                    icon: "fas fa-trash",
                    danger: true,
                    onSelect: async () => {
                        await ConnectionStore.removeConnection(edgeId);
                        hostApplication.render({ parts: ["content"] });
                    }
                }
            ]
        });
    }

    async #setConnectionOwnership({ edgeId, relatedNode, clear = false, hostApplication } = {}) {
        try {
            if (clear) {
                await OwnershipService.clearOwner(this.#target, { edgeId });
                ui.notifications.info("Place ownership removed.");
            } else {
                await OwnershipService.setOwner(this.#target, relatedNode, { edgeId });
                ui.notifications.info(`${relatedNode?.name || "That entity"} is now the Place owner.`);
            }
            hostApplication.render({ parts: ["content"] });
        } catch (error) {
            console.error("Augur: Nexus | Failed to update Place ownership.", error);
            ui.notifications.error(error?.message || "Place ownership could not be changed.");
        }
    }

    async #setConnectionTradingCenter({ edgeId, relatedNode, clear = false, hostApplication } = {}) {
        try {
            if (clear) {
                await TradingCenterService.clearTradingCenter(this.#target, { edgeId });
                ui.notifications.info("Location trading center removed.");
            } else {
                await TradingCenterService.setTradingCenter(this.#target, relatedNode, { edgeId });
                ui.notifications.info(`${relatedNode?.name || "That Location"} is now this Location's trading center.`);
            }
            hostApplication.render({ parts: ["content"] });
        } catch (error) {
            console.error("Augur: Nexus | Failed to update a Location trading center.", error);
            ui.notifications.error(error?.message || "The trading center could not be changed.");
        }
    }

    async #setConnectionMembership({ edgeId, relatedNode, tier = "member", clear = false, hostApplication } = {}) {
        try {
            if (clear) {
                await FactionMembershipService.clearMembership(edgeId);
                ui.notifications.info("Faction membership removed.");
            } else if (tier === "leader") {
                await FactionMembershipService.setLeader(this.#target, relatedNode, { edgeId });
                ui.notifications.info("Faction leader updated.");
            } else {
                await FactionMembershipService.setMember(this.#target, relatedNode, { edgeId });
                ui.notifications.info("Faction member updated.");
            }
            hostApplication.render({ parts: ["content"] });
        } catch (error) {
            console.error("Augur: Nexus | Failed to update Faction membership.", error);
            ui.notifications.error(error?.message || "Faction membership could not be changed.");
        }
    }

    async #setConnectionSeat({ edgeId, relatedNode, clear = false, hostApplication } = {}) {
        try {
            if (clear) {
                await FactionSeatService.clearSeat(this.#target, { edgeId });
                ui.notifications.info("Faction seat removed.");
            } else {
                await FactionSeatService.setSeat(this.#target, relatedNode, { edgeId });
                ui.notifications.info("Faction seat updated.");
            }
            hostApplication.render({ parts: ["content"] });
        } catch (error) {
            console.error("Augur: Nexus | Failed to update the Faction seat.", error);
            ui.notifications.error(error?.message || "The Faction seat could not be changed.");
        }
    }

    async #setConnectionLeadership({ edgeId, relatedNode, clear = false, hostApplication } = {}) {
        const presentation = LocationLeadershipService.getPresentationForTargets(this.#target, relatedNode);
        try {
            if (clear) {
                await LocationLeadershipService.clearLeader(this.#target, { edgeId });
                ui.notifications.info(`Location ${presentation.title.toLocaleLowerCase()} removed.`);
            } else {
                await LocationLeadershipService.setLeader(this.#target, relatedNode, { edgeId });
                ui.notifications.info(`Location ${presentation.title.toLocaleLowerCase()} updated.`);
            }
            hostApplication.render({ parts: ["content"] });
        } catch (error) {
            console.error("Augur: Nexus | Failed to update Location leadership.", error);
            ui.notifications.error(error?.message || `The Location ${presentation.title.toLocaleLowerCase()} could not be changed.`);
        }
    }

    #openGroupActions(button, hostApplication) {
        if (!this.#isGM) return;
        const nodeId = ConnectionTargetResolver.getNodeId(this.#target);
        const categoryId = button.dataset.connectionCategoryId || "";
        if (!nodeId || !categoryId) return;

        const hidden = ConnectionStore.isCategoryGroupHiddenForNode(nodeId, categoryId);
        openActionMenu({
            anchor: button,
            className: "connections-card-actions-menu",
            items: [
                {
                    id: "toggle-group-visibility",
                    label: hidden ? "Show Group To Players" : "Hide Group From Players",
                    icon: hidden ? "fas fa-eye" : "fas fa-eye-slash",
                    onSelect: async () => {
                        await ConnectionStore.toggleCategoryGroupHiddenForNode(nodeId, categoryId);
                        hostApplication.render({ parts: ["content"] });
                    }
                }
            ]
        });
    }

    #openPlayerVisibilityMenu(anchor, edgeId, hostApplication) {
        const edge = ConnectionStore.getConnectionEdge(edgeId);
        if (!edge) return;

        const current = ConnectionStore.getConnectionPlayerVisibility(edge);
        openActionMenu({
            anchor,
            className: "connections-card-actions-menu",
            items: [
                {
                    id: "inherit",
                    label: current === "inherit" ? "Global (Current)" : "Global",
                    icon: "fas fa-layer-group",
                    onSelect: () => this.#setPlayerVisibility(edgeId, "inherit", hostApplication)
                },
                {
                    id: "show",
                    label: current === "show" ? "Yes (Current)" : "Yes",
                    icon: "fas fa-eye",
                    onSelect: () => this.#setPlayerVisibility(edgeId, "show", hostApplication)
                },
                {
                    id: "hide",
                    label: current === "hide" ? "No (Current)" : "No",
                    icon: "fas fa-eye-slash",
                    onSelect: () => this.#setPlayerVisibility(edgeId, "hide", hostApplication)
                },
                {
                    id: "change-global",
                    label: "Change Global Setting...",
                    icon: "fas fa-sliders",
                    onSelect: () => PlayerConnectionVisibilityDialog.show()
                },
                {
                    id: "player-visibility-info",
                    label: "What's this?",
                    icon: "fas fa-circle-info",
                    onSelect: () => PlayerVisibilityInfoPanel.show("connectionVisibility")
                }
            ]
        });
    }

    async #setPlayerVisibility(edgeId, value, hostApplication) {
        await ConnectionStore.setConnectionPlayerVisibility(edgeId, value);
        hostApplication.render({ parts: ["content"] });
    }

    async #editRole(edgeId, nodeId, hostApplication) {
        const edge = ConnectionStore.getConnectionEdge(edgeId);
        if (!edge) return;
        const view = ConnectionStore.getConnectionView(edge, nodeId);
        const role = await promptTextInput({
            title: "Connection Role",
            label: "Role",
            value: view.role || ConnectionCategories.roleFor(view.category),
            placeholder: "Contact, Threat, Hidden Item...",
            confirmLabel: "Save",
            allowEmpty: false,
            emptyMessage: "Role cannot be empty."
        });
        if (role === null) return;
        await ConnectionStore.setConnectionViewForNode(edgeId, nodeId, { role });
        hostApplication.render({ parts: ["content"] });
    }

    async #editNote(edgeId, nodeId, hostApplication) {
        const edge = ConnectionStore.getConnectionEdge(edgeId);
        if (!edge) return;
        const note = await this.#promptConnectionNote(ConnectionStore.getConnectionNoteForNode(edge, nodeId) || "");
        if (note === null) return;
        await ConnectionStore.setConnectionViewForNode(edgeId, nodeId, { note });
        hostApplication.render({ parts: ["content"] });
    }

    async #promptConnectionNote(value = "") {
        const inputId = `augur-connection-note-${foundry.utils.randomID()}`;
        const current = String(value || "");
        const safeValue = foundry.utils.escapeHTML(current);
        const result = await foundry.applications.api.DialogV2.wait({
            window: { title: "Connection Note" },
            position: { width: 430 },
            modal: true,
            rejectClose: false,
            content: `
                <div class="nexus-text-prompt-form">
                    <label for="${inputId}">Add context for this connection.</label>
                    <input id="${inputId}" type="text" value="${safeValue}" placeholder="Why does this connection matter?" autocomplete="off">
                </div>
            `,
            buttons: [
                {
                    action: "confirm",
                    label: "Save",
                    icon: "fa-solid fa-check",
                    default: true,
                    callback: () => document.getElementById(inputId)?.value?.trim() || ""
                },
                ...(current.trim() ? [{
                    action: "clear",
                    label: "Clear",
                    icon: "fa-solid fa-eraser",
                    callback: () => ""
                }] : []),
                {
                    action: "cancel",
                    label: "Cancel",
                    icon: "fa-solid fa-xmark",
                    callback: () => null
                }
            ],
            render: (_event, dialog) => {
                const input = dialog.element?.querySelector?.(`#${CSS.escape(inputId)}`);
                input?.addEventListener("keydown", event => {
                    if (event.key !== "Enter") return;
                    event.preventDefault();
                    dialog.element?.querySelector("button[data-action='confirm']")?.click();
                });
                requestAnimationFrame(() => {
                    input?.focus({ preventScroll: true });
                    input?.select();
                });
            }
        });
        return typeof result === "string" && result !== "cancel" ? result : null;
    }

    async #openCategoryPicker(edgeId, nodeId, hostApplication) {
        const edge = ConnectionStore.getConnectionEdge(edgeId);
        const currentCategoryId = edge ? ConnectionStore.getConnectionCategoryForNode(edge, nodeId, ConnectionStore.getCustomCategories()) : "";
        const categoryId = await ConnectionCategoryPicker.choose({ currentCategoryId, target: this.#target });
        if (!categoryId) return;
        await ConnectionCategoryPicker.assignEdgeCategory(edgeId, categoryId, { nodeId });
        hostApplication.render({ parts: ["content"] });
    }
}
