import { Printful, PrintfulClient, ProductSyncer, Webflow, WebflowClient } from "@blade-and-brawn/commerce";
import { env, log } from "../util";
import { db } from "../database/db";
import { defineEventType, EventQueue } from "./event-queue";
import { Type as t } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
import { sql } from "kysely";
import zipcodesUs from "zipcodes-us";
import { secToMs } from "@blade-and-brawn/domain";
import { EventsService } from "./events";

export const WOrderStatusSchema = t.Union([
    t.Literal("pending"),
    t.Literal("unfulfilled"),
    t.Literal("fulfilled"),
    t.Literal("disputed"),
    t.Literal("dispute-lost"),
    t.Literal("refunded"),
]);

export class CommerceService {
    readonly Printful = new PrintfulClient({
        token: env.PRINTFUL_AUTH,
        storeId: env.PRINTFUL_STORE_ID,
    });
    readonly Webflow = new WebflowClient({
        siteId: env.WEBFLOW_SITE_ID,
        collectionIds: {
            products: env.WEBFLOW_COLLECTION_PRODUCTS_ID,
            skus: env.WEBFLOW_COLLECTION_SKUS_ID
        },
        token: env.WEBFLOW_AUTH,
        webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
    });
    readonly Apparel = new ApparelService(this.Printful, this.Webflow);
}

class ApparelService {
    Syncs: ApparelSyncService;
    Orders: ApparelOrdersService;
    constructor(private readonly Printful: PrintfulClient, private readonly Webflow: WebflowClient) {
        this.Syncs = new ApparelSyncService(this.Printful, this.Webflow);
        this.Orders = new ApparelOrdersService(this.Printful, this.Webflow);
    }
}

class ApparelOrdersService {
    readonly Queue;

    constructor(private readonly Printful: PrintfulClient, private readonly Webflow: WebflowClient) {
        this.Queue = new EventQueue({
            group: "apparel_order",
            cfg: {
                error: { limit: 3, initialRetryDelayMs: secToMs(1) },
                concurrency: { limit: 1 }
            },
            types: {
                "apparel_order_create": defineEventType({
                    schemas: { payload: t.Object({ wOrder: t.Object({}) }), state: t.Null() },
                    dedupeKey: (p) => (p.wOrder as Webflow.Orders.Order).orderId,
                    processor: async (payload) => {
                        const wOrder = payload.wOrder as Webflow.Orders.Order;

                        const stateCode = zipcodesUs.find(
                            wOrder.shippingAddress.postalCode.split("-")[0] ?? "",
                        ).stateCode;
                        if (!stateCode) throw new Error("Unrecognized state code");

                        // creates a DRAFT order
                        await this.Printful.Orders.create({
                            external_id: wOrder.orderId,
                            // TODO: derive from webflow
                            shipping: "STANDARD",
                            recipient: {
                                name: wOrder.shippingAddress.addressee,
                                address1: wOrder.shippingAddress.line1,
                                address2: wOrder.shippingAddress.line2,
                                city: wOrder.shippingAddress.city,
                                state_code: stateCode,
                                country_code: wOrder.shippingAddress.country,
                                zip: wOrder.shippingAddress.postalCode,
                            },
                            items: wOrder.purchasedItems.map((wOrderSku) => ({
                                external_variant_id: wOrderSku.variantId,
                                quantity: wOrderSku.count,
                            })),
                        });

                        // TODO: confirm the above created order
                        // right now we manually review and confirm it in the printful portal
                    }
                }),
                "apparel_order_fulfill": defineEventType({
                    schemas: { payload: t.Object({ wOrderId: t.String(), shipment: t.Object({}) }), state: t.Null() },
                    dedupeKey: (p) => `${p.wOrderId}:${(p.shipment as Printful.Webhook.PackageShipped["data"]["shipment"]).tracking_number}`,
                    processor: async (payload) => {
                        const wOrderId = payload.wOrderId;
                        const shipment = payload.shipment as Printful.Webhook.PackageShipped["data"]["shipment"];
                        log.info({ wOrderId, carrier: shipment.carrier, tracking: shipment.tracking_number }, "printful webhook: package shipped");

                        await this.Webflow.Orders.update(wOrderId, {
                            shippingTrackingURL: shipment.tracking_url,
                            shippingTracking: shipment.tracking_number,
                            shippingProvider: shipment.carrier,
                        });
                        await this.Webflow.Orders.fulfill(wOrderId, {
                            sendOrderFulfilledEmail: true,
                        });
                    }
                })
            }
        });
    }
}

class ApparelSyncService {
    readonly Queue;
    private readonly ProductSyncer: ProductSyncer;

    // TODO: make each service have their own logger, so we can enforce .child({component: <ServiceName>})

    constructor(private readonly Printful: PrintfulClient, private readonly Webflow: WebflowClient) {
        this.ProductSyncer = new ProductSyncer(this.Printful, this.Webflow, log);
        this.Queue = new EventQueue({
            group: "apparel_sync",
            cfg: {
                error: { limit: 3, initialRetryDelayMs: secToMs(1) },
                concurrency: { limit: 1 }
            },
            types: {
                "apparel_sync_update": defineEventType({
                    schemas: {
                        payload: t.Object({
                            session: t.Object({
                                name: t.String(),
                                id: t.String()
                            }),
                            filter: t.Optional(
                                t.Object({
                                    pProductIds: t.Optional(t.Array(t.Number()))
                                })
                            )
                        }),
                        state: t.Union([
                            t.Object({
                                pProductIds: t.Optional(t.Array(t.Number()))
                            }),
                            t.Null()
                        ])
                    },
                    dedupeKey: (p) => (p.filter?.pProductIds ?? []).join(","),
                    processor: async (payload, setState) => {
                        await this.ProductSyncer.syncApparel({
                            filter: payload.filter,
                            beforeStep: async (pProductIds: number[]) => {
                                await setState({ pProductIds });
                            }
                        });
                    }
                }),
                "apparel_sync_delete": defineEventType({
                    schemas: {
                        payload: t.Object({ wProductId: t.String() }),
                        state: t.Union([t.Object({}), t.Null()])
                    },
                    dedupeKey: (p) => p.wProductId,
                    processor: async (payload) => {
                        await this.Webflow.Products.remove(payload.wProductId);
                    }
                })
            }
        });
    }

    async getLatestSyncState(sessionId: string) {
        const latestSync = await db.selectFrom("events")
            .select(["started_at", "available_at", "ended_at", "has_failed", "state"])
            .where("group", "=", this.Queue.group)
            .where("type", "=", "apparel_sync_update")
            .where(sql<string>`payload->'session'->>'id'`, "=", sessionId)
            .orderBy(sql`(started_at is not null and ended_at is null)`, "desc")
            .orderBy("created_at", "desc")
            .limit(1)
            .executeTakeFirst();
        if (!latestSync) return;

        Value.Assert(this.Queue.types["apparel_sync_update"].schemas.state, latestSync.state);

        return {
            startDate: latestSync.started_at,
            status: EventsService.status(latestSync),
            syncingPProductIds: latestSync.state?.pProductIds ?? []
        };
    }
}
