import { canUserViewLocationDetails, getLocation, getLocationConnectionTarget, getLocations } from "/modules/augur-nexus/scripts/api/locations.js";
import { getTradeNetwork } from "/modules/augur-nexus/scripts/api/trade-connections.js";
import { getShopsForParentLocation } from "/modules/augur-nexus/scripts/api/shops.js";
import { SettlementMarketplaceModel } from "../models/SettlementMarketplaceModel.js";
import { TradeGoodsCatalog } from "./TradeGoodsCatalog.js";
import { TradeRegionSceneService } from "./TradeRegionSceneService.js";

const MODULE_ID = "augur-nexus";
const MARKETPLACE_TEMPLATE_ID = "settlement-marketplace";

export class TradeBasinEconomyService {
    static #projections = new Map();
    static #hooksRegistered = false;

    static registerHooks() {
        if (this.#hooksRegistered) return;
        this.#hooksRegistered = true;
        TradeRegionSceneService.registerHooks();
        const invalidate = () => { this.#projections.clear(); };
        Hooks.on("augurNexusConnectionsChanged", invalidate);
        Hooks.on("augurNexusCampaignEntitiesChanged", invalidate);
        Hooks.on("augurNexusTradeGoodsChanged", invalidate);
        Hooks.on("createTile", invalidate);
        Hooks.on("updateTile", invalidate);
        Hooks.on("deleteTile", invalidate);
    }

    static getMarketState(idOrEntity, goodId = "", { sceneId = "" } = {}) {
        const entity = typeof idOrEntity === "string" ? getLocation(idOrEntity) : idOrEntity;
        const resolvedSceneId = TradeRegionSceneService.resolveSceneId(entity, { sceneId });
        const projection = this.#getProjection(resolvedSceneId);
        const location = projection.locationsByEntityId.get(entity?.id) || this.#standaloneLocation(entity);
        const basin = projection.basinsByNodeId.get(location?.nodeId) || this.#standaloneBasin(location);
        const normalizedGoodId = String(goodId || "");
        const local = new Set(location?.produces || []);
        const supplied = basin.goods.has(normalizedGoodId);
        return {
            entity,
            location,
            basin,
            sceneId: resolvedSceneId,
            sceneName: TradeRegionSceneService.getSceneName(resolvedSceneId),
            goodId: normalizedGoodId,
            status: local.has(normalizedGoodId) ? "local" : supplied ? "supplied" : "opportunity",
            isLocal: local.has(normalizedGoodId),
            isRegionallySupplied: supplied,
            isImportOpportunity: !supplied
        };
    }

    static async getLocationView(idOrEntity, { user = game.user, sceneId = "" } = {}) {
        const state = this.getMarketState(idOrEntity, "", { sceneId });
        if (!state.entity) return null;
        const marketName = this.getMarketLabel(state.entity, { user, sceneId: state.sceneId });
        const goods = await TradeGoodsCatalog.getGoods();
        const goodsById = new Map(goods.map(good => [good.id, good]));
        const localIds = new Set(state.location?.produces || []);
        const goodView = (goodId, status) => {
            const good = goodsById.get(goodId);
            const destinations = status === "local"
                ? this.getKnownImportMarkets(state.entity, goodId, { user, limit: 3, sceneId: state.sceneId })
                : [];
            const producers = (state.basin.producersByGoodId.get(goodId) || [])
                .filter(producer => user?.isGM || canUserViewLocationDetails(producer.entityId, user));
            const sourceLabel = producers.slice(0, 3).map(producer => producer.name).join(", ");
            return {
                id: goodId,
                name: good?.name || goodId,
                imageSrc: good?.imageSrc || "",
                status,
                isLocal: status === "local",
                isSupplied: status === "supplied",
                tooltip: status === "local"
                    ? `Produced at ${state.location.name}.${destinations.length ? ` Buyer markets: ${destinations.map(destination => destination.name).join(", ")}.` : " No known buyer market has been visited yet."}`
                    : `Available through ${marketName}.${sourceLabel ? ` Produced by ${sourceLabel}.` : ""}`
            };
        };
        const produced = [...localIds].map(goodId => goodView(goodId, "local"));
        const supplied = [...state.basin.goods]
            .filter(goodId => !localIds.has(goodId))
            .map(goodId => goodView(goodId, "supplied"));
        return {
            produced,
            supplied,
            available: [...produced, ...supplied],
            hasProduced: produced.length > 0,
            hasSupplied: supplied.length > 0,
            hasGoods: produced.length > 0 || supplied.length > 0,
            marketName,
            sceneId: state.sceneId,
            sceneName: state.sceneName,
            hasRegion: !!state.sceneId,
            marketRootName: marketName === state.basin.marketName ? state.basin.rootName : "",
            isRegionalDestination: state.basin.rootNodeId === state.location.nodeId,
            tradingCenter: state.location.centerNodeId
                ? projectionLocation(this.#getProjection(state.sceneId), state.location.centerNodeId, user)
                : null
        };
    }

    static getMarketLabel(idOrEntity, { user = game.user, sceneId = "" } = {}) {
        const state = this.getMarketState(idOrEntity, "", { sceneId });
        if (user?.isGM || canUserViewLocationDetails(state.basin.rootEntityId, user)) return state.basin.marketName;
        return "Regional Trade Network";
    }

    static getKnownImportMarkets(idOrEntity, goodId, { user = game.user, limit = 3, sceneId = "" } = {}) {
        const state = this.getMarketState(idOrEntity, goodId, { sceneId });
        const projection = this.#getProjection(state.sceneId);
        const destinations = [];
        for (const basin of projection.basins) {
            if (basin.id === state.basin.id || basin.goods.has(String(goodId || ""))) continue;
            const market = basin.members
                .filter(location => user?.isGM || canUserViewLocationDetails(location.entityId, user))
                .map(location => {
                    const marketplace = getShopsForParentLocation(location.entityId)
                        .find(shop => shop.source?.templateId === MARKETPLACE_TEMPLATE_ID);
                    if (!marketplace) return null;
                    const rules = SettlementMarketplaceModel.getRules(marketplace, getLocation(location.entityId));
                    return rules.buysTradeGoods ? { ...location, rules } : null;
                })
                .filter(Boolean)
                .sort((left, right) => Number(right.rules.importMultiplier) - Number(left.rules.importMultiplier)
                    || Number(right.nodeId === basin.rootNodeId) - Number(left.nodeId === basin.rootNodeId)
                    || left.name.localeCompare(right.name))[0];
            if (!market) continue;
            destinations.push({
                entityId: market.entityId,
                name: market.name,
                basinName: basin.marketName,
                rootName: basin.rootName,
                ratePercent: Math.round(market.rules.importMultiplier * 100)
            });
        }
        return destinations
            .sort((left, right) => Number(right.ratePercent) - Number(left.ratePercent) || left.name.localeCompare(right.name))
            .slice(0, Math.max(0, Number(limit) || 0));
    }

    static getProjection(sceneId = "") {
        return this.#getProjection(sceneId);
    }

    static #getProjection(sceneId = "") {
        const normalizedSceneId = String(sceneId || "").trim();
        if (this.#projections.has(normalizedSceneId)) return this.#projections.get(normalizedSceneId);
        const locations = TradeRegionSceneService.getRegionLocations(normalizedSceneId, { locations: getLocations() })
            .filter(entity => entity?.type === "location")
            .map(entity => this.#locationNode(entity));
        const locationsByNodeId = new Map(locations.map(location => [location.nodeId, location]));
        const locationsByEntityId = new Map(locations.map(location => [location.entityId, location]));
        const edges = (getTradeNetwork({ includeHidden: true })?.edges || [])
            .filter(edge => locationsByNodeId.has(edge.locationNodeId) && locationsByNodeId.has(edge.centerNodeId));
        const adjacency = new Map(locations.map(location => [location.nodeId, new Set()]));
        const upstreamByNodeId = new Map();
        for (const edge of edges) {
            adjacency.get(edge.locationNodeId)?.add(edge.centerNodeId);
            adjacency.get(edge.centerNodeId)?.add(edge.locationNodeId);
            upstreamByNodeId.set(edge.locationNodeId, edge.centerNodeId);
        }
        for (const location of locations) location.centerNodeId = upstreamByNodeId.get(location.nodeId) || "";

        const basins = [];
        const basinsByNodeId = new Map();
        const visited = new Set();
        for (const location of locations) {
            if (visited.has(location.nodeId)) continue;
            const memberNodeIds = [];
            const queue = [location.nodeId];
            while (queue.length) {
                const nodeId = queue.shift();
                if (!nodeId || visited.has(nodeId)) continue;
                visited.add(nodeId);
                memberNodeIds.push(nodeId);
                for (const relatedId of adjacency.get(nodeId) || []) if (!visited.has(relatedId)) queue.push(relatedId);
            }
            const members = memberNodeIds.map(nodeId => locationsByNodeId.get(nodeId)).filter(Boolean);
            const roots = members.filter(member => !upstreamByNodeId.has(member.nodeId)).sort((left, right) => left.name.localeCompare(right.name));
            const root = roots[0] || [...members].sort((left, right) => left.name.localeCompare(right.name))[0];
            const goods = new Set(members.flatMap(member => member.produces));
            const producersByGoodId = new Map();
            for (const member of members) {
                for (const goodId of member.produces) {
                    const producers = producersByGoodId.get(goodId) || [];
                    producers.push(member);
                    producersByGoodId.set(goodId, producers);
                }
            }
            const basin = {
                id: root?.nodeId || location.nodeId,
                rootNodeId: root?.nodeId || location.nodeId,
                rootEntityId: root?.entityId || location.entityId,
                rootName: root?.name || location.name,
                marketName: `${root?.name || location.name} Trade Network`,
                members,
                goods,
                producersByGoodId
            };
            basins.push(basin);
            for (const member of members) basinsByNodeId.set(member.nodeId, basin);
        }
        const projection = { sceneId: normalizedSceneId, locations, locationsByNodeId, locationsByEntityId, edges, basins, basinsByNodeId };
        this.#projections.set(normalizedSceneId, projection);
        return projection;
    }

    static #locationNode(entity) {
        const target = getLocationConnectionTarget(entity);
        return {
            entityId: entity.id,
            nodeId: target?.id || `nexus-entity:${entity.id}`,
            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.#ids(entity.economy?.produces),
            centerNodeId: ""
        };
    }

    static #standaloneLocation(entity) {
        if (!entity) return null;
        return this.#locationNode(entity);
    }

    static #standaloneBasin(location) {
        const goods = new Set(location?.produces || []);
        const producersByGoodId = new Map([...goods].map(goodId => [goodId, [location]]));
        return {
            id: location?.nodeId || "",
            rootNodeId: location?.nodeId || "",
            rootEntityId: location?.entityId || "",
            rootName: location?.name || "Independent Market",
            marketName: `${location?.name || "Independent"} Trade Network`,
            members: location ? [location] : [],
            goods,
            producersByGoodId
        };
    }

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

function projectionLocation(projection, nodeId, user = game.user) {
    const location = projection.locationsByNodeId.get(nodeId);
    if (location && !user?.isGM && !canUserViewLocationDetails(location.entityId, user)) return null;
    return location ? {
        entityId: location.entityId,
        name: location.name,
        imageSrc: location.imageSrc,
        subtitle: location.subtitle
    } : null;
}
