import {
    ActivityPerformanceSchema,
    PlayerSchema,
} from "@blade-and-brawn/domain"
import { cors } from "@elysiajs/cors";
import { Elysia, NotFoundError, status, t } from "elysia";
import {
    PrintfulError,
    WebflowError,
    Printful,
    Webflow,
} from "@blade-and-brawn/commerce";
import { DEFAULT_NAME, env, log, sha256Sum } from "./util";
import serverTiming from "@elysia/server-timing";
import jwt from "@elysia/jwt";
import { CommerceService, WOrderStatusSchema } from "./services/commerce";
import cluster from "node:cluster";
import { randomUUIDv7, sleep } from "bun";
import { CalculatorService, CalculatorUnavailableError } from "./services/calculator";
import { StandardsParamsSchema } from "@blade-and-brawn/calculator";
import { StandardsService } from "./services/standards";
import { EventsService, EventStatusSchema } from "./services/events";

// CONSTANTS
// -----------------------
const EVENT_QUEUE_MANAGE_DELAY_MS = 1000   // 1  sec
const JWT_EXP = "1d";

// SERVICES
// -----------------------
const s = (() => {
    const Standards = new StandardsService();
    const Calculator = new CalculatorService(DEFAULT_NAME);
    const Commerce = new CommerceService();
    const Events = new EventsService();
    return { Standards, Calculator, Commerce, Events };
})();

// QUEUES
// -----------------------
const queues = [
    s.Commerce.Apparel.Syncs.Queue,
    s.Commerce.Apparel.Orders.Queue,
] as const;

// PLUGINS
// -----------------------
const authPlugin = new Elysia({ name: "auth" })
    .use(jwt({ name: "jwt", secret: env.AUTH_SECRET }))
    .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()) }) })
    .macro({
        auth: {
            async resolve({ jwt, cookie: { auth } }) {
                const token = auth.value && await jwt.verify(auth.value);
                if (!token || !token.sessionId) throw status(401, "Unauthorized");
                return { sessionId: token.sessionId.toString() };
            }
        }
    });

// 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",
            ],
        }),
    )
    .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()) }) })
    .use(authPlugin)

    .error({
        PrintfulError,
        WebflowError,
        CalculatorUnavailableError
    })

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

    .onAfterResponse(({ request, status, path }) => {
        if (env.NODE_ENV === "development") {
            const skip: Record<string, string[]> = { "/commerce/products/sync/": ["GET"] };
            if (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 } }) => {
        const match = crypto.timingSafeEqual(sha256Sum(body.password), Buffer.from(env.ADMIN_PASSWORD, "hex"));
        if (!match) throw status(401, "Invalid credentials");

        auth.set({
            value: await jwt.sign({ sessionId: randomUUIDv7(), exp: JWT_EXP }),
            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() }) })

    // CALCULATOR
    .group("/calculator", (app) => app
        // Non-authenticated
        .post("/calculate", async ({ body }) => {
            return { levels: await s.Calculator.calculate(body.player, body.activityPerformances) };
        }, {
            body: t.Object({
                player: PlayerSchema,
                activityPerformances: t.Array(ActivityPerformanceSchema)
            }),
        })
        // Authenticated
        .guard({ auth: true })
        .get("/standards/config", async () => {
            return await s.Calculator.Standards.Config.get();
        })
        .post("/standards/config/switch", async ({ body: { standardsConfigId } }) => {
            await s.Calculator.Standards.Config.switch(standardsConfigId);
        }, {
            body: t.Object({ standardsConfigId: t.String() })
        })
    )
    .group("/standards", { auth: true }, (app) => app
        .post("/configs", async ({ body: { name, datasetId, params } }) => {
            return await s.Standards.Configs.create(name, datasetId, params);
        }, {
            body: t.Object({ name: t.String(), datasetId: t.String(), params: StandardsParamsSchema })
        })
        .get("/configs", async () => {
            return await s.Standards.Configs.list();
        })
        .get("/configs/:id", async ({ params: { id } }) => {
            return await s.Standards.Configs.get(id);
        }, {
            params: t.Object({ id: t.String() })
        })
        .put("/configs/:id", async ({ params: { id }, body: { name, datasetId, params: parameters } }) => {
            await s.Standards.Configs.update(id, name, datasetId, parameters);
        }, {
            params: t.Object({ id: t.String() }),
            body: t.Object({ name: t.String(), datasetId: t.String(), params: StandardsParamsSchema })
        })
        .delete("/configs/:id", async ({ params: { id } }) => {
            await s.Standards.Configs.delete(id);
        }, { params: t.Object({ id: t.String() }) })
        .get("/datasets", async () => {
            return await s.Standards.Datasets.list();
        })
        .get("/datasets/:id", async ({ params: { id } }) => {
            return await s.Standards.Datasets.get(id);
        }, {
            params: t.Object({ id: t.String() })
        })
        .patch("/datasets/:id", async ({ params: { id }, body: { name } }) => {
            await s.Standards.Datasets.update(id, name);
        }, {
            params: t.Object({ id: t.String() }),
            body: t.Object({ name: t.String() })
        })
    )

    // COMMERCE
    .group("/commerce", { auth: true }, (app) => app
        .group("/products", (app) => app
            .get("/", async ({ query }) => {
                const pProducts = await s.Commerce.Printful.Products.list({
                    limit: query.limit,
                    offset: query.offset,
                });
                const isSyncedFlags = await Promise.all(
                    pProducts.map(async (pProduct) => {
                        const wProductId = pProduct.external_id.split("-")[0];
                        if (!wProductId) return false;
                        return (await s.Commerce.Webflow.Products.get(wProductId)) !== undefined;
                    })
                );
                return pProducts.map((pProduct, i) => ({ pProduct, isSynced: isSyncedFlags[i] }));
            }, {
                query: t.Object({
                    limit: t.Optional(t.Numeric()),
                    offset: t.Optional(t.Numeric()),
                })
            })
            .group("/sync", (app) => app
                // Sync status
                .get("/", async ({ sessionId }) => {
                    const latestSyncState = await s.Commerce.Apparel.Syncs.getLatestSyncState(sessionId);
                    if (!latestSyncState) throw new NotFoundError("No product sync found for the provided session");
                    return latestSyncState;
                })
                // Run sync
                .post("/:pProductId?", async ({ params: { pProductId }, sessionId }) => {
                    await s.Commerce.Apparel.Syncs.Queue.enqueue({
                        type: "apparel_sync_update",
                        source: "portal",
                        payload: {
                            session: { id: sessionId, name: "Portal" },
                            filter: {
                                pProductIds: pProductId ? [pProductId] : undefined
                            }
                        }
                    });
                }, { params: t.Object({ pProductId: t.Optional(t.Numeric()) }) }),
            )
            .get("/:pProductId", async ({ params: { pProductId } }) => {
                const pProduct = await s.Commerce.Printful.Products.get(pProductId);
                if (!pProduct) throw new NotFoundError("Missing printful product");

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

                const wProduct = await s.Commerce.Webflow.Products.get(wProductId);

                return { pProduct, wProduct };
            }, {
                params: t.Object({ pProductId: t.Numeric() })
            })
        )
        .group("/orders", (app) => app
            .get("/", async ({ query }) => {
                const wOrders = await s.Commerce.Webflow.Orders.list({
                    status: query.status,
                    limit: query.limit,
                    offset: query.offset,
                });
                const isSyncedFlags = await Promise.all(
                    wOrders.map(async (wOrder) =>
                        (await s.Commerce.Printful.Orders.get(`@${wOrder.orderId}`)) !== undefined
                    )
                );
                return wOrders.map((wOrder, i) => ({ wOrder, isSynced: isSyncedFlags[i] }));
            }, {
                query: t.Object({
                    status: t.Optional(WOrderStatusSchema),
                    limit: t.Optional(t.Numeric()),
                    offset: t.Optional(t.Numeric()),
                })
            })
            .get("/:wOrderId", async ({ params: { wOrderId } }) => {
                const wOrder = await s.Commerce.Webflow.Orders.get(wOrderId);
                if (!wOrder) throw new NotFoundError("Missing webflow order");

                const pOrder = await s.Commerce.Printful.Orders.get(`@${wOrder.orderId}`);

                return { wOrder, pOrder };
            }, { params: t.Object({ wOrderId: t.String() }) })
        )
    )

    // EVENTS
    .group("/events", { auth: true }, (app) => app
        .get("/", async ({ query }) => {
            const events = await s.Events.list({
                filter: { status: query.status, type: query.type, group: query.group },
                limit: query.limit,
                offset: query.offset,
            });
            return events.map((event) => ({ ...event, status: EventsService.status(event) }));
        }, {
            query: t.Object({
                status: t.Optional(EventStatusSchema),
                type: t.Optional(t.String()),
                group: t.Optional(t.String()),
                limit: t.Optional(t.Numeric()),
                offset: t.Optional(t.Numeric()),
            })
        })
        .get("/:id", async ({ params: { id } }) => {
            const event = await s.Events.get(id);
            if (!event) throw new NotFoundError("Event not found");
            return { ...event, status: EventsService.status(event) };
        }, {
            params: t.Object({ id: t.String() })
        })
        .post("/:id/retry", async ({ params: { id } }) => {
            const retried = await s.Events.retry(id);
            if (!retried) throw new NotFoundError("Event not found or not in a failed state");
        }, {
            params: t.Object({ id: t.String() })
        })
        .get("/groups", async ({ }) => queues.map((q) => q.group))
    )

    // WEBHOOKS
    .post("/webhooks/printful", async ({ body, query }) => {
        // https://webflow.com/integrations/printful
        const verified = crypto.timingSafeEqual(Buffer.from(query.secret, "hex"), Buffer.from(env.PRINTFUL_WEBHOOK_SECRET, "hex"));
        if (!verified) throw status(400, "Invalid secret");

        const payload = body as Printful.Webhook.EventPayload;

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

                await s.Commerce.Apparel.Syncs.Queue.enqueue({
                    type: "apparel_sync_update",
                    source: "printful",
                    payload: {
                        session: { id: randomUUIDv7(), name: "Printful" },
                        filter: { pProductIds: [pProduct.id] }
                    }
                });
                break;
            }
            case Printful.Webhook.Event.ProductDeleted: {
                const pProduct = payload.data.sync_product;
                const wProductId = pProduct.external_id.split("-")[0];
                log.info({ externalId: payload.data.sync_product.external_id, wProductId }, "printful webhook: product deleted");
                if (!wProductId) throw new NotFoundError("Missing webflow product ID");

                await s.Commerce.Apparel.Syncs.Queue.enqueue({
                    type: "apparel_sync_delete",
                    source: "printful",
                    payload: { wProductId }
                });

                break;
            }
            case Printful.Webhook.Event.PackageShipped: {
                const wOrderId = payload.data.order.external_id;
                const shipment = payload.data.shipment;
                log.info({ wOrderId, carrier: shipment.carrier, tracking: shipment.tracking_number }, "printful webhook: package shipped");

                // TODO: possible to have multiple packages shipped
                // order should NOT be immediately fulfulled in that case

                await s.Commerce.Apparel.Orders.Queue.enqueue({
                    type: "apparel_order_fulfill",
                    source: "printful",
                    payload: { wOrderId, shipment }
                });

                break;
            }
            default:
                log.warn({ type: (payload as any).type }, "printful webhook: unhandled event type");
        }
    }, { query: t.Object({ secret: t.String() }) })
    .post("/webhooks/webflow", async ({ request, body }) => {
        if (!s.Commerce.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 wOrder = payload.payload;
                log.info({ orderId: wOrder.orderId }, "webflow webhook: order created");

                await s.Commerce.Apparel.Orders.Queue.enqueue({
                    type: "apparel_order_create",
                    source: "webflow",
                    payload: { wOrder }
                });

                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")

    // MANAGE QUEUES
    for (const queue of queues) {
        (async () => {
            while (true) {
                try {
                    // clean, if ready
                    if ((Date.now() - queue.lastCleanDate.getTime()) >= EventsService.CONCURRENCY_TIMEOUT_MS)
                        await queue.clean().catch((err) => log.error({ name: queue.group, err }));
                    // drain
                    await queue.drain();
                }
                catch (err) {
                    log.error({ name: queue.group, err }, "error occurred during queue management");
                }
                await sleep(EVENT_QUEUE_MANAGE_DELAY_MS);
            }
        })();
    }
});

export type API = typeof app
export { type EventStatus } from "./services/events";
