import { ShopModel } from "../models/ShopModel.js";
import { ShopRestockProfileModel } from "../models/ShopRestockProfileModel.js";
import { ShopCatalogFilterService } from "../services/ShopCatalogFilterService.js";
import { ShopPresetRegistry } from "../services/ShopPresetRegistry.js";
import { ShopIconCatalogRegistry } from "../services/ShopIconCatalogRegistry.js";
import { ShopProductProviderRegistry } from "../services/ShopProductProviderRegistry.js";
import { ShopRestockService } from "../services/ShopRestockService.js";
import { ShopStore } from "../services/ShopStore.js";
import { ShopPresetDraftService } from "../services/ShopPresetDraftService.js";
import { ShopSystemAdapterRegistry } from "../services/ShopSystemAdapterRegistry.js";
import { LocationEntityStore } from "../../campaign-entities/services/LocationEntityStore.js";
import { ShopIconPicker } from "./ShopIconPicker.js";

const { ApplicationV2, HandlebarsApplicationMixin, DialogV2 } = foundry.applications.api;

export class ShopEditorApplication extends HandlebarsApplicationMixin(ApplicationV2) {
    #shopId = "";
    #shop = null;
    #locationId = "";
    #onSaved = null;
    #onReady = null;
    #didSignalReady = false;
    #draft = null;
    #tab = "settings";
    #dirty = false;
    #catalog = [];
    #packOptions = [];
    #filteredCatalog = [];
    #query = "";
    #stockQuery = "";
    #stockFilter = "all";
    #selectedAvailable = new Set();
    #collapsedGroups = new Set();
    #visibleLimit = 100;
    #searchTimer = null;
    #restoreSearchFocus = false;
    #initializedCatalog = false;
    #advancedPoolEdited = false;

    static DEFAULT_OPTIONS = {
        id: "augur-nexus-shop-editor",
        classes: ["augur-nexus", "nexus-shop-editor-window"],
        tag: "div",
        window: { title: "Shop Editor", resizable: true, minimizable: true },
        position: { width: 1280, height: 820 },
        actions: {
            selectTab: ShopEditorApplication._onSelectTab,
            saveShop: ShopEditorApplication._onSaveShop,
            cancel: ShopEditorApplication._onCancel,
            applyShopType: ShopEditorApplication._onApplyShopType,
            changeShopIcon: ShopEditorApplication._onChangeShopIcon,
            refreshGenerated: ShopEditorApplication._onRefreshGenerated,
            replaceStock: ShopEditorApplication._onReplaceStock,
            filterStock: ShopEditorApplication._onFilterStock,
            changeStockQuantity: ShopEditorApplication._onChangeStockQuantity,
            editStockPrice: ShopEditorApplication._onEditStockPrice,
            togglePinnedStock: ShopEditorApplication._onTogglePinnedStock,
            removeStockItem: ShopEditorApplication._onRemoveStockItem,
            addItem: ShopEditorApplication._onAddItem,
            removePoolItem: ShopEditorApplication._onRemovePoolItem,
            addSelected: ShopEditorApplication._onAddSelected,
            addAll: ShopEditorApplication._onAddAll,
            loadMore: ShopEditorApplication._onLoadMore,
            cycleFilter: ShopEditorApplication._onCycleFilter,
            clearFilters: ShopEditorApplication._onClearFilters,
            clearPool: ShopEditorApplication._onClearPool,
            removeMissing: ShopEditorApplication._onRemoveMissing,
            importPool: ShopEditorApplication._onImportPool,
            exportPool: ShopEditorApplication._onExportPool
        }
    };

    static PARTS = { content: { template: "modules/augur-nexus/templates/features/shops/shop-editor.hbs" } };

    static show({ shopId = "", locationId = "", onSaved = null, onReady = null } = {}) {
        if (!game.user?.isGM) return null;
        const app = new this({ shopId, locationId, onSaved, onReady });
        app.render(true, { focus: true });
        return app;
    }

    constructor({ shopId = "", locationId = "", onSaved = null, onReady = null } = {}, options = {}) {
        super(options);
        this.#shopId = String(shopId || "");
        this.#locationId = String(locationId || "");
        this.#onSaved = onSaved;
        this.#onReady = typeof onReady === "function" ? onReady : null;
    }

    get title() {
        if (!this.#draft) return this.#shopId ? "Edit Shop" : "Create Shop";
        return `${this.#shopId ? "Edit" : "Create"} Shop — ${this.#draft.name}`;
    }

    async _prepareContext() {
        if (!this.#draft) await this.#initializeDraft();
        const providers = ShopProductProviderRegistry.listCatalogProviders();
        if (!this.#initializedCatalog) await this.#loadCatalog(providers);
        const adapter = ShopSystemAdapterRegistry.getActive();
        const preset = ShopPresetRegistry.get(this.#draft.restockProfile.preset.id);
        const defaultIcon = await ShopIconCatalogRegistry.getIcon(preset?.defaultImageId);
        const location = this.#locationId ? LocationEntityStore.getLocation(this.#locationId) : ShopStore.getParentLocation(this.#draft);
        const base = {
            missing: this.#shopId && !this.#shop,
            isNew: !this.#shopId,
            draft: this.#settingsView(),
            tab: this.#tab,
            isPoolTab: this.#tab === "restock",
            isSettingsTab: this.#tab === "settings",
            dirty: this.#dirty,
            preset,
            defaultIcon,
            presets: ShopPresetRegistry.list().map(row => ({ ...row, selected: row.id === preset?.id })),
            isAdvancedCustomized: this.#advancedPoolEdited,
            locationName: location?.display?.name || "Independent Shop",
            adapterLabel: adapter?.label || game.system?.title || "Game system",
            canDropItems: typeof adapter?.createStockProduct === "function"
        };
        if (this.#tab === "restock") return { ...base, ...(await this.#poolContext(providers, adapter)) };
        return { ...base, ...(await this.#stockContext(adapter)) };
    }

    _onRender(context, options) {
        super._onRender(context, options);
        if (this.window?.title) this.window.title.textContent = this.title;
        if (!this.#didSignalReady) {
            this.#didSignalReady = true;
            this.#onReady?.(this);
        }
        for (const input of this.element.querySelectorAll("[data-shop-setting]")) {
            const eventName = input.matches('select, input[type="checkbox"]') ? "change" : "input";
            input.addEventListener(eventName, () => { this.#captureFields(); this.#markDirty(); });
        }
        for (const details of this.element.querySelectorAll("[data-filter-group-details]")) details.addEventListener("toggle", () => {
            const id = String(details.dataset.groupId || "");
            if (details.open) this.#collapsedGroups.delete(id); else this.#collapsedGroups.add(id);
        });
        for (const button of this.element.querySelectorAll('[data-action="cycleFilter"]')) button.addEventListener("contextmenu", event => {
            event.preventDefault(); this.#cycleFilter(button, -1);
        });
        for (const input of this.element.querySelectorAll('[name="catalogType"]')) input.addEventListener("change", () => {
            this.#draft.restockProfile.discovery.activeTypes = [...this.element.querySelectorAll('[name="catalogType"]:checked')].map(control => control.value);
            this.#advancedPoolEdited = true; this.#resetCatalogView(); this.#markProfileDirty(); this.render({ parts: ["content"] });
        });
        for (const input of this.element.querySelectorAll('[name="priceMinimumGp"], [name="priceMaximumGp"]')) input.addEventListener("change", () => {
            this.#capturePrice(); this.#advancedPoolEdited = true; this.#resetCatalogView(); this.#markProfileDirty(); this.render({ parts: ["content"] });
        });
        for (const input of this.element.querySelectorAll('[name="availableSelection"]')) input.addEventListener("change", event => {
            if (event.currentTarget.checked) this.#selectedAvailable.add(event.currentTarget.value);
            else this.#selectedAvailable.delete(event.currentTarget.value);
            const button = this.element.querySelector('[data-action="addSelected"]');
            if (button) button.disabled = !this.#selectedAvailable.size;
        });
        for (const input of this.element.querySelectorAll("[data-pool-entry-setting]")) input.addEventListener("change", event => this.#updatePoolEntry(event.currentTarget));
        this.#bindSearch("catalogSearch", value => { this.#query = value; this.#resetCatalogView(false); }, true);
        this.#bindSearch("stockSearch", value => { this.#stockQuery = value; this.#applyStockFilters(); });
        const importInput = this.element.querySelector("[data-restock-import]");
        importInput?.addEventListener("change", event => this.#readImportFile(event.currentTarget.files?.[0]));
        this.#activateStockDrop();
        this.#applyStockFilters();
    }

    static _onSelectTab(_event, target) { this.#captureFields(); this.#tab = String(target.dataset.tab || "settings"); this.render({ parts: ["content"] }); }
    static async _onSaveShop() { await this.#save(); }
    static async _onCancel() { await this.close(); }
    static async _onApplyShopType() { await this.#applyShopType(); }
    static async _onChangeShopIcon() { await this.#openIconPicker(); }
    static async _onRefreshGenerated() { await this.#generate(false); }
    static async _onReplaceStock() {
        if (await DialogV2.confirm({ window: { title: "Replace All Current Stock?" }, content: "<p>This removes generated, pinned, manually added, and player-sold merchandise before generating new stock.</p>", yes: { label: "Replace Everything", icon: "fas fa-triangle-exclamation" }, no: { label: "Cancel" } })) await this.#generate(true);
    }
    static _onFilterStock(_event, target) { this.#stockFilter = String(target.dataset.filter || "all"); this.render({ parts: ["content"] }); }
    static _onChangeStockQuantity(_event, target) {
        const row = this.#draft.inventory.find(item => item.id === target.dataset.rowId);
        if (!row) return;
        row.quantity = Math.max(0, row.quantity + Number(target.dataset.delta || 0));
        if (!row.quantity) this.#removeStockRow(row.id); else this.#markDirty();
        this.render({ parts: ["content"] });
    }
    static async _onEditStockPrice(_event, target) {
        const row = this.#draft.inventory.find(item => item.id === target.dataset.rowId);
        if (!row) return;
        const current = row.priceOverride === null ? "" : this.#copperToGp(row.priceOverride);
        const value = await DialogV2.prompt({
            window: { title: "Set Shop Price" },
            content: `<label>Price override in gp <input type="number" name="priceGp" min="0" step="0.01" value="${current}" placeholder="Use source price" autofocus></label><p>Leave this blank to use the price from the source Item.</p>`,
            rejectClose: false,
            ok: { label: "Apply Price", callback: (_event, button) => String(button.form.elements.priceGp?.value ?? "") }
        });
        if (value === null) return;
        row.priceOverride = value === "" ? null : Math.max(0, Math.round((Number(value) || 0) * 100));
        this.#markDirty(); this.render({ parts: ["content"] });
    }
    static _onTogglePinnedStock(_event, target) { const row = this.#draft.inventory.find(item => item.id === target.dataset.rowId); if (row) { row.pinned = !row.pinned; this.#markDirty(); this.render({ parts: ["content"] }); } }
    static _onRemoveStockItem(_event, target) { this.#removeStockRow(target.dataset.rowId); this.render({ parts: ["content"] }); }
    static _onAddItem(_event, target) { this.#addPoolKeys([target.dataset.productKey]); }
    static _onRemovePoolItem(_event, target) { this.#removePoolKeys([target.dataset.productKey]); }
    static _onAddSelected() { this.#addPoolKeys([...this.#selectedAvailable]); }
    static _onAddAll() { this.#addPoolKeys(this.#searchCatalog(this.#filteredCatalog).map(product => `${product.providerId}::${product.productId}`)); }
    static _onLoadMore() { this.#visibleLimit += 100; this.render({ parts: ["content"] }); }
    static _onCycleFilter(_event, target) { this.#cycleFilter(target, 1); }
    static _onClearFilters() { this.#draft.restockProfile.discovery.filterState = {}; this.#advancedPoolEdited = true; this.#markProfileDirty(); this.render({ parts: ["content"] }); }
    static async _onClearPool() {
        if (!this.#draft.restockProfile.entries.length) return;
        if (await DialogV2.confirm({ window: { title: "Clear Restock Pool" }, content: `<p>Remove all <strong>${this.#draft.restockProfile.entries.length}</strong> potential-stock references?</p>`, yes: { label: "Clear Pool", icon: "fas fa-trash" }, no: { label: "Cancel" } })) { this.#draft.restockProfile.entries = []; this.#advancedPoolEdited = true; this.#markProfileDirty(); this.render({ parts: ["content"] }); }
    }
    static async _onRemoveMissing() {
        const { missing } = await ShopRestockService.resolve(this.#draft.restockProfile, { shop: this.#draft });
        const keys = new Set(missing.map(entry => `${entry.providerId}::${entry.productId}`));
        this.#draft.restockProfile.entries = this.#draft.restockProfile.entries.filter(entry => !keys.has(`${entry.providerId}::${entry.productId}`));
        this.#advancedPoolEdited = true; this.#markProfileDirty(); this.render({ parts: ["content"] });
    }
    static _onImportPool() { this.element.querySelector("[data-restock-import]")?.click(); }
    static _onExportPool() { this.#exportPool(); }

    async close(options = {}) {
        if (this.#dirty && !options.force) {
            const discard = await DialogV2.confirm({ window: { title: "Discard Shop Changes?" }, content: "<p>Close the Shop Editor and discard all unsaved changes?</p>", yes: { label: "Discard", icon: "fas fa-trash" }, no: { label: "Keep Editing" }, rejectClose: false });
            if (!discard) return this;
        }
        return super.close(options);
    }

    async #initializeDraft() {
        this.#shop = this.#shopId ? ShopStore.getShop(this.#shopId) : null;
        const location = this.#locationId
            ? LocationEntityStore.getLocation(this.#locationId)
            : ShopStore.getParentLocation(this.#shop);
        this.#draft = ShopModel.normalize(this.#shop || {
            name: location ? `${location.display.name} Shop` : "Shop",
            naming: location ? {
                mode: "derived",
                templateId: "augur-nexus:location-shop:generic",
                pattern: "{parent} Shop",
                parentNameSnapshot: location.display.name
            } : undefined,
            hierarchy: { parentLocationId: this.#locationId || null }
        });
        this.#adoptDefaultNaming(location);
        this.#advancedPoolEdited = this.#draft.restockProfile.preset.customized === true;
    }

    async #loadCatalog(providers) {
        const profile = this.#draft.restockProfile;
        const packSets = await Promise.all(providers.map(async provider => {
            const packs = typeof provider.getAvailablePacks === "function" ? await provider.getAvailablePacks() : [];
            if (!Object.hasOwn(profile.discovery.providerScopes, provider.id)) profile.discovery.providerScopes[provider.id] = packs.map(pack => pack.id);
            return packs.map(pack => ({ ...pack, providerId: provider.id }));
        }));
        this.#packOptions = packSets.flat();
        const catalogs = await Promise.all(providers.map(provider => {
            const packIds = profile.discovery.providerScopes[provider.id] || [];
            return packIds.length ? ShopProductProviderRegistry.getCatalog(provider.id, { packIds, limit: 5000 }) : [];
        }));
        this.#catalog = catalogs.flat().sort((a, b) => a.name.localeCompare(b.name) || String(a.sourceLabel || "").localeCompare(String(b.sourceLabel || "")));
        this.#initializedCatalog = true;
        if (!this.#shopId && !profile.entries.length) {
            const preset = ShopPresetRegistry.list()[0];
            if (preset) await this.#applyPreset(preset, { generate: true, rename: true, markDirty: false, initializeGeneration: true });
        }
    }

    async #stockContext(adapter) {
        const products = await Promise.all(this.#draft.inventory.map(row => ShopProductProviderRegistry.resolve(row.providerId, row.productId, { shop: this.#draft, row })));
        const stock = this.#draft.inventory.map((row, index) => {
            const product = products[index];
            if (!product) return null;
            const origin = row.generated ? "generated" : row.providerId.includes("acquired") ? "acquired" : "curated";
            const originLabel = ({ generated: "Generated", acquired: "Acquired", curated: "Curated" })[origin];
            const price = row.priceOverride ?? Number(product.price?.amount || 0);
            return { ...row, name: product.name, imageSrc: product.imageSrc, detailLabel: [product.typeLabel || product.category, product.subtypeLabel].filter(Boolean).join(" · "), sourceLabel: product.sourceLabel || product.packLabel || product.packId || "Unknown source", priceLabel: adapter?.formatPrice(price) || String(price), origin, originLabel, searchText: `${product.name} ${product.typeLabel || ""} ${product.subtypeLabel || ""} ${product.sourceLabel || ""} ${originLabel}`.toLocaleLowerCase() };
        }).filter(Boolean);
        const count = origin => stock.filter(row => row.origin === origin).length;
        return { stock, stockCount: stock.length, stockQuery: this.#stockQuery, stockFilters: [{ id: "all", label: "All", count: stock.length }, { id: "generated", label: "Generated", count: count("generated") }, { id: "curated", label: "Curated", count: count("curated") }, { id: "acquired", label: "Acquired", count: count("acquired") }, { id: "pinned", label: "Pinned", count: stock.filter(row => row.pinned).length }].map(filter => ({ ...filter, selected: filter.id === this.#stockFilter })), canGenerate: this.#draft.restockProfile.entries.some(entry => entry.enabled !== false) };
    }

    async #poolContext(providers, adapter) {
        const profile = this.#draft.restockProfile;
        const schema = this.#mergeSchemas(ShopProductProviderRegistry.getCatalogSchemas());
        const visible = ShopCatalogFilterService.visibleGroupIds(schema, profile.discovery);
        const effective = Object.fromEntries(Object.entries(profile.discovery.filterState).filter(([id]) => visible.has(id)));
        this.#filteredCatalog = ShopCatalogFilterService.apply(this.#catalog, { ...profile.discovery, filterState: effective });
        const searched = this.#searchCatalog(this.#filteredCatalog);
        const existing = new Set(profile.entries.map(entry => `${entry.providerId}::${entry.productId}`));
        const availableItems = searched.slice(0, this.#visibleLimit).map(product => this.#productView(product, existing, adapter));
        const resolution = await ShopRestockService.resolve(profile, { shop: this.#draft });
        const resolved = new Map(resolution.resolved.map(({ entry, product }) => [`${entry.providerId}::${entry.productId}`, product]));
        const poolItems = profile.entries.map(entry => this.#poolItemView(entry, resolved, adapter));
        const facetCatalog = ShopCatalogFilterService.apply(this.#catalog, { ...profile.discovery, filterState: {} });
        const groupView = group => ({ ...group, active: group.options.some(option => option.state), open: !this.#collapsedGroups.has(group.id) });
        const groups = ShopCatalogFilterService.buildGroups(schema, facetCatalog, profile.discovery, { allProducts: this.#catalog });
        return { unavailable: !providers.length, query: this.#query, typeOptions: schema.types.map(type => ({ ...type, selected: profile.discovery.activeTypes.includes(type.id) })), filterGroups: groups.filter(group => group.placement !== "bottom").map(groupView), sourceGroups: groups.filter(group => group.placement === "bottom").map(groupView), hasFilters: groups.some(group => group.options.some(option => option.state)), priceMinimumGp: this.#copperToGp(profile.discovery.price.minimum), priceMaximumGp: this.#copperToGp(profile.discovery.price.maximum), availableItems, availableCount: searched.length, showingCount: availableItems.length, hasMore: searched.length > availableItems.length, selectedAvailableCount: this.#selectedAvailable.size, poolItems, poolCount: poolItems.length, missingCount: resolution.missing.length, hasMissing: resolution.missing.length > 0, hasTypeSelection: profile.discovery.activeTypes.length > 0 };
    }

    #settingsView() {
        const profile = this.#draft.restockProfile;
        return { name: this.#draft.name, imageSrc: ShopModel.getImage(this.#draft), imageSelection: ShopModel.getImageSelection(this.#draft), presetId: profile.preset.id, countMin: profile.generation.countMin, countMax: profile.generation.countMax, quantityMin: profile.generation.quantityMin, quantityMax: profile.generation.quantityMax, customerPurchasePercent: Math.round(this.#draft.configuration.customerPurchaseMultiplier * 100), customerSalePercent: Math.round(this.#draft.configuration.customerSaleMultiplier * 100), treasuryGp: Number((this.#draft.treasury.amount / 100).toFixed(2)), unlimitedTreasury: this.#draft.treasury.unlimited, buyingEnabled: this.#draft.configuration.buyingEnabled, sellingEnabled: this.#draft.configuration.sellingEnabled, finiteStock: this.#draft.configuration.finiteStock };
    }

    #captureFields() {
        if (!this.element || !this.#draft) return;
        const value = name => this.element.querySelector(`[name="${name}"]`)?.value;
        const checked = name => this.element.querySelector(`[name="${name}"]`)?.checked;
        if (value("shopName") !== undefined) {
            const name = String(value("shopName") || "Shop").trim() || "Shop";
            if (name !== this.#draft.name && this.#draft.naming?.mode === "derived") {
                this.#draft.naming.mode = "custom";
            }
            this.#draft.name = name;
        }
        const generation = this.#draft.restockProfile.generation;
        const countMin = Math.max(1, Math.floor(Number(value("countMin") ?? generation.countMin) || 1));
        const quantityMin = Math.max(1, Math.floor(Number(value("quantityMin") ?? generation.quantityMin) || 1));
        generation.countMin = countMin; generation.countMax = Math.max(countMin, Math.floor(Number(value("countMax") ?? generation.countMax) || countMin));
        generation.quantityMin = quantityMin; generation.quantityMax = Math.max(quantityMin, Math.floor(Number(value("quantityMax") ?? generation.quantityMax) || quantityMin));
        if (value("customerPurchasePercent") !== undefined) this.#draft.configuration.customerPurchaseMultiplier = Math.max(0, Number(value("customerPurchasePercent")) || 0) / 100;
        if (value("customerSalePercent") !== undefined) this.#draft.configuration.customerSaleMultiplier = Math.max(0, Number(value("customerSalePercent")) || 0) / 100;
        if (value("treasuryGp") !== undefined) this.#draft.treasury.amount = Math.max(0, Math.round((Number(value("treasuryGp")) || 0) * 100));
        if (checked("unlimitedTreasury") !== undefined) this.#draft.treasury.unlimited = checked("unlimitedTreasury");
        if (checked("buyingEnabled") !== undefined) this.#draft.configuration.buyingEnabled = checked("buyingEnabled");
        if (checked("sellingEnabled") !== undefined) this.#draft.configuration.sellingEnabled = checked("sellingEnabled");
        if (checked("finiteStock") !== undefined) this.#draft.configuration.finiteStock = checked("finiteStock");
    }

    async #applyShopType() {
        const preset = ShopPresetRegistry.get(this.element.querySelector('[name="presetId"]')?.value);
        if (!preset) return ui.notifications.warn("Choose a Shop Type first.");
        if (this.#draft.restockProfile.entries.length || this.#draft.inventory.length) {
            const confirmed = await DialogV2.confirm({
                window: { title: `Apply ${preset.name} Shop Type?` },
                content: `<p>This applies the <strong>${preset.name}</strong> filters, replaces the Restock Pool, and refreshes generated Current Stock.</p>${this.#advancedPoolEdited ? "<p><strong>Your item-level Restock Pool edits will be replaced.</strong></p>" : ""}<p>Curated and pinned merchandise is preserved.</p>`,
                yes: { label: "Apply Shop Type", icon: "fas fa-wand-magic-sparkles" },
                no: { label: "Cancel" }
            });
            if (!confirmed) return;
        }
        const previousPreset = ShopPresetRegistry.get(this.#draft.restockProfile.preset.id);
        const location = this.#locationId ? LocationEntityStore.getLocation(this.#locationId) : ShopStore.getParentLocation(this.#draft);
        const previousAutoName = location ? `${location.display.name} ${previousPreset?.name || "Shop"}` : previousPreset?.name || "Shop";
        const currentName = String(this.#draft.name || "").trim();
        const rename = this.#draft.naming?.mode === "derived"
            || currentName === previousAutoName
            || currentName === "Shop";
        this.#draft.name = currentName || this.#draft.name;
        await this.#applyPreset(preset, { generate: true, rename });
        this.#advancedPoolEdited = false;
        this.render({ parts: ["content"] });
    }

    async #openIconPicker() {
        this.#captureFields();
        const preset = ShopPresetRegistry.get(this.#draft.restockProfile.preset.id);
        const defaultIcon = await ShopIconCatalogRegistry.getIcon(preset?.defaultImageId);
        new ShopIconPicker({
            currentImageSrc: ShopModel.getImage(this.#draft),
            defaultIcon,
            onSelect: selection => {
                this.#draft.display.imageSrc = String(selection?.src || "");
                this.#draft.display.imageSelection = {
                    mode: ["preset", "catalog", "custom"].includes(selection?.mode) ? selection.mode : "custom",
                    id: String(selection?.id || "")
                };
                this.#markDirty();
                this.render({ parts: ["content"] });
            }
        }).render(true);
    }

    async #applyPreset(preset, { generate = false, rename = false, markDirty = true, initializeGeneration = false } = {}) {
        const location = this.#locationId
            ? LocationEntityStore.getLocation(this.#locationId)
            : ShopStore.getParentLocation(this.#draft);
        const name = rename ? (location ? `${location.display.name} ${preset.name}` : preset.name) : "";
        this.#draft = await ShopPresetDraftService.applyPreset(this.#draft, preset, {
            catalog: this.#catalog,
            packOptions: this.#packOptions,
            initializeGeneration,
            name
        });
        if (location) {
            this.#draft.naming = {
                mode: rename ? "derived" : "custom",
                templateId: `augur-nexus:location-shop:${preset.id}`,
                pattern: `{parent} ${preset.name}`,
                parentNameSnapshot: location.display.name
            };
        }
        if (generate) await this.#generate(false, { notify: false, captureFields: false });
        if (markDirty) this.#markDirty();
    }

    async #generate(replaceAll, { notify = true, captureFields = true } = {}) {
        if (captureFields) this.#captureFields();
        const result = await ShopRestockService.generate(this.#draft.restockProfile, { shop: this.#draft, seed: foundry.utils.randomID(), warn: true });
        if (!result.inventory.length) return ui.notifications.warn("The restock pool has no available enabled Items to generate.");
        const retained = replaceAll ? [] : this.#draft.inventory.filter(row => row.pinned || !row.generated);
        this.#draft.inventory = [...retained, ...result.inventory];
        if (replaceAll) this.#draft.providerData = {};
        this.#markDirty();
        if (notify) ui.notifications.info(`${result.inventory.length} products generated for ${this.#draft.name}.`);
        if (this.rendered) this.render({ parts: ["content"] });
    }

    async #save() {
        this.#captureFields();
        this.#draft.restockProfile = ShopRestockProfileModel.normalize(this.#draft.restockProfile);
        this.#draft.revision += 1;
        const saved = this.#shopId
            ? await ShopStore.updateShop(this.#shopId, this.#draft, {
                nameIntent: this.#draft.naming?.mode === "derived" ? "derived" : "manual"
            })
            : await ShopStore.createShop(this.#draft);
        this.#shopId = saved.id; this.#shop = saved; this.#draft = ShopModel.normalize(saved); this.#dirty = false;
        await this.#onSaved?.(saved);
        ui.notifications.info(`${saved.name} saved.`);
        await super.close({ force: true });
    }

    #markDirty() { this.#dirty = true; }

    #adoptDefaultNaming(location) {
        if (!location || this.#draft.naming?.mode !== "unmanaged") return;
        const preset = ShopPresetRegistry.get(this.#draft.restockProfile.preset.id);
        const suffix = preset?.name || "Shop";
        const generatedName = `${location.display.name} ${suffix}`;
        this.#draft.naming = {
            mode: this.#draft.name === generatedName ? "derived" : "custom",
            templateId: `augur-nexus:location-shop:${preset?.id || "generic"}`,
            pattern: `{parent} ${suffix}`,
            parentNameSnapshot: location.display.name
        };
    }

    #markProfileDirty() { this.#draft.restockProfile.preset.customized = true; this.#advancedPoolEdited = true; this.#draft.restockProfile.revision += 1; this.#markDirty(); }
    #resetCatalogView(clearSelection = true) { if (clearSelection) this.#selectedAvailable.clear(); this.#visibleLimit = 100; }
    #cycleFilter(target, direction) { const groupId = String(target.dataset.filterGroup || ""); const value = String(target.dataset.filterValue || ""); if (!groupId || !value) return; const group = this.#draft.restockProfile.discovery.filterState[groupId] ||= {}; const next = ShopCatalogFilterService.cycle(group[value], direction); if (next) group[value] = next; else delete group[value]; if (!Object.keys(group).length) delete this.#draft.restockProfile.discovery.filterState[groupId]; this.#advancedPoolEdited = true; this.#resetCatalogView(); this.#markProfileDirty(); this.render({ parts: ["content"] }); }
    #capturePrice() { const toCopper = value => value === "" ? null : Math.max(0, Math.round((Number(value) || 0) * 100)); this.#draft.restockProfile.discovery.price = { minimum: toCopper(this.element.querySelector('[name="priceMinimumGp"]')?.value ?? ""), maximum: toCopper(this.element.querySelector('[name="priceMaximumGp"]')?.value ?? ""), requireListed: false }; }
    #addPoolKeys(keys) { const products = new Map(this.#catalog.map(product => [`${product.providerId}::${product.productId}`, product])); const existing = new Set(this.#draft.restockProfile.entries.map(entry => `${entry.providerId}::${entry.productId}`)); for (const key of keys.filter(Boolean)) { const product = products.get(key); if (!product || existing.has(key)) continue; this.#draft.restockProfile.entries.push({ providerId: product.providerId, productId: product.productId, enabled: true, weight: 1, quantity: { min: 0, max: 0 }, priceOverride: null, labelHint: product.name, sourceHint: product.packId || product.sourceLabel || "" }); existing.add(key); } this.#selectedAvailable.clear(); this.#advancedPoolEdited = true; this.#markProfileDirty(); this.render({ parts: ["content"] }); }
    #removePoolKeys(keys) { const removed = new Set(keys); this.#draft.restockProfile.entries = this.#draft.restockProfile.entries.filter(entry => !removed.has(`${entry.providerId}::${entry.productId}`)); this.#advancedPoolEdited = true; this.#markProfileDirty(); this.render({ parts: ["content"] }); }
    #updatePoolEntry(input) { const entry = this.#draft.restockProfile.entries.find(row => `${row.providerId}::${row.productId}` === input.closest("[data-pool-key]")?.dataset.poolKey); if (!entry) return; if (input.name === "poolEnabled") entry.enabled = input.checked; else if (input.name === "poolWeight") entry.weight = Math.max(1, Math.floor(Number(input.value) || 1)); else if (input.name === "poolQuantityMin") entry.quantity.min = Math.max(0, Math.floor(Number(input.value) || 0)); else if (input.name === "poolQuantityMax") entry.quantity.max = Math.max(entry.quantity.min, Math.floor(Number(input.value) || 0)); else if (input.name === "poolPriceGp") entry.priceOverride = input.value === "" ? null : Math.max(0, Math.round((Number(input.value) || 0) * 100)); this.#advancedPoolEdited = true; this.#markProfileDirty(); }
    #removeStockRow(id) { const removed = this.#draft.inventory.find(row => row.id === id); this.#draft.inventory = this.#draft.inventory.filter(row => row.id !== id); if (removed && !this.#draft.inventory.some(row => row.providerId === removed.providerId && row.productId === removed.productId)) { delete this.#draft.providerData?.[removed.providerId]?.[removed.productId]; if (this.#draft.providerData?.[removed.providerId] && !Object.keys(this.#draft.providerData[removed.providerId]).length) delete this.#draft.providerData[removed.providerId]; } this.#markDirty(); }
    #applyStockFilters() { if (!this.element || this.#tab !== "settings") return; const query = this.#stockQuery.trim().toLocaleLowerCase(); let visible = 0; for (const card of this.element.querySelectorAll("[data-stock-row]")) { const matchQuery = !query || String(card.dataset.searchText || "").includes(query); const matchType = this.#stockFilter === "all" || (this.#stockFilter === "pinned" ? card.dataset.pinned === "true" : card.dataset.origin === this.#stockFilter); card.hidden = !(matchQuery && matchType); if (!card.hidden) visible++; } const count = this.element.querySelector("[data-visible-stock-count]"); if (count) count.textContent = `${visible} shown`; }
    #bindSearch(name, callback, delayed = false) { const input = this.element.querySelector(`[name="${name}"]`); input?.addEventListener("input", event => { callback(String(event.currentTarget.value || "")); if (!delayed) return; clearTimeout(this.#searchTimer); this.#searchTimer = setTimeout(() => { this.#restoreSearchFocus = true; this.render({ parts: ["content"] }); }, 250); }); if (delayed && this.#restoreSearchFocus) { input?.focus(); input?.setSelectionRange(input.value.length, input.value.length); this.#restoreSearchFocus = false; } }
    #searchCatalog(products) { const needle = this.#query.trim().toLocaleLowerCase(); return needle ? products.filter(product => `${product.name} ${product.typeLabel || ""} ${product.subtypeLabel || ""} ${product.sourceLabel || ""}`.toLocaleLowerCase().includes(needle)) : products; }
    #productView(product, existing, adapter) { const key = `${product.providerId}::${product.productId}`; return { ...product, key, alreadyAdded: existing.has(key), selected: this.#selectedAvailable.has(key), priceLabel: adapter?.formatPrice(Number(product.price?.amount) || 0) || String(product.price?.amount || 0), detailLabel: [product.typeLabel || product.category, product.subtypeLabel].filter(Boolean).join(" · "), sourceLabel: product.sourceLabel || product.packLabel || product.packId || "Unknown source" }; }
    #poolItemView(entry, resolved, adapter) { const key = `${entry.providerId}::${entry.productId}`; const product = resolved.get(key); const price = entry.priceOverride ?? Number(product?.price?.amount || 0); return { ...entry, key, name: product?.name || entry.labelHint || "Unavailable Item", imageSrc: product?.imageSrc || "icons/svg/hazard.svg", detailLabel: [product?.typeLabel || product?.category, product?.subtypeLabel].filter(Boolean).join(" · "), priceLabel: product ? adapter?.formatPrice(price) || String(price) : "Missing", priceGp: entry.priceOverride === null ? "" : this.#copperToGp(entry.priceOverride), sourceLabel: product?.sourceLabel || product?.packLabel || entry.sourceHint || entry.productId, missing: !product };
    }
    #mergeSchemas(schemas) { const types = new Map(); const groups = new Map(); for (const schema of schemas) { for (const type of schema.types || []) if (!types.has(type.id)) types.set(type.id, type); for (const group of schema.filterGroups || []) { if (!groups.has(group.id)) groups.set(group.id, foundry.utils.deepClone(group)); else if (group.dynamic) groups.get(group.id).dynamic = true; } } if (!groups.has("merchandiseKind")) groups.set("merchandiseKind", { id: "merchandiseKind", label: "Merchandise", dynamic: true, optionLabels: { armor: "Armor", shield: "Shields", weapon: "Weapons", ammunition: "Ammunition" } }); if (!groups.has("availability")) groups.set("availability", { id: "availability", label: "Availability", dynamic: true, optionLabels: { standard: "Standard", special: "Magical / Uncommon" } }); return { types: [...types.values()], filterGroups: [...groups.values()] }; }
    #copperToGp(value) { return value === null || value === undefined ? "" : Number((Number(value) / 100).toFixed(2)); }

    #exportPool() {
        const data = { kind: "augur-nexus-shop-restock-profile", schemaVersion: 1, systemId: game.system?.id || "", restockProfile: ShopRestockProfileModel.normalize(this.#draft.restockProfile) };
        const filename = `${this.#draft.name.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "shop"}-restock.json`;
        foundry.utils.saveDataToFile(JSON.stringify(data, null, 2), "application/json", filename);
    }
    async #readImportFile(file) { if (!file) return; try { const data = JSON.parse(await file.text()); if (data?.kind !== "augur-nexus-shop-restock-profile" || data.schemaVersion !== 1 || !Array.isArray(data.restockProfile?.entries)) throw new Error("This is not a supported Shop Restock Profile file."); if (data.restockProfile.entries.length > 5000) throw new Error("Restock pools may contain at most 5,000 references."); if (data.systemId && data.systemId !== game.system?.id) ui.notifications.warn(`This profile was created for ${data.systemId}; unavailable references remain visible and are skipped.`); this.#draft.restockProfile = ShopRestockProfileModel.normalize(data.restockProfile); this.#advancedPoolEdited = true; this.#markProfileDirty(); this.#selectedAvailable.clear(); this.#visibleLimit = 100; ui.notifications.info("Restock profile imported. Review it, then save the Shop."); this.render({ parts: ["content"] }); } catch (error) { ui.notifications.error(error?.message || "The Restock Profile file could not be imported."); } }

    #activateStockDrop() { const zone = this.element.querySelector("[data-stock-drop]"); if (!zone || !game.user?.isGM) return; zone.addEventListener("dragover", event => { event.preventDefault(); zone.classList.add("is-dragover"); }); zone.addEventListener("dragleave", event => { if (!zone.contains(event.relatedTarget)) zone.classList.remove("is-dragover"); }); zone.addEventListener("drop", event => this.#onStockDrop(event)); }
    async #onStockDrop(event) { event.preventDefault(); event.currentTarget.classList.remove("is-dragover"); const adapter = ShopSystemAdapterRegistry.getActive(); if (typeof adapter?.createStockProduct !== "function") return ui.notifications.warn(`${game.system.title} does not support dropping Items into Shops yet.`); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const item = data.uuid ? await fromUuid(data.uuid).catch(() => null) : data.type === "Item" ? game.items?.get(data.id) : null; if (item?.documentName !== "Item") return ui.notifications.warn("Drop a physical Item from a compendium, Item directory, or Actor inventory."); try { const stocked = await adapter.createStockProduct(item, { shop: this.#draft }); const inventory = this.#draft.inventory; let existing = inventory.find(row => row.providerId === stocked.providerId && row.productId === stocked.productId); if (stocked.productData) { this.#draft.providerData[stocked.providerId] ||= {}; this.#draft.providerData[stocked.providerId][stocked.productId] = foundry.utils.deepClone(stocked.productData); } if (existing) { existing.quantity++; existing.pinned = true; existing.generated = false; } else inventory.push({ id: foundry.utils.randomID(), providerId: stocked.providerId, productId: stocked.productId, quantity: 1, priceOverride: null, pinned: true, generated: false }); this.#markDirty(); ui.notifications.info(`${item.name} added to current stock. The original Item was not changed.`); this.render({ parts: ["content"] }); } catch (error) { ui.notifications.warn(error?.message || "That Item cannot be stocked in this Shop."); } }
}
