import {
    DEFAULT_STANDARDS_DATA,
    LevelCalculator,
    Standards,
} from "@blade-and-brawn/calculator";

import {
    ActivityPerformanceSchema,
    PlayerSchema,
} from "@blade-and-brawn/domain"
import { cors } from "@elysiajs/cors";
import { Elysia, NotFoundError, status, t } from "elysia";
import {
    PrintfulClient,
    PrintfulError,
    WebflowClient,
    WebflowError,
    Printful,
    Webflow,
} from "@blade-and-brawn/commerce";
import zipcodesUs from "zipcodes-us";
import { env, log } from "./util";
import serverTiming from "@elysia/server-timing";
import jwt from "@elysia/jwt";
import { SyncService } from "./services/sync";
import cluster from "node:cluster";

// SERVICES
// -----------
const levelCalculator = new LevelCalculator(
    new Standards(DEFAULT_STANDARDS_DATA),
);

const printful = new PrintfulClient({
    token: env.PRINTFUL_AUTH,
    storeId: env.PRINTFUL_STORE_ID,
});

const webflow = new WebflowClient({
    siteId: env.WEBFLOW_SITE_ID,
    collectionsId: env.WEBFLOW_COLLECTION_ID,
    token: env.WEBFLOW_AUTH,
    webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
});

const syncService = new SyncService(printful, webflow);

// ELYSIA
// -----------
export const app = new Elysia()
    .use(serverTiming())
    .use(
        cors({
            origin: [
                // production
                /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
                // testing
                /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
                // development
                "http://localhost:5173",
            ],
        }),
    ).use(jwt({ name: "jwt", secret: env.AUTH_SECRET }))

    .error({
        PrintfulError,
        WebflowError,
    })

    .onError(({ code, error }) => {
        switch (code) {
            case "PrintfulError":
            case "WebflowError":
                log.error(
                    { upstreamStatus: error.status, payload: error.payload },
                    error.message,
                );
                return status(502, { error: error.message });
            case "NOT_FOUND":
                return status(404, { error: "Not found" });
            case "VALIDATION":
                return status(400, { error: error.message });
            default:
                log.error({ err: error }, "unhandled error");
        }
    })

    .onAfterResponse(({ request, status, path }) => {
        const skip: Record<string, string[]> = { "/products/sync/": ["GET"] };
        if (env.NODE_ENV === "development" && skip[path]?.includes(request.method)) return;

        log.info({
            method: request.method,
            path,
            status
        }, "request");
    })

    .get("/", () => ({ status: "ok" }))
    .get("/health", () => ({ status: "ok" }))

    // AUTHENTICATION
    .post("/auth/login", async ({ jwt, body, cookie: { auth } }) => {
        if (body.password !== env.ADMIN_PASSWORD) throw status(401, "Invalid credentials");

        auth.set({
            value: await jwt.sign({ id: "admin", exp: "7d" }),
            path: "/",
            maxAge: 60 * 60 * 24 * 7,
            sameSite: "lax",
            httpOnly: true,
            secure: env.NODE_ENV === "production",
            domain: env.NODE_ENV === "production" ?
                ".bladeandbrawn.com" :
                undefined
        });
    }, {
        body: t.Object({
            password: t.String()
        }),
        cookie: t.Cookie({ auth: t.Optional(t.String()) })
    })

    // CALCULATOR
    .post("/calculate", async ({ body }) => {
        return {
            levels: levelCalculator.calculate(body.player, body.activityPerformances),
        };
    }, {
        body: t.Object({
            player: PlayerSchema,
            activityPerformances: t.Array(ActivityPerformanceSchema)
        }),
    })

    // COMMERCE
    .group("/products", (app) =>
        app
            .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()) }) })
            .onBeforeHandle(async ({ jwt, cookie: { auth } }) => {
                const token = auth.value && await jwt.verify(auth.value);
                if (!token) throw status(401, "Unauthorized");
            })
            .group("/sync", (app) =>
                app
                    // Sync status
                    .get("/", async ({ }) => {
                        const activeSync = await syncService.getActiveSync();
                        if (!activeSync) throw new NotFoundError("No sync in progress");
                        return {
                            startDate: activeSync.started_at,
                            syncingPrintfulProductIds: activeSync.syncing_printful_product_ids
                        }
                    })
                    // Run sync
                    .post("/:printfulProductId?", async ({ params: { printfulProductId } }) => {
                        const printfulProductIds = printfulProductId ? [printfulProductId] : null;
                        log.debug({ printfulProductIds });
                        await syncService.sync({ filter: { printfulProductIds } });
                    }, { params: t.Object({ printfulProductId: t.Optional(t.Numeric()) }) }),
            )
            .get("/:printfulProductId", async ({ params }) => {
                const printfulProduct = await printful.Products.get(params.printfulProductId);
                if (!printfulProduct) throw new NotFoundError("Missing printful product");

                const webflowProductId = printfulProduct.sync_product.external_id.split("-")[0];
                if (!webflowProductId) throw new NotFoundError("Missing webflow product ID");

                const webflowProduct = await webflow.Products.get(webflowProductId);

                return { printfulProduct, webflowProduct };
            }, {
                params: t.Object({ printfulProductId: t.Numeric() })
            })
    )

    // WEBHOOKS
    .post("/webhook/printful", async ({ body }) => {
        // https://webflow.com/integrations/printful
        const payload = body as Printful.Webhook.EventPayload;

        switch (payload.type) {
            case Printful.Webhook.Event.ProductUpdated: {
                const printfulProduct = payload.data.sync_product;
                log.info({ productId: printfulProduct.id }, "printful webhook: product updated");

                await syncService.sync({ filter: { printfulProductIds: [printfulProduct.id] } });
                break;
            }
            case Printful.Webhook.Event.ProductDeleted: {
                const printfulProduct = payload.data.sync_product;
                const webflowProductId = printfulProduct.external_id.split("-")[0];
                log.info({ externalId: payload.data.sync_product.external_id, webflowProductId }, "printful webhook: product deleted");
                if (!webflowProductId) throw new NotFoundError("Missing webflow product ID");

                await webflow.Products.remove(webflowProductId);
                break;
            }
            case Printful.Webhook.Event.PackageShipped: {
                const webflowOrderId = payload.data.order.external_id;
                const shipInfo = payload.data.shipment;
                log.info({ webflowOrderId, carrier: shipInfo.carrier, tracking: shipInfo.tracking_number }, "printful webhook: package shipped");

                await webflow.Orders.update(webflowOrderId, {
                    shippingTrackingURL: shipInfo.tracking_url,
                    shippingTracking: shipInfo.tracking_number,
                    shippingProvider: shipInfo.carrier,
                });
                await webflow.Orders.fulfill(webflowOrderId, {
                    sendOrderFulfilledEmail: true,
                });
                break;
            }
            default:
                log.warn({ type: (payload as any).type }, "printful webhook: unhandled event type");
        }
    })
    .post("/webhook/webflow", async ({ request, body }) => {
        if (!webflow.Util.verifyWebflowSignature(request, body))
            throw status(400, "Invalid signature");

        const payload = body as Webflow.Webhook.EventPayload;

        switch (payload.triggerType) {
            case Webflow.Webhook.Event.OrderCreated: {
                const webflowOrder = payload.payload;
                log.info({ orderId: webflowOrder.orderId }, "webflow webhook: order created");

                await printful.Orders.create({
                    external_id: webflowOrder.orderId,
                    // TODO: derive from webflow
                    shipping: "STANDARD",
                    recipient: {
                        name: webflowOrder.shippingAddress.addressee,
                        address1: webflowOrder.shippingAddress.line1,
                        address2: webflowOrder.shippingAddress.line2,
                        city: webflowOrder.shippingAddress.city,
                        state_code: zipcodesUs.find(
                            webflowOrder.shippingAddress.postalCode.split("-")[0] ?? "",
                        ).stateCode,
                        country_code: webflowOrder.shippingAddress.country,
                        zip: webflowOrder.shippingAddress.postalCode,
                    },
                    items: webflowOrder.purchasedItems.map((webflowOrderSku) => ({
                        external_variant_id: webflowOrderSku.variantId,
                        quantity: webflowOrderSku.count,
                    })),
                });

                break;
            }
            default:
                log.warn({ triggerType: (payload as any).triggerType }, "webflow webhook: unhandled event type");
        }
    });

app.listen(3000, async () => {
    if (cluster.worker?.id === 1) {
        log.info({ port: 3000 }, "server started")
        await syncService.syncNext().catch((err) => log.error({ err }, "failed to resume product sync queue"));
    }
});

export type API = typeof app
