const API_VERSION = 1;
const RELATIONSHIP_KINDS = new Set([
    "connection",
    "ship-crew",
    "ship-captain",
    "ship-owner",
    "faction-member",
    "faction-leader",
    "location-leader",
    "ownership",
    "faction-seat"
]);

const ENTITY_API = Object.freeze({
    npc: ["./npcs.js", "getNpcConnectionTarget"],
    ship: ["./ships.js", "getShipConnectionTarget"],
    faction: ["./factions.js", "getFactionConnectionTarget"],
    location: ["./locations.js", "getLocationConnectionTarget"]
});

function clone(value) { return foundry.utils.deepClone(value); }
function text(value) { return String(value ?? "").trim(); }
function assertGm() { if (!game.user?.isGM) throw new Error("Only a GM can change Nexus relationships."); }

function canonical(value) {
    if (Array.isArray(value)) return value.map(canonical);
    if (!value || typeof value !== "object") return value;
    return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
}

function fingerprint(value) {
    const source = JSON.stringify(canonical(value));
    let hash = 0x811c9dc5;
    for (let index = 0; index < source.length; index++) {
        hash ^= source.charCodeAt(index);
        hash = Math.imul(hash, 0x01000193);
    }
    return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}:${source.length}`;
}

function normalizeReference(reference = {}) {
    const entityType = text(reference.entityType).toLocaleLowerCase();
    const entityId = text(reference.entityId || reference.id);
    if (!ENTITY_API[entityType] || !entityId) throw new Error("Relationship endpoints require an exact Nexus entity type and ID.");
    return { entityType, entityId, id: entityId, name: text(reference.name) };
}

async function resolveTarget(reference = {}) {
    const normalized = normalizeReference(reference);
    const [path, method] = ENTITY_API[normalized.entityType];
    const api = await import(path);
    const target = api[method](normalized.entityId);
    if (!target) throw new Error(`Nexus could not resolve ${normalized.entityType}:${normalized.entityId}.`);
    return { reference: { ...normalized, name: normalized.name || target.name }, target };
}

function assertKind(kind, sourceType, relatedType) {
    if (!RELATIONSHIP_KINDS.has(kind)) throw new Error(`Unsupported Nexus relationship kind: ${kind || "missing"}.`);
    const pair = `${sourceType}:${relatedType}`;
    const reverse = `${relatedType}:${sourceType}`;
    if (["faction-member", "faction-leader"].includes(kind) && pair !== "faction:npc" && reverse !== "faction:npc") {
        throw new Error(`${kind} requires one faction and one NPC.`);
    }
    if (kind === "location-leader" && pair !== "location:npc" && reverse !== "location:npc") {
        throw new Error("location-leader requires one location and one NPC.");
    }
    if (["ship-crew", "ship-captain"].includes(kind) && pair !== "ship:npc" && reverse !== "ship:npc") {
        throw new Error(`${kind} requires one ship and one NPC.`);
    }
    if (kind === "ship-owner" && !(pair === "ship:npc" || pair === "ship:faction" || reverse === "ship:npc" || reverse === "ship:faction")) {
        throw new Error("ship-owner requires one ship and one NPC or faction.");
    }
    if (kind === "ownership" && !(["location:faction", "location:npc", "faction:location", "npc:location"].includes(pair))) {
        throw new Error("Nexus ownership currently requires a location asset and an NPC or faction owner.");
    }
    if (kind === "faction-seat" && pair !== "faction:location" && reverse !== "faction:location") {
        throw new Error("faction-seat requires one faction and one owned location.");
    }
}

export function validateEntityRelationshipEndpoints({ kind = "connection", source = {}, related = {} } = {}) {
    const normalizedKind = text(kind);
    const sourceType = text(source.entityType).toLocaleLowerCase();
    const relatedType = text(related.entityType).toLocaleLowerCase();
    if (!ENTITY_API[sourceType] || !ENTITY_API[relatedType]) {
        throw new Error("Relationship endpoints require Nexus NPC, ship, faction, or location types.");
    }
    assertKind(normalizedKind, sourceType, relatedType);
    return { ok: true, kind: normalizedKind, sourceType, relatedType };
}

function orient(kind, source, related) {
    const endpoints = [source, related];
    const byType = type => endpoints.find(endpoint => endpoint.reference.entityType === type);
    if (["faction-member", "faction-leader"].includes(kind)) return { primary: byType("faction"), secondary: byType("npc") };
    if (kind === "location-leader") return { primary: byType("location"), secondary: byType("npc") };
    if (["ship-crew", "ship-captain"].includes(kind)) return { primary: byType("ship"), secondary: byType("npc") };
    if (kind === "ship-owner") return { primary: byType("ship"), secondary: byType("faction") || byType("npc") };
    if (kind === "ownership") return { primary: byType("location"), secondary: byType("faction") || byType("npc") };
    if (kind === "faction-seat") return { primary: byType("faction"), secondary: byType("location") };
    return { primary: source, secondary: related };
}

async function pairState(sourceTarget, relatedTarget) {
    const { ConnectionStore } = await import("../features/connections/services/ConnectionStore.js");
    const sourceId = text(sourceTarget.id);
    const relatedId = text(relatedTarget.id);
    const edges = (ConnectionStore.getGraphContext().adjacency.get(sourceId) || [])
        .filter(entry => entry.node?.id === relatedId)
        .map(entry => clone(entry.edge))
        .sort((left, right) => text(left.id).localeCompare(text(right.id)));
    return { sourceId, relatedId, edges, fingerprint: fingerprint({ sourceId, relatedId, edges }) };
}

function relationshipSemantics(edge = {}, subjectNode = {}, relatedNode = {}) {
    const subjectNodeId = text(subjectNode.id);
    const semantics = [];
    const membership = edge.factionMembership;
    if (membership?.factionNodeId && membership?.memberNodeId) {
        const leader = membership.tier === "leader";
        const subjectIsFaction = subjectNodeId === membership.factionNodeId;
        semantics.push({
            kind: leader ? "faction-leader" : "faction-member",
            perspective: subjectIsFaction ? (leader ? "led-by" : "has-member") : (leader ? "leads" : "member-of"),
            role: text(membership.rank?.label) || (leader ? "Faction Leader" : "Member"),
            rankLabel: text(membership.rank?.label),
            rankValue: Number(membership.rank?.value || 0)
        });
    }

    const leadership = edge.locationLeadership;
    if (leadership?.locationNodeId && leadership?.leaderNodeId) {
        const subjectIsLocation = subjectNodeId === leadership.locationNodeId;
        const authorityType = text(leadership.authorityType) || "ruler";
        semantics.push({
            kind: "location-leader",
            perspective: subjectIsLocation ? "led-by" : "leads",
            role: authorityType === "owner" ? (subjectIsLocation ? "Owner" : "Owns") : (subjectIsLocation ? "Ruler" : "Rules"),
            authorityType
        });
    }

    const shipOwnership = edge.shipOwnership;
    if (shipOwnership?.shipNodeId && shipOwnership?.ownerNodeId) {
        const subjectIsShip = subjectNodeId === shipOwnership.shipNodeId;
        semantics.push({
            kind: "ship-owner",
            perspective: subjectIsShip ? "owned-by" : "owns-ship",
            role: subjectIsShip ? "Owned By" : "Ship Owner"
        });
    }

    const shipLocation = edge.shipLocation;
    if (shipLocation?.shipNodeId && shipLocation?.placeNodeId) {
        const subjectIsShip = subjectNodeId === shipLocation.shipNodeId;
        semantics.push({
            kind: "connection",
            perspective: subjectIsShip ? "located-at" : "has-ship",
            role: subjectIsShip ? "Current Location" : "Ship Present"
        });
    }

    if (edge.relationType === "ownership") {
        const subjectIsAsset = subjectNodeId === text(edge.targetNodeId);
        semantics.push({
            kind: "ownership",
            perspective: subjectIsAsset ? "owned-by" : "owns",
            role: subjectIsAsset ? "Owned By" : "Owns"
        });
    }

    const seat = edge.factionSeat;
    if (seat?.factionNodeId && seat?.placeNodeId) {
        const subjectIsFaction = subjectNodeId === seat.factionNodeId;
        semantics.push({
            kind: "faction-seat",
            perspective: subjectIsFaction ? "has-seat" : "seat-of",
            role: subjectIsFaction ? "Seat of Power" : "Faction Seat"
        });
    }

    if (edge.shipCaptain || ["ship-crew", "ship-captain"].includes(edge.relationType)) {
        const captain = !!edge.shipCaptain || edge.relationType === "ship-captain";
        const subjectIsShip = subjectNode.entityType === "ship";
        semantics.push({
            kind: captain ? "ship-captain" : "ship-crew",
            perspective: subjectIsShip ? (captain ? "captained-by" : "has-crew") : (captain ? "captain-of" : "crew-of"),
            role: subjectIsShip ? (captain ? "Captain" : "Crew") : (captain ? "Captains" : "Crew Member")
        });
    }

    if (!semantics.length) {
        semantics.push({
            kind: "connection",
            perspective: "connected-to",
            role: text(edge.relationType) || text(relatedNode.category) || "Connection"
        });
    }
    return semantics;
}

function entityReferenceFromNode(node = {}) {
    const entityId = text(node.entityId);
    return {
        nodeId: text(node.id),
        kind: text(node.kind),
        entityType: text(node.entityType),
        entityId,
        id: entityId || text(node.id),
        name: text(node.name) || "Unknown Entity",
        subtitle: text(node.subtitle)
    };
}

export async function inspectEntityRelationshipPair(sourceReference, relatedReference) {
    const [source, related] = await Promise.all([resolveTarget(sourceReference), resolveTarget(relatedReference)]);
    return {
        apiVersion: API_VERSION,
        source: clone(source.reference),
        related: clone(related.reference),
        ...(await pairState(source.target, related.target))
    };
}

export async function inspectEntityRelationships(reference = {}) {
    const resolved = await resolveTarget(reference);
    const { ConnectionStore } = await import("../features/connections/services/ConnectionStore.js");
    const { ConnectionTargetResolver } = await import("../features/connections/services/ConnectionTargetResolver.js");
    const subjectNode = ConnectionStore.getNode(resolved.target.id) || resolved.target;
    const relationships = ConnectionStore.getConnectionsForNode(subjectNode.id).map(({ edge, node }) => {
        const display = ConnectionTargetResolver.resolveDisplayNodeSync(node) || node;
        return {
            edgeId: text(edge.id),
            counterpart: entityReferenceFromNode({ ...node, ...display, id: node.id, entityId: node.entityId, entityType: node.entityType }),
            semantics: relationshipSemantics(edge, subjectNode, node),
            subjectView: clone(ConnectionStore.getConnectionView(edge, subjectNode.id)),
            counterpartView: clone(ConnectionStore.getConnectionView(edge, node.id))
        };
    }).sort((left, right) => left.counterpart.name.localeCompare(right.counterpart.name)
        || left.edgeId.localeCompare(right.edgeId));
    return {
        apiVersion: API_VERSION,
        subject: clone(resolved.reference),
        subjectNodeId: text(subjectNode.id),
        relationshipCount: relationships.length,
        relationships
    };
}

export async function planEntityRelationship({ kind = "connection", source, related, options = {} } = {}) {
    assertGm();
    const normalizedKind = text(kind);
    const [resolvedSource, resolvedRelated] = await Promise.all([resolveTarget(source), resolveTarget(related)]);
    validateEntityRelationshipEndpoints({ kind: normalizedKind, source: resolvedSource.reference, related: resolvedRelated.reference });
    const state = await pairState(resolvedSource.target, resolvedRelated.target);
    const oriented = orient(normalizedKind, resolvedSource, resolvedRelated);
    return {
        apiVersion: API_VERSION,
        planId: `nexus-relationship.${foundry.utils.randomID()}`,
        kind: "entity-relationship",
        operation: normalizedKind,
        sourceFingerprint: state.fingerprint,
        target: { source: clone(resolvedSource.reference), related: clone(resolvedRelated.reference) },
        summary: `${normalizedKind.replaceAll("-", " ")}: ${resolvedSource.reference.name} and ${resolvedRelated.reference.name}`,
        effects: [{ operation: normalizedKind, source: resolvedSource.reference.name, related: resolvedRelated.reference.name, existingEdgeCount: state.edges.length }],
        payload: {
            primary: clone(oriented.primary.reference),
            secondary: clone(oriented.secondary.reference),
            options: {
                label: text(options.label), category: text(options.category), notes: text(options.notes),
                rankLabel: text(options.rankLabel), rankValue: Number(options.rankValue || 0),
                authorityType: text(options.authorityType), role: text(options.role)
            }
        }
    };
}

export async function applyEntityRelationshipPlan(plan = {}) {
    assertGm();
    if (plan.apiVersion !== API_VERSION || plan.kind !== "entity-relationship" || !RELATIONSHIP_KINDS.has(plan.operation)) {
        throw new Error("Unsupported Nexus relationship plan.");
    }
    const [source, related, primary, secondary] = await Promise.all([
        resolveTarget(plan.target?.source), resolveTarget(plan.target?.related),
        resolveTarget(plan.payload?.primary), resolveTarget(plan.payload?.secondary)
    ]);
    const state = await pairState(source.target, related.target);
    if (state.fingerprint !== plan.sourceFingerprint) throw new Error("This Nexus relationship changed after the plan was prepared. Refresh it before applying.");
    const options = plan.payload?.options || {};
    let edge;
    if (plan.operation === "faction-member" || plan.operation === "faction-leader") {
        const api = await import("./memberships.js");
        const method = plan.operation === "faction-leader" ? api.setFactionLeader : api.setFactionMember;
        edge = await method(primary.target, secondary.target, {
            role: options.role || undefined,
            rank: options.rankLabel ? { label: options.rankLabel, value: Number(options.rankValue || 0) } : undefined
        });
    } else if (plan.operation === "location-leader") {
        const api = await import("./leadership.js");
        edge = await api.setLocationLeader(primary.target, secondary.target, { authorityType: options.authorityType || undefined, role: options.role || undefined });
    } else if (plan.operation === "ownership") {
        const api = await import("./ownership.js");
        edge = await api.setOwner(primary.target, secondary.target);
    } else if (plan.operation === "faction-seat") {
        const api = await import("./leadership.js");
        edge = await api.setFactionSeat(primary.target, secondary.target);
    } else if (plan.operation === "ship-owner") {
        const { ShipOwnershipService } = await import("../features/connections/services/ShipOwnershipService.js");
        edge = await ShipOwnershipService.setOwner(primary.target, secondary.target);
    } else if (plan.operation === "ship-captain") {
        const { ShipCaptainService } = await import("../features/connections/services/ShipCaptainService.js");
        edge = await ShipCaptainService.setCaptain(primary.target, secondary.target);
    } else if (plan.operation === "ship-crew") {
        const { ShipCrewService } = await import("../features/connections/services/ShipCrewService.js");
        edge = await ShipCrewService.addCrew(primary.target, secondary.target);
    } else {
        const api = await import("./connections.js");
        edge = await api.addConnection(primary.target, secondary.target, {
            relationType: options.label || "",
            category: options.category || undefined,
            role: options.role || options.label || undefined,
            note: options.notes || undefined
        });
    }
    if (!edge?.id) throw new Error("Nexus did not apply the requested relationship.");
    return {
        ok: true, planId: plan.planId, kind: plan.operation, edgeId: text(edge.id), id: text(edge.id),
        source: clone(source.reference), related: clone(related.reference), label: options.label || plan.operation.replaceAll("-", " ")
    };
}

export function getEntityRelationshipCapability() {
    return {
        apiVersion: API_VERSION,
        id: "augur-nexus.entity-relationships",
        subjects: ["npc", "ship", "faction", "location"],
        relationshipKinds: [...RELATIONSHIP_KINDS],
        operations: ["inspect-entity", "inspect-pair", "validate", "plan", "apply"]
    };
}

export const __entityRelationshipTestUtils = Object.freeze({ fingerprint, normalizeReference, assertKind, relationshipSemantics, entityReferenceFromNode });
