import { canSetTradingCenter, getTradeNetwork, getTradingCenter } from "/modules/augur-nexus/scripts/api/trade-connections.js";
import { canUserViewLocationDetails, getLocation, getLocationConnectionTarget, getLocations } from "/modules/augur-nexus/scripts/api/locations.js";
import { getFaction, getFactionConnectionTarget } from "/modules/augur-nexus/scripts/api/factions.js";
import { getOwnedHoldings, getOwnership } from "/modules/augur-nexus/scripts/api/ownership.js";
import { TradeGoodsCatalog } from "../services/TradeGoodsCatalog.js";
import { TradeBasinEconomyService } from "../services/TradeBasinEconomyService.js";
import { TradeRegionSceneService } from "../services/TradeRegionSceneService.js";
import { CumulativeTradeFlowResolver } from "../services/CumulativeTradeFlowResolver.js";

const MODULE_ID = "augur-nexus";

export class RegionalTradeNetworkModel {
    static async build({ focusLocationId = "", factionId = "", selectedLocationId = "", scope = "connected", sceneId = "" } = {}) {
        const network = getTradeNetwork({ includeHidden: game.user?.isGM === true });
        const focusEntity = focusLocationId ? getLocation(focusLocationId) : null;
        const resolvedSceneId = String(sceneId || TradeRegionSceneService.resolveSceneId(focusEntity) || "");
        const worldLocations = getLocations();
        const regionalCandidates = resolvedSceneId
            ? TradeRegionSceneService.getRegionLocations(resolvedSceneId, { locations: worldLocations })
            : worldLocations.filter(entity => entity.id === focusLocationId);
        const regionLocations = regionalCandidates.filter(entity => canUserViewLocationDetails(entity, game.user));
        const locations = regionLocations
            .map(entity => this.#locationNode(entity));
        const nodesById = new Map(locations.map(node => [node.nodeId, node]));
        const nodesByEntityId = new Map(locations.map(node => [node.entityId, node]));
        const edges = (network.edges || []).filter(edge => nodesById.has(edge.centerNodeId) && nodesById.has(edge.locationNodeId));
        const edgesById = new Map(edges.map(edge => [edge.id, edge]));
        const upstreamByNodeId = new Map(edges.map(edge => [edge.locationNodeId, edge]));
        const childrenByNodeId = new Map();
        for (const edge of edges) {
            const children = childrenByNodeId.get(edge.centerNodeId) || [];
            children.push(edge);
            childrenByNodeId.set(edge.centerNodeId, children);
        }
        const { exportsByNodeId } = CumulativeTradeFlowResolver.resolve({
            nodeIds: [...nodesById.keys()],
            producedByNodeId: new Map(locations.map(node => [node.nodeId, node.produces])),
            centerByNodeId: new Map(edges.map(edge => [edge.locationNodeId, edge.centerNodeId]))
        });

        const faction = factionId ? getFaction(factionId) : null;
        const factionTarget = faction ? getFactionConnectionTarget(faction) : null;
        const holdingEntityIds = new Set((factionTarget ? getOwnedHoldings(factionTarget, { includeHidden: game.user?.isGM === true }) : [])
            .filter(holding => holding.asset?.entityType === "location")
            .map(holding => holding.asset.entityId));
        const anchorNodeIds = new Set([...holdingEntityIds].map(id => nodesByEntityId.get(id)?.nodeId).filter(Boolean));
        const focusNode = nodesByEntityId.get(focusLocationId) || null;
        const normalizedScope = scope === "faction" && faction ? "faction" : scope === "all" ? "all" : "connected";
        const includedNodeIds = this.#includedNodes({
            scope: normalizedScope,
            focusNodeId: focusNode?.nodeId || "",
            factionAnchorNodeIds: anchorNodeIds,
            allNodeIds: new Set(nodesById.keys()),
            edges
        });
        const includedEdges = edges.filter(edge => includedNodeIds.has(edge.centerNodeId) && includedNodeIds.has(edge.locationNodeId));
        const includedEdgeIds = new Set(includedEdges.map(edge => edge.id));

        const goods = await this.#goodsCatalog({ locations, location: focusEntity || regionLocations[0] || null });
        const goodsById = new Map(goods.map(good => [good.id, good]));
        const selectedNode = nodesByEntityId.get(selectedLocationId)
            || focusNode
            || nodesById.get([...includedNodeIds][0])
            || null;
        const rows = this.#treeRows({
            nodesById,
            includedNodeIds,
            includedEdgeIds,
            upstreamByNodeId,
            childrenByNodeId,
            goodsById,
            exportsByNodeId,
            selectedNodeId: selectedNode?.nodeId || "",
            factionAnchorNodeIds: anchorNodeIds
        });

        const selected = selectedNode
            ? this.#selectedView({
                node: selectedNode,
                nodesById,
                locations,
                edgesById,
                upstreamByNodeId,
                childrenByNodeId,
                goods,
                goodsById,
                exportsByNodeId,
                factionAnchorNodeIds: anchorNodeIds,
                sceneId: resolvedSceneId
            })
            : null;
        const routeCount = includedEdges.length;
        const connectedLocationCount = new Set(includedEdges.flatMap(edge => [edge.centerNodeId, edge.locationNodeId])).size;

        return {
            scope: normalizedScope,
            sceneId: resolvedSceneId,
            sceneName: TradeRegionSceneService.getSceneName(resolvedSceneId),
            hasRegion: !!resolvedSceneId,
            focusLocationId,
            factionId: faction?.id || "",
            factionName: faction?.display?.name || faction?.journalEntryName || "",
            scopes: [
                { id: "connected", label: "This Network", icon: "fas fa-route", active: normalizedScope === "connected" },
                ...(faction ? [{ id: "faction", label: "Faction Holdings", icon: "fas fa-shield-halved", active: normalizedScope === "faction" }] : []),
                { id: "all", label: "This Region", icon: "fas fa-map", active: normalizedScope === "all" }
            ],
            rows,
            hasRows: rows.length > 0,
            selected,
            selectedLocationId: selected?.entityId || "",
            summary: {
                locationCount: includedNodeIds.size,
                connectedLocationCount,
                routeCount,
                unconnectedCount: Math.max(0, includedNodeIds.size - connectedLocationCount)
            },
            isGM: game.user?.isGM === true
        };
    }

    static #locationNode(entity) {
        const target = getLocationConnectionTarget(entity);
        const economy = entity.economy || {};
        const ownership = target ? getOwnership(target, { includeHidden: game.user?.isGM === true }) : null;
        return {
            nodeId: target?.id || `nexus-entity:${entity.id}`,
            entityId: entity.id,
            target,
            name: String(entity.display?.name || entity.journalEntryName || "Location"),
            imageSrc: String(entity.display?.thumbnailSrc || entity.display?.imageSrc || ""),
            subtitle: [entity.profile?.type, entity.profile?.kindLabel].filter(Boolean).join(" - ") || "Location",
            typeId: TradeRegionSceneService.getLocationTypeId(entity),
            produces: this.#entryIds(economy.produces),
            ownerFactionId: ownership?.owner?.entityType === "faction" ? ownership.owner.entityId : "",
            entity
        };
    }

    static #includedNodes({ scope, focusNodeId, factionAnchorNodeIds, allNodeIds, edges }) {
        if (scope === "all") return new Set(allNodeIds);
        const anchors = scope === "faction" ? factionAnchorNodeIds : new Set([focusNodeId].filter(Boolean));
        if (!anchors.size) return scope === "faction" ? new Set() : new Set(allNodeIds);
        const adjacency = new Map();
        for (const edge of edges) {
            const center = adjacency.get(edge.centerNodeId) || new Set();
            center.add(edge.locationNodeId);
            adjacency.set(edge.centerNodeId, center);
            const location = adjacency.get(edge.locationNodeId) || new Set();
            location.add(edge.centerNodeId);
            adjacency.set(edge.locationNodeId, location);
        }
        const included = new Set();
        const queue = [...anchors];
        while (queue.length) {
            const nodeId = queue.shift();
            if (!nodeId || included.has(nodeId)) continue;
            included.add(nodeId);
            for (const relatedId of adjacency.get(nodeId) || []) if (!included.has(relatedId)) queue.push(relatedId);
        }
        return included;
    }

    static #treeRows({ nodesById, includedNodeIds, includedEdgeIds, upstreamByNodeId, childrenByNodeId, goodsById, exportsByNodeId, selectedNodeId, factionAnchorNodeIds }) {
        const rows = [];
        const visited = new Set();
        const childEdges = nodeId => (childrenByNodeId.get(nodeId) || [])
            .filter(edge => includedEdgeIds.has(edge.id))
            .sort((left, right) => (nodesById.get(left.locationNodeId)?.name || "").localeCompare(nodesById.get(right.locationNodeId)?.name || ""));
        const append = (nodeId, depth) => {
            if (visited.has(nodeId)) return;
            const node = nodesById.get(nodeId);
            if (!node || !includedNodeIds.has(nodeId)) return;
            visited.add(nodeId);
            const upstream = upstreamByNodeId.get(nodeId);
            const route = upstream && includedEdgeIds.has(upstream.id) ? upstream : null;
            const children = childEdges(nodeId);
            const fantasySends = this.#goodViews(exportsByNodeId.get(nodeId) || [], goodsById);
            rows.push({
                ...node,
                depth,
                depthOffset: depth * 18,
                selected: nodeId === selectedNodeId,
                isFactionHolding: factionAnchorNodeIds.has(nodeId),
                hasChildren: children.length > 0,
                childCount: children.length,
                route: route ? {
                    edgeId: route.id,
                    centerNodeId: route.centerNodeId,
                    centerName: nodesById.get(route.centerNodeId)?.name || "Trading Center",
                    sends: fantasySends.slice(0, 2),
                    sendsMore: Math.max(0, fantasySends.length - 2),
                    hasSends: fantasySends.length > 0,
                    hasFlows: fantasySends.length > 0,
                    inconsistent: false
                } : null,
                produced: this.#goodViews(node.produces, goodsById, 3),
                producedMore: Math.max(0, node.produces.length - 3)
            });
            for (const edge of children) append(edge.locationNodeId, depth + 1);
        };
        const roots = [...includedNodeIds]
            .filter(nodeId => {
                const upstream = upstreamByNodeId.get(nodeId);
                return !upstream || !includedEdgeIds.has(upstream.id);
            })
            .sort((left, right) => (nodesById.get(left)?.name || "").localeCompare(nodesById.get(right)?.name || ""));
        for (const rootId of roots) append(rootId, 0);
        for (const nodeId of includedNodeIds) append(nodeId, 0);
        return rows;
    }

    static #selectedView({ node, nodesById, locations, edgesById, upstreamByNodeId, childrenByNodeId, goods, goodsById, exportsByNodeId, factionAnchorNodeIds, sceneId }) {
        const tradingCenter = node.target ? getTradingCenter(node.target, { includeHidden: game.user?.isGM === true }) : null;
        const upstream = upstreamByNodeId.get(node.nodeId) || null;
        const edge = upstream ? edgesById.get(upstream.id) || upstream : null;
        const centerNode = tradingCenter ? nodesById.get(tradingCenter.centerNodeId) || null : null;
        const feederRoutes = (childrenByNodeId.get(node.nodeId) || [])
            .map(feederEdge => {
                const feeder = nodesById.get(feederEdge.locationNodeId);
                if (!feeder) return null;
                const incomingGoods = this.#goodViews(exportsByNodeId.get(feeder.nodeId) || [], goodsById);
                return {
                    edgeId: feederEdge.id,
                    entityId: feeder.entityId,
                    name: feeder.name,
                    imageSrc: feeder.imageSrc,
                    subtitle: feeder.subtitle,
                    incomingGoods,
                    hasIncomingGoods: incomingGoods.length > 0,
                    hasCargo: incomingGoods.length > 0
                };
            })
            .filter(Boolean)
            .sort((left, right) => left.name.localeCompare(right.name));
        const market = TradeBasinEconomyService.getMarketState(node.entityId, "", { sceneId });
        const marketName = TradeBasinEconomyService.getMarketLabel(node.entityId, { user: game.user, sceneId });
        const basinGoodIds = [...market.basin.goods];
        const basinGoodsExcludingSelected = [...new Set(market.basin.members
            .filter(member => member.entityId !== node.entityId)
            .flatMap(member => member.produces))];
        const candidates = locations
            .filter(candidate => candidate.entityId !== node.entityId && candidate.target)
            .filter(candidate => TradeRegionSceneService.isTradingCenter(candidate.entity))
            .filter(candidate => candidate.nodeId === tradingCenter?.centerNodeId || canSetTradingCenter(node.target, candidate.target))
            .sort((left, right) => left.name.localeCompare(right.name))
            .map(candidate => ({
                entityId: candidate.entityId,
                name: candidate.name,
                subtitle: candidate.subtitle,
                selected: candidate.nodeId === tradingCenter?.centerNodeId
            }));
        return {
            ...node,
            isFactionHolding: factionAnchorNodeIds.has(node.nodeId),
            tradingCenter: centerNode ? {
                entityId: centerNode.entityId,
                name: centerNode.name,
                imageSrc: centerNode.imageSrc,
                subtitle: centerNode.subtitle
            } : null,
            invalidTradingCenter: tradingCenter && !centerNode ? {
                name: tradingCenter.center?.name || "Outside this region",
                reason: "This saved route points to a Location outside the current Scene and is not used by this regional market."
            } : null,
            routeEdgeId: edge?.id || tradingCenter?.edgeId || "",
            dependentCount: (childrenByNodeId.get(node.nodeId) || []).length,
            feederRoutes,
            hasFeederRoutes: feederRoutes.length > 0,
            feederRouteCount: feederRoutes.length,
            feederIncomingGoodCount: new Set(feederRoutes.flatMap(route => route.incomingGoods.map(good => good.id))).size,
            marketName,
            marketRootName: marketName === market.basin.marketName ? market.basin.rootName : "",
            basinGoodsExcludingSelected,
            availableGoods: this.#goodViews(basinGoodIds, goodsById).map(good => ({ ...good, isLocal: node.produces.includes(good.id) })),
            hasAvailableGoods: basinGoodIds.length > 0,
            candidates,
            producedGoods: this.#goodViews(node.produces, goodsById),
            externalFlows: edge?.resourceFlows || [],
            hasExternalFlows: (edge?.resourceFlows || []).length > 0,
            goods: goods.map(good => ({
                ...good,
                produced: node.produces.includes(good.id)
            }))
        };
    }

    static async #goodsCatalog({ locations, location = null }) {
        const catalog = await TradeGoodsCatalog.getGoodsForLocation(location);
        const goodsById = new Map(catalog.map(good => [good.id, {
            id: String(good.id),
            name: String(good.name || good.id),
            imageSrc: String(good.imageSrc || ""),
            family: String(good.familyId || "Trade Good"),
            fallback: false
        }]));
        const ensure = (id, label = "", imageSrc = "") => {
            const key = String(id || "").trim();
            if (!key || goodsById.has(key)) return;
            goodsById.set(key, { id: key, name: String(label || key), imageSrc: String(imageSrc || ""), family: "Uncatalogued", fallback: true });
        };
        for (const location of locations) for (const id of location.produces) ensure(id);
        return [...goodsById.values()].sort((left, right) => left.name.localeCompare(right.name));
    }

    static #entryIds(entries = []) {
        return [...new Set((Array.isArray(entries) ? entries : [])
            .map(entry => String(entry?.goodId || "").trim())
            .filter(Boolean))];
    }

    static #goodViews(ids = [], goodsById, limit = Infinity) {
        return ids.map(id => goodsById.get(id) || { id, name: id, imageSrc: "", fallback: true }).slice(0, limit);
    }

}
