import { ShopProductProviderRegistry } from "./ShopProductProviderRegistry.js";
import { ShopQuoteService } from "./ShopQuoteService.js";
import { ShopStore } from "./ShopStore.js";
import { ShopSystemAdapterRegistry } from "./ShopSystemAdapterRegistry.js";

const SOCKET_NAME = "module.augur-nexus";
const REQUEST_TIMEOUT_MS = 30000;

export class ShopTransactionService {
    static #pending = new Map();
    static #locks = new Map();
    static #registered = false;

    static registerSocket() {
        if (this.#registered) return;
        this.#registered = true;
        game.socket?.on(SOCKET_NAME, message => this.#onSocketMessage(message));
    }

    static async requestTrade({ shopId, walletUuid, inventoryUuid, purchaseLines = [], saleLines = [] }) {
        const request = {
            type: "nexusShopTradeRequest",
            requestId: foundry.utils.randomID(),
            transactionId: foundry.utils.randomID(),
            requestingUserId: game.user.id,
            shopId: String(shopId || ""),
            walletUuid: String(walletUuid || ""),
            inventoryUuid: String(inventoryUuid || walletUuid || ""),
            purchaseLines: this.#normalizePurchaseLines(purchaseLines),
            saleLines: this.#normalizeSaleLines(saleLines)
        };
        if (!request.purchaseLines.length && !request.saleLines.length) throw new Error("The trade ledger is empty.");
        if (game.user.isGM) {
            const result = await this.#executeTrade(request);
            this.#announceShopChange(shopId);
            return result;
        }
        const activeGM = game.users?.activeGM || game.users?.find(user => user.active && user.isGM);
        if (!activeGM) throw new Error("An active GM connection is required to update this Shop.");
        return new Promise((resolve, reject) => {
            const timeout = window.setTimeout(() => {
                this.#pending.delete(request.requestId);
                reject(new Error("The Shop transaction timed out."));
            }, REQUEST_TIMEOUT_MS);
            this.#pending.set(request.requestId, { resolve, reject, timeout });
            game.socket.emit(SOCKET_NAME, request);
        });
    }

    static async #onSocketMessage(message) {
        if (!message?.type) return;
        if (message.type === "nexusShopTradeRequest") {
            const activeGM = game.users?.activeGM || game.users?.find(user => user.active && user.isGM);
            if (!game.user.isGM || activeGM?.id !== game.user.id) return;
            let response;
            try {
                const result = await this.#executeTrade(message);
                response = {
                    type: "nexusShopTradeResult",
                    requestId: message.requestId,
                    userId: message.requestingUserId,
                    ok: true,
                    result
                };
                this.#announceShopChange(message.shopId);
            } catch (error) {
                console.error("Augur Nexus | Shop trade failed", error);
                response = {
                    type: "nexusShopTradeResult",
                    requestId: message.requestId,
                    userId: message.requestingUserId,
                    ok: false,
                    error: error?.message || "The Shop trade failed."
                };
            }
            game.socket.emit(SOCKET_NAME, response);
            return;
        }
        if (message.type === "nexusShopTradeResult" && message.userId === game.user.id) {
            const pending = this.#pending.get(message.requestId);
            if (!pending) return;
            this.#pending.delete(message.requestId);
            window.clearTimeout(pending.timeout);
            if (message.ok) pending.resolve(message.result);
            else pending.reject(new Error(message.error || "The Shop trade failed."));
            return;
        }
        if (message.type === "nexusShopChanged" && message.shopId) {
            Hooks.callAll("augurNexusShopInventoryChanged", { shopId: message.shopId });
        }
    }

    static async #executeTrade(request) {
        return this.#withLock(`shop:${request.shopId}`, async () => {
            const adapter = ShopSystemAdapterRegistry.getActive();
            if (!adapter) throw new Error(`Shop transactions are not supported for ${game.system.title}.`);
            const requestingUser = game.users.get(request.requestingUserId);
            if (!requestingUser) throw new Error("The trading player could not be found.");

            const wallet = await adapter.resolveActor(request.walletUuid);
            if (!adapter.isEligibleWallet(wallet, requestingUser)) {
                throw new Error("Choose an owned wallet supported by the active game system.");
            }
            const inventoryActor = await adapter.resolveActor(request.inventoryUuid || request.walletUuid);
            if (!adapter.isEligibleInventoryHolder(inventoryActor, requestingUser)) {
                throw new Error("Choose an owned trading inventory.");
            }

            const shop = ShopStore.getShop(request.shopId);
            if (!shop) throw new Error("The Shop no longer exists.");
            if (shop.processedTransactionIds.includes(request.transactionId)) {
                return { transactionId: request.transactionId, duplicate: true, purchaseTotal: 0, saleTotal: 0, netAmount: 0 };
            }

            const purchaseLines = this.#normalizePurchaseLines(request.purchaseLines);
            const saleLines = this.#normalizeSaleLines(request.saleLines);
            if (purchaseLines.length && !shop.configuration.buyingEnabled) {
                throw new Error("This Shop is not selling merchandise.");
            }
            if (saleLines.length && !shop.configuration.sellingEnabled) {
                throw new Error("This Shop is not buying items.");
            }

            const rowsByKey = new Map(shop.inventory.map(row => [this.#productKey(row), row]));
            const purchases = [];
            let purchaseTotal = 0;
            for (const line of purchaseLines) {
                const row = rowsByKey.get(this.#productKey(line));
                if (!row) throw new Error("One of the selected Shop products is no longer available.");
                if (shop.configuration.finiteStock && row.quantity < line.quantity) {
                    throw new Error("One of the selected products no longer has enough stock.");
                }
                const product = await ShopProductProviderRegistry.resolve(line.providerId, line.productId, { shop, row });
                if (!product) throw new Error("One of the selected products is no longer available.");
                const quote = await ShopQuoteService.quoteCustomerPurchase({ shop, row, product });
                if (!quote.allowed) throw new Error(quote.reason || `${product.name} is not available for purchase.`);
                const priceEach = quote.amount;
                purchaseTotal += priceEach * line.quantity;
                purchases.push({ line, row, product, quote, priceEach });
            }

            const sales = [];
            let saleTotal = 0;
            for (const line of saleLines) {
                const item = inventoryActor.items?.get(line.itemId);
                if (!item) throw new Error("One of the selected inventory items no longer exists.");
                const available = Math.max(1, Math.floor(Number(adapter.getItemQuantity(item)) || 1));
                if (line.quantity > available) throw new Error(`${item.name} no longer has enough quantity to sell.`);
                const adapterQuote = await adapter.getSaleQuote(item, { actor: inventoryActor, shop });
                const quote = await ShopQuoteService.quoteCustomerSale({ shop, item, actor: inventoryActor, adapterQuote });
                if (!quote.allowed) throw new Error(quote.reason || `${item.name} cannot be sold here.`);
                const priceEach = quote.amount;
                saleTotal += priceEach * line.quantity;
                sales.push({ line, item, available, quote, priceEach });
            }

            const netAmount = purchaseTotal - saleTotal;
            if (netAmount > 0 && !adapter.canAfford(wallet, netAmount, { currencyId: shop.configuration.currencyId })) {
                throw new Error(`${wallet.name} cannot afford ${adapter.formatPrice(netAmount)}.`);
            } else if (netAmount < 0 && !shop.treasury.unlimited && shop.treasury.amount < Math.abs(netAmount)) {
                throw new Error(`This Shop only has ${adapter.formatPrice(shop.treasury.amount)} available.`);
            }

            const purchasedItemData = [];
            for (const purchase of purchases) {
                purchasedItemData.push(await adapter.buildItemData(purchase.product, purchase.line.quantity, {
                    shopUuid: shop.uuid,
                    priceEach: purchase.priceEach,
                    transactionId: request.transactionId,
                    walletUuid: wallet.uuid,
                    inventoryUuid: inventoryActor.uuid
                }));
            }

            const acquiredProducts = [];
            for (const sale of sales) {
                if (sale.quote.disposition === "consume") continue;
                acquiredProducts.push(await adapter.createSaleProduct(sale.item, sale.line.quantity, {
                    shop,
                    transactionId: request.transactionId,
                    sellerUuid: inventoryActor.uuid,
                    acquisitionPrice: sale.priceEach
                }));
            }

            const actorTrade = await adapter.createActorTradeSession({
                wallet,
                inventoryActor,
                purchasedItemData,
                sales,
                netAmount,
                currencyId: shop.configuration.currencyId,
                transactionId: request.transactionId
            });
            if (!actorTrade || typeof actorTrade.apply !== "function" || typeof actorTrade.rollback !== "function") {
                throw new Error("The active game system returned an invalid Shop transaction session.");
            }
            try {
                await actorTrade.apply();

                const inventory = foundry.utils.deepClone(shop.inventory);
                if (shop.configuration.finiteStock) {
                    for (const purchase of purchases) {
                        const row = inventory.find(candidate => candidate.id === purchase.row.id);
                        if (row) row.quantity -= purchase.line.quantity;
                    }
                }
                const providerData = foundry.utils.deepClone(shop.providerData || {});
                for (const acquired of acquiredProducts) {
                    if (!acquired?.providerId || !acquired?.productId || !acquired?.productData) {
                        throw new Error("The active game system could not preserve a sold item.");
                    }
                    providerData[acquired.providerId] ||= {};
                    providerData[acquired.providerId][acquired.productId] = foundry.utils.deepClone(acquired.productData);
                    inventory.push({
                        id: foundry.utils.randomID(),
                        providerId: acquired.providerId,
                        productId: acquired.productId,
                        quantity: Math.max(1, Number(acquired.quantity) || 1),
                        priceOverride: null,
                        pinned: false,
                        generated: false
                    });
                }
                const treasuryAmount = shop.treasury.unlimited
                    ? shop.treasury.amount
                    : Math.max(0, shop.treasury.amount + netAmount);
                await ShopStore.updateShop(shop.id, {
                    inventory: inventory.filter(row => !shop.configuration.finiteStock || row.quantity > 0),
                    providerData,
                    treasury: { ...shop.treasury, amount: treasuryAmount },
                    revision: shop.revision + 1,
                    processedTransactionIds: [...shop.processedTransactionIds, request.transactionId].slice(-50)
                });
                await actorTrade.commit?.();
            } catch (error) {
                try { await actorTrade.rollback(); }
                catch (rollbackError) { console.error("Augur Nexus | Shop actor rollback failed", rollbackError); }
                throw error;
            }

            return {
                transactionId: request.transactionId,
                walletName: wallet.name,
                inventoryName: inventoryActor.name,
                purchaseTotal,
                saleTotal,
                netAmount,
                purchaseLabel: adapter.formatPrice(purchaseTotal),
                saleLabel: adapter.formatPrice(saleTotal),
                netLabel: adapter.formatPrice(Math.abs(netAmount)),
                purchasedCount: purchaseLines.reduce((sum, line) => sum + line.quantity, 0),
                soldCount: saleLines.reduce((sum, line) => sum + line.quantity, 0)
            };
        });
    }


    static #normalizePurchaseLines(lines) {
        const quantities = new Map();
        for (const line of Array.isArray(lines) ? lines : []) {
            const providerId = String(line?.providerId || "");
            const productId = String(line?.productId || "");
            const quantity = Math.max(0, Math.floor(Number(line?.quantity) || 0));
            if (!providerId || !productId || !quantity) continue;
            const key = this.#productKey({ providerId, productId });
            quantities.set(key, { providerId, productId, quantity: Math.min(1000, (quantities.get(key)?.quantity || 0) + quantity) });
        }
        return Array.from(quantities.values());
    }

    static #normalizeSaleLines(lines) {
        const quantities = new Map();
        for (const line of Array.isArray(lines) ? lines : []) {
            const itemId = String(line?.itemId || "");
            const quantity = Math.max(0, Math.floor(Number(line?.quantity) || 0));
            if (!itemId || !quantity) continue;
            quantities.set(itemId, { itemId, quantity: Math.min(1000, (quantities.get(itemId)?.quantity || 0) + quantity) });
        }
        return Array.from(quantities.values());
    }

    static #productKey(line) {
        return `${line.providerId || ""}::${line.productId || ""}`;
    }

    static async #withLock(key, operation) {
        const previous = this.#locks.get(key) || Promise.resolve();
        const current = previous.catch(() => {}).then(operation);
        this.#locks.set(key, current);
        try { return await current; }
        finally { if (this.#locks.get(key) === current) this.#locks.delete(key); }
    }

    static #announceShopChange(shopId) {
        Hooks.callAll("augurNexusShopInventoryChanged", { shopId });
        game.socket?.emit(SOCKET_NAME, { type: "nexusShopChanged", shopId });
    }
}
