export class ShopCatalogFilterService {
    static normalizeState(state = {}) {
        const filterState = {};
        for (const [groupId, values] of Object.entries(state.filterState || {})) {
            const normalized = {};
            for (const [value, mode] of Object.entries(values || {})) {
                const numeric = Number(mode);
                if (numeric === 1 || numeric === -1) normalized[String(value)] = numeric;
            }
            if (Object.keys(normalized).length) filterState[String(groupId)] = normalized;
        }
        return {
            schemaId: String(state.schemaId || "nexus-catalog-v1"),
            activeTypes: [...new Set((Array.isArray(state.activeTypes) ? state.activeTypes : []).map(String).filter(Boolean))],
            filterState,
            providerScopes: foundry.utils.deepClone(state.providerScopes || {}),
            price: {
                minimum: this.#numberOrNull(state.price?.minimum),
                maximum: this.#numberOrNull(state.price?.maximum),
                requireListed: false
            }
        };
    }

    static apply(products, state = {}) {
        const normalized = this.normalizeState(state);
        if (!normalized.activeTypes.length) return [];
        const activeTypes = new Set(normalized.activeTypes);
        return (Array.isArray(products) ? products : []).filter(product => {
            const facets = product.facets || {};
            const type = String(facets.type || product.category || "");
            if (!activeTypes.has(type)) return false;
            const price = Math.max(0, Number(product.price?.amount) || 0);
            if (normalized.price.requireListed && price <= 0) return false;
            if (normalized.price.minimum !== null && price < normalized.price.minimum) return false;
            if (normalized.price.maximum !== null && price > normalized.price.maximum) return false;
            return Object.entries(normalized.filterState).every(([groupId, modes]) =>
                this.#matchesGroup(this.#facetValues(facets[groupId]), modes)
            );
        });
    }

    static buildGroups(schema = {}, products = [], state = {}, { allProducts = products } = {}) {
        const normalized = this.normalizeState(state);
        const active = new Set(normalized.activeTypes);
        const definitions = Array.isArray(schema.filterGroups) ? schema.filterGroups : [];
        return definitions.filter(group => this.#groupIsVisible(group, active)).map(group => {
            const configured = Array.isArray(group.options) ? group.options : [];
            const discovered = group.dynamic
                ? this.#discoverOptions(group.placement === "bottom" ? allProducts : products, group.id, group.optionLabels || {})
                : [];
            const optionsById = new Map([...configured, ...discovered].map(option => [String(option.id), option]));
            const current = normalized.filterState[group.id] || {};
            return {
                ...group,
                options: Array.from(optionsById.values()).map(option => ({
                    ...option,
                    state: Number(current[option.id]) || 0,
                    include: Number(current[option.id]) === 1,
                    exclude: Number(current[option.id]) === -1
                }))
            };
        }).filter(group => group.options.length);
    }

    static visibleGroupIds(schema = {}, state = {}) {
        const active = new Set(this.normalizeState(state).activeTypes);
        return new Set((Array.isArray(schema.filterGroups) ? schema.filterGroups : [])
            .filter(group => this.#groupIsVisible(group, active))
            .map(group => String(group.id)));
    }

    static cycle(current, direction = 1) {
        const state = Number(current) || 0;
        if (direction < 0) return state === -1 ? 0 : -1;
        return state === 1 ? 0 : 1;
    }

    static #matchesGroup(values, modes) {
        const include = Object.entries(modes).filter(([, mode]) => Number(mode) === 1).map(([value]) => value);
        const exclude = new Set(Object.entries(modes).filter(([, mode]) => Number(mode) === -1).map(([value]) => value));
        if (values.some(value => exclude.has(value))) return false;
        return !include.length || values.some(value => include.includes(value));
    }

    static #facetValues(value) {
        if (Array.isArray(value)) return value.map(String).filter(Boolean);
        if (value === null || value === undefined || value === "") return [""];
        return [String(value)];
    }

    static #groupIsVisible(group, active) {
        const types = Array.isArray(group.types) ? group.types : [];
        if (!types.length) return true;
        if (group.visibility === "all") return active.size > 0 && [...active].every(type => types.includes(type));
        if (group.visibility === "single") return active.size === 1 && types.includes([...active][0]);
        return [...active].some(type => types.includes(type));
    }

    static #discoverOptions(products, groupId, labels) {
        const values = new Set();
        for (const product of products) {
            for (const value of this.#facetValues(product.facets?.[groupId])) if (value) values.add(value);
        }
        return [...values].sort((a, b) => String(labels[a] || a).localeCompare(String(labels[b] || b))).map(id => ({
            id,
            label: String(labels[id] || id)
        }));
    }

    static #numberOrNull(value) {
        if (value === "" || value === null || value === undefined) return null;
        const number = Number(value);
        return Number.isFinite(number) ? Math.max(0, Math.round(number)) : null;
    }
}
