import { PrintfulClient, ProductSyncer, WebflowClient, type ProductSyncerOptions } from "@blade-and-brawn/commerce";
import { log } from "../util";
import { db } from "../database/db";
import { DatabaseError } from "pg";

type QueuedSync = {
    id?: string | null,
    filter: ProductSyncerOptions["filter"]
}

class AlreadyClaimedError extends Error { }

export class SyncService {
    private readonly productSyncer: ProductSyncer;
    readonly queue: SyncQueue;

    constructor(printful: PrintfulClient, webflow: WebflowClient) {
        this.productSyncer = new ProductSyncer(printful, webflow, log);
        this.queue = new SyncQueue();
    }

    async sync(queuedSync: QueuedSync) {
        let id = queuedSync.id ?? null;
        let freedSlot = false;
        try {
            log.info("starting sync run");
            await this.productSyncer.sync({
                filter: queuedSync.filter,
                onStart: async (startDate) => {
                    const values = {
                        started_at: startDate,
                        printful_product_id_filter: queuedSync.filter?.printfulProductIds ?? null,
                    };
                    if (id) {
                        const result = await db.updateTable("product_syncs")
                            .set(values)
                            .where("id", "=", id)
                            .where("started_at", "is", null)
                            .executeTakeFirst();

                        // Guard against another worker/machine having already claimed
                        // this queued row between `queue.front()` and now.
                        if (result.numUpdatedRows === 0n) throw new AlreadyClaimedError();
                    }
                    else {
                        const result = await db.insertInto("product_syncs")
                            .values(values)
                            .returning("id")
                            .executeTakeFirstOrThrow();
                        id = result.id;
                    }
                },
                onStep: async (printfulProductIds: number[]) => {
                    if (!id) throw new Error("Expected sync run ID");
                    await db.updateTable("product_syncs")
                        .set({ syncing_printful_product_ids: printfulProductIds })
                        .where("id", "=", id)
                        .execute();
                },
                onCompletion: async (completionDate, duration) => {
                    if (!id) throw new Error("Expected sync run ID");
                    await db.updateTable("product_syncs")
                        .set({ syncing_printful_product_ids: [], ended_at: completionDate })
                        .where("id", "=", id)
                        .execute();
                    freedSlot = true;
                }
            });
        }
        catch (err) {
            if (err instanceof AlreadyClaimedError) {
                log.info("sync already claimed by another worker, skipping");
                return;
            }

            const activeSync = await this.getActiveSync();

            const activeSyncConflict = activeSync && err instanceof DatabaseError && err.code === "23505";
            if (activeSyncConflict) {
                // If active sync has been syncing for 10 min (600000ms), clear it and retry this sync
                if (Date.now() - (activeSync.started_at?.getTime() ?? 0) > 600000) {
                    log.info("already active sync exceeded timeout");
                    await db.updateTable("product_syncs")
                        .set({ syncing_printful_product_ids: [], ended_at: new Date(), has_failed: true })
                        .where("id", "=", activeSync.id)
                        .execute();
                    await this.sync(queuedSync);
                }
                else {
                    log.info("sync already active, enqueuing next sync");
                    await this.queue.enqueue(queuedSync);
                }
            }
            else {
                if (id) {
                    await db.updateTable("product_syncs")
                        .set({ syncing_printful_product_ids: [], ended_at: new Date(), has_failed: true })
                        .where("id", "=", id)
                        .execute();
                    freedSlot = true;
                }
                throw err;
            }
        }
        finally {
            if (freedSlot)
                this.syncNext().catch((err) => log.error({ err }, "failed to process next queued sync"));
        }
    }

    async syncNext() {
        const nextSync = await this.queue.front();
        if (nextSync) {
            await this.sync({
                id: nextSync.id,
                filter: { printfulProductIds: nextSync.printful_product_id_filter }
            });
        }
    }

    async getActiveSync() {
        return await db.selectFrom("product_syncs")
            .selectAll()
            .where("started_at", "is not", null)
            .where("ended_at", "is", null)
            .limit(1)
            .executeTakeFirst();
    }
}

class SyncQueue {
    async enqueue(queuedSync: QueuedSync) {
        const values = { printful_product_id_filter: queuedSync.filter?.printfulProductIds };
        if (queuedSync.id) {
            await db.updateTable("product_syncs")
                .set(values)
                .where("id", "=", queuedSync.id)
                .execute();
        }
        else {
            await db.insertInto("product_syncs")
                .values(values)
                .execute();
        }
    }

    async front() {
        return await db.selectFrom("product_syncs")
            .selectAll()
            .where("started_at", "is", null)
            .orderBy("created_at", "asc")
            .limit(1)
            .executeTakeFirst();
    }
}
