import type { Printful, Webflow } from "./util/types";
import { formatSlug, type DeepPartial } from "./util/misc";
import { WebflowClient } from "./webflow";
import { PrintfulClient } from "./printful";
import pino from "pino";

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

type ProductEntry = {
    colorGroup: string | null;
    product: Printful.Products.Product;
};

type PMetaProduct = {
    name: string;
    wProductId: string;
    entries: ProductEntry[];
};

export type ProductSyncerOptions = {
    filter?: {
        pProductIds: number[] | null;
    },
    onStart?: (startDate: Date) => Promise<void>
    onCompletion?: (completionDate: Date, duration: number) => Promise<void>
    onStep?: (pProductIds: number[]) => Promise<void>
};

export class ProductSyncer {
    private log: pino.Logger;

    constructor(
        private Printful: PrintfulClient,
        private Webflow: WebflowClient,
        log?: pino.Logger,
    ) {
        this.log = (log ?? pino({ level: "silent" })).child({ component: "ProductSyncer" });
    }

    async run(opt: ProductSyncerOptions = {}) {
        const startDate = new Date();
        await opt.onStart?.(startDate);

        if (opt.filter?.pProductIds)
            this.log.info({ pProductIds: opt.filter.pProductIds }, "sync started (filtered)");
        else
            this.log.info("sync started (all)");

        this.log.debug("retrieving webflow products");
        const allWProducts = await this.Webflow.Products.list({ forceAll: true });

        this.log.debug("retrieving printful products");
        const allPProducts = await this.Printful.Products.list({ forceAll: true });

        this.log.info({ pProductCount: allPProducts.length, filter: opt.filter ?? "N/A" }, "generating printful meta products");
        const pMetaProducts = await this.generatePMetaProducts(allWProducts, allPProducts, opt);

        for (const pMetaProduct of pMetaProducts) {
            const pProductIds = pMetaProduct.entries.map((e) => e.product.sync_product.id);
            this.log.info({ name: pMetaProduct.name, externalId: pMetaProduct.wProductId, pProductIds }, "syncing printful meta product");
            await opt.onStep?.(pProductIds);

            const newWSkus: DeepPartial<Webflow.Products.Skus.Sku>[] = this.generateWSkus(pMetaProduct);
            const foundColors = new Set<string>(
                newWSkus.map((sku) => sku.fieldData?.["sku-values"]?.["color"]).filter((v) => v !== undefined)
            );
            const foundSizes = new Set<string>(
                newWSkus.map((sku) => sku.fieldData?.["sku-values"]?.["size"]).filter((v) => v !== undefined)
            );

            if (!newWSkus[0]) throw new Error("no webflow SKUs generated — all variants were filtered out");

            const wProductFieldData = {
                name: pMetaProduct.name,
                slug: formatSlug(pMetaProduct.name),
                shippable: true,
                "tax-category": "standard-taxable",
                "sku-properties": [
                    {
                        id: "color",
                        name: "Color",
                        enum: Array.from(foundColors).map((color) => ({
                            id: color,
                            slug: formatSlug(color),
                            name: color,
                        })),
                    },
                    {
                        id: "size",
                        name: "Size",
                        enum: Array.from(foundSizes).map((size) => ({
                            id: size,
                            slug: formatSlug(size),
                            name: size,
                        })),
                    },
                ],
            };

            const wProduct = allWProducts.find((wp) => wp.product.id === pMetaProduct.wProductId);

            if (wProduct) {
                this.log.info({ id: wProduct.product.id }, "existing webflow product found, updating it");

                // do not update images
                for (const sku of newWSkus) delete sku.fieldData?.["main-image"];

                // handle case where webflow SKU created by associated printful variant external_id update failed
                if (!newWSkus[0].id) {
                    this.log.warn({ wSku: newWSkus[0] }, "missing webflow SKU id");
                    const wSkuMatch = this.resolveWSku(
                        wProduct.skus,
                        newWSkus[0].fieldData?.["sku-values"]?.["color"],
                        newWSkus[0].fieldData?.["sku-values"]?.["size"],
                    );

                    if (wSkuMatch)
                        newWSkus[0].id = wSkuMatch.id;
                    else
                        this.log.warn({ wSku: newWSkus[0] }, "could not find existing webflow SKU that matches");
                }

                await this.Webflow.Products.update(wProduct.product.id, {
                    product: { fieldData: wProductFieldData },
                    sku: newWSkus[0],
                });

                for (const newWSku of newWSkus) {
                    const existingWSku = this.resolveWSku(
                        wProduct.skus,
                        newWSku.fieldData?.["sku-values"]?.["color"],
                        newWSku.fieldData?.["sku-values"]?.["size"],
                        newWSku.id,
                    );
                    if (existingWSku) {
                        await this.Webflow.Products.Skus.update(
                            pMetaProduct.wProductId,
                            existingWSku.id,
                            newWSku,
                        );
                    } else {
                        await this.Webflow.Products.Skus.create(
                            pMetaProduct.wProductId,
                            [newWSku],
                        );
                    }
                }
            } else {
                this.log.info("existing webflow product not found, creating it");

                const wProductId = await this.Webflow.Products.create({
                    product: { fieldData: wProductFieldData },
                    sku: newWSkus[0],
                });
                pMetaProduct.wProductId = wProductId;

                if (newWSkus.length > 1)
                    await this.Webflow.Products.Skus.create(wProductId, newWSkus.slice(1));
            }

            // Re-fetch after SKU mutations so newly created SKUs have their real IDs.
            const freshWProduct = await this.Webflow.Products.get(pMetaProduct.wProductId);
            if (!freshWProduct) throw new Error("webflow product missing");

            // Update printful external IDs
            for (const { colorGroup, product: pProduct } of pMetaProduct.entries) {
                const expectedExternalId = colorGroup ?
                    `${pMetaProduct.wProductId}-${colorGroup}` :
                    pMetaProduct.wProductId;
                if (pProduct.sync_product.external_id === expectedExternalId) continue;

                const pVariantPatches: DeepPartial<Printful.Products.SyncVariant>[] = [];
                for (const pVariant of pProduct.sync_variants) {
                    const wSku = this.resolveWSku(
                        freshWProduct.skus,
                        colorGroup ?? pVariant.color,
                        pVariant.size,
                    );
                    if (wSku) {
                        pVariantPatches.push({
                            id: pVariant.id,
                            external_id: String(wSku.id),
                        });
                    }
                }

                await sleep(10000);
                this.log.info({ pProduct: pProduct.sync_product.id, externalId: expectedExternalId }, "updating printful product's external ID");
                await this.Printful.Products.update(pProduct.sync_product.id, {
                    sync_product: {
                        id: pProduct.sync_product.id,
                        external_id: expectedExternalId,
                    },
                    sync_variants: pVariantPatches,
                });
            }
        }

        const endDate = new Date();
        const duration = endDate.getTime() - startDate.getTime();
        await opt.onCompletion?.(endDate, duration);
        this.log.info({ durationMs: duration }, "sync complete");
    }

    parseColorGroup(name: string): string | null {
        const m = name.match(/\[([^\]]+)\]/);
        return m?.[1] ?? null;
    }

    private generateWSkus(pMetaProduct: PMetaProduct): DeepPartial<Webflow.Products.Skus.Sku>[] {
        const wSkus: DeepPartial<Webflow.Products.Skus.Sku>[] = [];
        for (const { colorGroup, product: pProduct } of pMetaProduct.entries) {
            const isColorGrouped = colorGroup !== null;
            for (const pVariant of pProduct.sync_variants) {
                if (isColorGrouped) {
                    // Only include sizes present across every color variant.
                    const sizeInAll = pMetaProduct.entries.every((e) =>
                        e.product.sync_variants.some((sv) => sv.size === pVariant.size)
                    );
                    if (!sizeInAll) continue;
                }

                const color = isColorGrouped ?
                    colorGroup :
                    pVariant.color;

                this.log.debug(
                    { color, size: pVariant.size, retailPrice: pVariant.retail_price },
                    "generating webflow SKU from printful variant"
                );

                wSkus.push({
                    id: pVariant.external_id,
                    fieldData: {
                        name: pVariant.name,
                        slug: formatSlug(pVariant.name),
                        "sku-values": {
                            color,
                            size: pVariant.size,
                        },
                        price: {
                            value: Math.round(+pVariant.retail_price * 100),
                            unit: pVariant.currency,
                            currency: pVariant.currency,
                        },
                        "main-image": PrintfulClient.Util.getVariantMainImage(pVariant),
                    },
                });
            }
        }

        return wSkus;
    }

    async generatePMetaProducts(
        allWProducts: Webflow.Products.ProductAndSkus[],
        allPProducts: Printful.Products.SyncProduct[],
        opt: ProductSyncerOptions,
    ) {
        const metaNameFilter = opt.filter?.pProductIds
            ?.map((id) => allPProducts.find((p) => p.id === id))
            ?.filter((p) => p !== undefined)
            ?.map((p) => this.getPMetaProductName(p));

        const pMetaProducts: PMetaProduct[] = [];
        for (const pProduct of allPProducts) {
            const metaName = this.getPMetaProductName(pProduct);
            const colorGroup = this.parseColorGroup(pProduct.name);
            const wProductId = this.isPProductSynced(pProduct, allWProducts) ?
                pProduct.external_id.split("-")[0] ?? "" :
                "";

            if (metaNameFilter && !metaNameFilter.includes(metaName)) continue;

            let pMetaProduct = pMetaProducts.find((mp) => mp.name === metaName);
            if (!pMetaProduct) {
                this.log.debug({ metaName, colorGroup, wProductId }, "generating printful meta product");
                pMetaProduct = {
                    name: metaName,
                    wProductId,
                    entries: [],
                };
                pMetaProducts.push(pMetaProduct);
            } else if (!pMetaProduct.wProductId && wProductId) {
                // Prefer a non-empty wProductId so a newly added color doesn't shadow an already-synced color's ID.
                pMetaProduct.wProductId = wProductId;
            }

            const fullPProduct = await this.Printful.Products.get(pProduct.id);
            if (!fullPProduct) throw new Error(`Printful product ${pProduct.id} not found`);

            const isDuplicate = allPProducts.some((p) => p.id !== pProduct.id && p.name === pProduct.name);
            if (isDuplicate) this.log.warn({ pProductName: pProduct.name }, "found duplicate printful product");

            this.log.debug({ name: metaName, colorGroup }, "adding entry to printful meta product");
            pMetaProduct.entries.push({ colorGroup, product: fullPProduct });
        }

        return pMetaProducts;
    }

    getPMetaProductName(pProduct: Printful.Products.SyncProduct): string {
        const color = this.parseColorGroup(pProduct.name);
        if (color) return pProduct.name.replace(`[${color}]`, "").trimEnd();
        return pProduct.name;
    }

    private resolveWSku(
        existingSkus: Webflow.Products.Skus.Sku[],
        color: string | undefined,
        size: string | undefined,
        id?: string,
    ): Webflow.Products.Skus.Sku | undefined {
        return existingSkus.find(
            (sku) =>
                (id && sku.id === id) ||
                (sku.fieldData["sku-values"]?.["color"] === color &&
                    sku.fieldData["sku-values"]?.["size"] === size),
        );
    }

    private isPProductSynced(pProduct: Printful.Products.SyncProduct, allWProducts: Webflow.Products.ProductAndSkus[]) {
        return allWProducts.some((wp) => wp.product.id === pProduct.external_id.split("-")[0]);
    }
}
