import { MAX_MARKETPLACE_GOODS_PER_ROLE, SettlementMarketplaceModel } from "../features/trade/models/SettlementMarketplaceModel.js";
import { TradeGoodsCatalog } from "../features/trade/services/TradeGoodsCatalog.js";

const API_VERSION = 1;
const ECONOMY_OPERATIONS = new Set([
    "add-production", "remove-production", "replace-production",
    "add-stock", "remove-stock", "set-stock-quantity", "adjust-stock-quantity",
    "set-stock-price", "adjust-stock-price", "clear-stock-price"
]);

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 a settlement economy."); }

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}`;
}

async function nexusApis() {
    const [locations, shops] = await Promise.all([
        import("/modules/augur-nexus/scripts/api/locations.js"),
        import("/modules/augur-nexus/scripts/api/shops.js")
    ]);
    return { locations, shops };
}

function marketplaceFor(shops = []) {
    return shops.find(shop => SettlementMarketplaceModel.isMarketplace(shop)) || null;
}

function producedIds(entity = {}) {
    return SettlementMarketplaceModel.getEconomy(entity).produces.map(entry => entry.goodId);
}

function goodView(good = {}) {
    return {
        id: text(good.id), catalogKey: text(good.catalogKey), providerId: text(good.providerId), providerLabel: text(good.providerLabel),
        name: text(good.name), familyId: text(good.familyId), valueBand: text(good.valueBand),
        tradeValueGp: Number(good.tradeValue || 0), imageSrc: text(good.imageSrc), lot: clone(good.lot || {}),
        sourceIndustryIds: clone(good.sourceIndustryIds || []), demandCategoryIds: clone(good.demandCategoryIds || []),
        builtIn: good.builtIn === true, custom: good.custom === true, archived: good.archived === true
    };
}

function slug(value) {
    return text(value).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLocaleLowerCase()
        .replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 36);
}

function customGoodInput(source = {}) {
    const name = text(source.name);
    if (!name) throw new Error("A custom trade good requires a name.");
    const list = value => [...new Set((Array.isArray(value) ? value : []).map(text).filter(Boolean))].slice(0, 24);
    return {
        id: text(source.id) || `custom-${slug(name) || "trade-good"}-${foundry.utils.randomID(6).toLocaleLowerCase()}`,
        name,
        familyId: text(source.familyId) || "custom",
        valueBand: text(source.valueBand) || "custom",
        tradeValue: money(source.tradeValueGp ?? source.tradeValue ?? 0),
        imageSrc: text(source.imageSrc),
        lot: {
            label: text(source.lot?.label) || `Lots of ${name}`,
            description: text(source.lot?.description) || "A trade shipment managed by the GM.",
            weightLb: money(source.lot?.weightLb ?? 0)
        },
        sourceIndustryIds: list(source.sourceIndustryIds),
        demandCategoryIds: list(source.demandCategoryIds),
        custom: true,
        archived: false
    };
}

function stockView(row = {}, goodsById = new Map()) {
    const good = goodsById.get(row.productId);
    return {
        rowId: text(row.id), goodId: text(row.productId), name: good?.name || text(row.productId),
        quantity: Number(row.quantity || 0), priceOverrideGp: row.priceOverride === null || row.priceOverride === undefined ? null : Number(row.priceOverride) / 100,
        basePriceGp: Number(good?.tradeValue || 0), generated: row.generated === true, pinned: row.pinned === true
    };
}

export async function inspectSettlementEconomy(locationId) {
    const id = text(locationId);
    const { locations, shops } = await nexusApis();
    const entity = locations.getLocation(id);
    if (!entity?.id || entity.type !== "location") throw new Error(`Settlement could not be found: ${id || "missing ID"}.`);
    const goods = await TradeGoodsCatalog.getGoods({ includeArchived: false });
    const goodsById = new Map(goods.map(good => [good.id, good]));
    const marketplace = marketplaceFor(shops.getShopsForParentLocation(entity.id));
    const inventory = (marketplace?.inventory || []).filter(row => goodsById.has(row.productId));
    const economy = clone(entity.economy || SettlementMarketplaceModel.toEconomy([]));
    const state = {
        economy,
        marketplace: marketplace ? { id: marketplace.id, revision: Number(marketplace.revision || 0), inventory: clone(marketplace.inventory || []) } : null
    };
    return {
        apiVersion: API_VERSION,
        location: { id: entity.id, uuid: text(entity.uuid), name: text(entity.display?.name || entity.id), typeId: text(entity.generation?.templateId || entity.profile?.type) },
        catalog: goods.map(goodView),
        production: producedIds(entity).map(goodId => goodView(goodsById.get(goodId) || { id: goodId, name: goodId })),
        marketplace: marketplace ? {
            id: marketplace.id, name: marketplace.name, revision: Number(marketplace.revision || 0),
            rules: SettlementMarketplaceModel.getRules(marketplace, entity),
            stock: inventory.map(row => stockView(row, goodsById))
        } : null,
        fingerprint: fingerprint(state)
    };
}

export async function inspectFantasyTradeGoods() {
    const goods = await TradeGoodsCatalog.getGoods({ includeArchived: false });
    return {
        apiVersion: API_VERSION,
        goods: goods.map(goodView),
        marketRates: TradeGoodsCatalog.getMarketRates(),
        limits: { producedGoods: MAX_MARKETPLACE_GOODS_PER_ROLE }
    };
}

export async function planCustomTradeGoodCreation(input = {}) {
    assertGm();
    const catalog = await TradeGoodsCatalog.getGoods({ includeArchived: true });
    const plannedGood = customGoodInput(input.good || input);
    const duplicate = catalog.find(good => good.name.toLocaleLowerCase() === plannedGood.name.toLocaleLowerCase());
    if (duplicate) throw new Error(`The catalog already has a trade good named '${duplicate.name}' (${duplicate.id}).`);
    return {
        apiVersion: API_VERSION,
        planId: `trade-good.${foundry.utils.randomID()}`,
        kind: "trade-good",
        operation: "create",
        target: goodView(plannedGood),
        sourceFingerprint: fingerprint(TradeGoodsCatalog.getWorldCatalog()),
        summary: `Create ${plannedGood.name} as a global custom trade good`,
        effects: [{ operation: "create-custom-good", goodId: plannedGood.id, name: plannedGood.name, tradeValueGp: plannedGood.tradeValue }],
        payload: { good: clone(plannedGood) }
    };
}

export async function applyCustomTradeGoodPlan(plan = {}) {
    assertGm();
    if (plan.apiVersion !== API_VERSION || plan.kind !== "trade-good" || plan.operation !== "create") {
        throw new Error("Unsupported trade-good plan.");
    }
    if (fingerprint(TradeGoodsCatalog.getWorldCatalog()) !== plan.sourceFingerprint) {
        throw new Error("The global trade-good catalog changed after this plan was prepared. Refresh it before applying.");
    }
    const existing = await TradeGoodsCatalog.getGoods({ includeArchived: true });
    const plannedGood = customGoodInput(plan.payload?.good || {});
    if (existing.some(good => good.id === plannedGood.id || good.name.toLocaleLowerCase() === plannedGood.name.toLocaleLowerCase())) {
        throw new Error(`A trade good matching '${plannedGood.name}' already exists.`);
    }
    const saved = await TradeGoodsCatalog.saveCustomGood(plannedGood);
    return { ok: true, planId: plan.planId, ...goodView(saved), entityType: "trade-good", entityId: saved.id };
}

export async function applyTradeGoodImage(goodId, imageSrc) {
    assertGm();
    const id = text(goodId);
    const src = text(imageSrc);
    if (!id || !src) throw new Error("Applying a trade-good image requires an exact good ID and uploaded image path.");
    const saved = await TradeGoodsCatalog.setImage(id, src);
    if (!saved?.id) throw new Error(`Could not update trade good '${id}'.`);
    return { ok: true, ...goodView(saved), entityType: "trade-good", entityId: saved.id };
}

function integer(value, minimum = 0, maximum = 999999) {
    const number = Math.floor(Number(value));
    if (!Number.isFinite(number)) throw new Error("Economy quantities require finite numbers.");
    return Math.max(minimum, Math.min(maximum, number));
}

function money(value) {
    const number = Number(value);
    if (!Number.isFinite(number)) throw new Error("Trade-good prices require finite GP amounts.");
    return Math.max(0, Math.min(1_000_000, Math.round(number * 100) / 100));
}

function applyEconomyOperations(inspection, operations = []) {
    if (!Array.isArray(operations) || !operations.length) throw new Error("An economy plan requires at least one operation.");
    const knownGoods = new Set(inspection.catalog.map(good => good.id));
    const production = new Set(inspection.production.map(good => good.id));
    const inventory = clone(inspection.marketplace?.stock || []);
    const effects = [];
    let productionChanged = false;
    let stockChanged = false;

    for (const [index, requested] of operations.entries()) {
        const operation = text(requested?.operation);
        const goodId = text(requested?.goodId);
        if (!ECONOMY_OPERATIONS.has(operation)) throw new Error(`Unsupported economy operation at index ${index}: ${operation || "missing"}.`);
        if (operation === "replace-production") {
            const nextIds = [...new Set((Array.isArray(requested.goodIds) ? requested.goodIds : []).map(text).filter(Boolean))];
            if (nextIds.some(id => !knownGoods.has(id))) throw new Error("Replacement production contains an unknown trade-good ID.");
            if (nextIds.length > MAX_MARKETPLACE_GOODS_PER_ROLE) throw new Error(`A settlement can produce up to ${MAX_MARKETPLACE_GOODS_PER_ROLE} trade goods.`);
            production.clear();
            nextIds.forEach(id => production.add(id));
            productionChanged = true;
            effects.push({ operation, goodIds: nextIds });
            continue;
        }
        if (!knownGoods.has(goodId)) throw new Error(`Unknown trade-good ID: ${goodId || "missing"}.`);
        if (operation === "add-production" || operation === "remove-production") {
            if (operation === "add-production") production.add(goodId);
            else production.delete(goodId);
            if (production.size > MAX_MARKETPLACE_GOODS_PER_ROLE) throw new Error(`A settlement can produce up to ${MAX_MARKETPLACE_GOODS_PER_ROLE} trade goods.`);
            productionChanged = true;
            effects.push({ operation, goodId });
            continue;
        }
        if (!inspection.marketplace) throw new Error("Stock and local price operations require a settlement Marketplace.");
        const rowIndex = inventory.findIndex(row => row.goodId === goodId);
        if (operation === "add-stock") {
            if (rowIndex >= 0) throw new Error(`${goodId} is already stocked at this Marketplace.`);
            inventory.push({ rowId: `planned.${foundry.utils.randomID()}`, goodId, quantity: integer(requested.quantity, 0), priceOverrideGp: requested.priceGp === null || requested.priceGp === undefined ? null : money(requested.priceGp), generated: false, pinned: true });
        } else {
            if (rowIndex < 0) throw new Error(`${goodId} is not stocked at this Marketplace.`);
            const row = inventory[rowIndex];
            if (operation === "remove-stock") inventory.splice(rowIndex, 1);
            else if (operation === "set-stock-quantity") row.quantity = integer(requested.quantity, 0);
            else if (operation === "adjust-stock-quantity") row.quantity = integer(row.quantity + Number(requested.quantity || 0), 0);
            else if (operation === "set-stock-price") row.priceOverrideGp = money(requested.priceGp);
            else if (operation === "clear-stock-price") row.priceOverrideGp = null;
            else if (operation === "adjust-stock-price") {
                const basis = row.priceOverrideGp ?? row.basePriceGp;
                row.priceOverrideGp = requested.adjustmentMode === "percent"
                    ? money(basis * (1 + Number(requested.amount || 0) / 100))
                    : money(basis + Number(requested.amount || 0));
            }
        }
        stockChanged = true;
        effects.push({ operation, goodId, quantity: inventory.find(row => row.goodId === goodId)?.quantity ?? 0, priceOverrideGp: inventory.find(row => row.goodId === goodId)?.priceOverrideGp ?? null });
    }
    return { production: [...production], inventory, effects, productionChanged, stockChanged };
}

export async function planSettlementEconomyChanges({ locationId, operations = [] } = {}) {
    assertGm();
    const inspection = await inspectSettlementEconomy(locationId);
    const planned = applyEconomyOperations(inspection, operations);
    return {
        apiVersion: API_VERSION,
        planId: `fantasy-economy.${foundry.utils.randomID()}`,
        kind: "settlement-economy",
        operation: "update",
        target: clone(inspection.location),
        sourceFingerprint: inspection.fingerprint,
        summary: `${planned.effects.length} economy change${planned.effects.length === 1 ? "" : "s"} for ${inspection.location.name}`,
        effects: planned.effects,
        payload: {
            production: planned.production,
            inventory: planned.inventory,
            productionChanged: planned.productionChanged,
            stockChanged: planned.stockChanged,
            marketplaceId: inspection.marketplace?.id || ""
        }
    };
}

export async function applySettlementEconomyPlan(plan = {}) {
    assertGm();
    if (plan.apiVersion !== API_VERSION || plan.kind !== "settlement-economy") throw new Error("Unsupported settlement economy plan.");
    const inspection = await inspectSettlementEconomy(plan.target?.id);
    if (inspection.fingerprint !== plan.sourceFingerprint) throw new Error("The settlement economy changed after this plan was prepared. Refresh it before applying.");
    const { locations, shops } = await nexusApis();
    if (plan.payload?.productionChanged) {
        const current = locations.getLocation(inspection.location.id);
        await locations.updateLocation(current.id, { economy: SettlementMarketplaceModel.toEconomy(plan.payload.production || []) }, { syncMarkers: false });
    }
    if (plan.payload?.stockChanged) {
        const marketplace = inspection.marketplace;
        if (!marketplace || marketplace.id !== plan.payload.marketplaceId) throw new Error("The planned Marketplace is no longer available.");
        const current = shops.getShop(marketplace.id);
        const catalog = await TradeGoodsCatalog.getGoods({ includeArchived: true });
        const goodsById = new Map(catalog.map(good => [good.id, good]));
        const unrelated = (current.inventory || []).filter(row => !goodsById.has(row.productId));
        const inventory = (plan.payload.inventory || []).map(row => ({
            id: row.rowId.startsWith("planned.") ? foundry.utils.randomID() : row.rowId,
            providerId: goodsById.get(row.goodId)?.providerId || "augur-nexus:custom-trade-goods",
            productId: row.goodId,
            quantity: integer(row.quantity, 0),
            priceOverride: row.priceOverrideGp === null ? null : Math.round(money(row.priceOverrideGp) * 100),
            pinned: row.pinned !== false,
            generated: row.generated === true
        }));
        await shops.updateShop(current.id, { inventory: [...unrelated, ...inventory] });
    }
    return { ok: true, planId: plan.planId, locationId: inspection.location.id, id: inspection.location.id, name: inspection.location.name, effectCount: plan.effects?.length || 0 };
}

export function getFantasyEconomyCapability() {
    return {
        apiVersion: API_VERSION,
        id: "augur-nexus.settlement-economy",
        subjects: ["location", "settlement", "marketplace", "trade-good"],
        operations: ["inspect", "plan", "apply"],
        changes: [...ECONOMY_OPERATIONS],
        limits: { producedGoods: MAX_MARKETPLACE_GOODS_PER_ROLE }
    };
}

export function getFantasyTradeGoodsCapability() {
    return {
        apiVersion: API_VERSION,
        id: "augur-nexus.trade-goods-catalog",
        subjects: ["trade-good", "global-catalog"],
        operations: ["inspect", "plan-create", "apply-create", "apply-image"],
        assetSlots: [{ id: "icon", label: "Trade Good Image", displayField: "imageSrc", aspectRatio: "1:1" }]
    };
}

export const __economyTestUtils = Object.freeze({ fingerprint, applyEconomyOperations });

export const inspectTradeGoodsCatalog = inspectFantasyTradeGoods;
export const getEconomyCapability = getFantasyEconomyCapability;
export const getTradeGoodsCapability = getFantasyTradeGoodsCapability;
