import type { Webflow } from "./util/types";
import { type DeepPartial, type PagingOptions } from "./util/misc";
import crypto from "node:crypto";

const parsePayload = (res: Response): Promise<unknown> =>
    res.json().catch(() => res.text().catch(() => undefined));

type ClientOptions = {
    siteId: string;
    collectionsId: string;
    token: string;
    webhookSecret: string;
};

type Credentials = {
    apiBaseUrl: string;
    apiSitesUrl: string;
    apiCollectionsUrl: string;
    authHeader: Record<string, string>;
};

export class WebflowError extends Error {
    constructor(
        message: string,
        public status: number,
        public payload: unknown,
    ) {
        super(message);
        this.name = "WebflowError";
    }
}

class SkusClient {
    constructor(private creds: Credentials) { }

    async create(
        webflowProductId: string,
        skus: DeepPartial<Webflow.Products.Skus.Sku>[],
    ): Promise<string[]> {
        const res = await fetch(
            `${this.creds.apiSitesUrl}/products/${webflowProductId}/skus`,
            {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    ...this.creds.authHeader,
                },
                body: JSON.stringify({ skus }),
            },
        );
        if (!res.ok) throw new WebflowError("Failed to create Webflow product SKU", res.status, await parsePayload(res));
        const payload = (await res.json()) as { skus: { id: string }[] };
        return payload.skus.map((sku) => sku.id);
    }

    async update(
        webflowProductId: string,
        webflowSkuId: string,
        webflowSku: DeepPartial<Webflow.Products.Skus.Sku>,
    ): Promise<void> {
        const res = await fetch(
            `${this.creds.apiSitesUrl}/products/${webflowProductId}/skus/${webflowSkuId}`,
            {
                method: "PATCH",
                headers: {
                    "Content-Type": "application/json",
                    ...this.creds.authHeader,
                },
                body: JSON.stringify({ sku: webflowSku }),
            },
        );
        if (!res.ok) throw new WebflowError("Failed to update Webflow product SKU", res.status, await parsePayload(res));
    }
}

class ProductsClient {
    readonly Skus: SkusClient;

    constructor(private creds: Credentials) {
        this.Skus = new SkusClient(creds);
    }

    async create(
        webflowProductAndSku: DeepPartial<Webflow.Products.ProductAndSku>,
    ): Promise<string> {
        const res = await fetch(`${this.creds.apiSitesUrl}/products`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                ...this.creds.authHeader,
            },
            body: JSON.stringify(webflowProductAndSku),
        });
        if (!res.ok) throw new WebflowError("Failed to create Webflow product", res.status, await parsePayload(res));
        const payload = (await res.json()) as { product: { id: string } };
        return payload.product.id;
    }

    async list(opt: PagingOptions = {}): Promise<Webflow.Products.ProductAndSkus[]> {
        const allWebflowProducts: Webflow.Products.ProductAndSkus[] = [];
        let offset = opt.offset ?? 0;
        do {
            const res = await fetch(`${this.creds.apiSitesUrl}/products?offset=${offset}&limit=${opt.limit ?? 100}`, {
                method: "GET",
                headers: this.creds.authHeader,
            });
            if (!res.ok) throw new WebflowError("Failed to list Webflow products", res.status, await parsePayload(res));
            const payload = (await res.json()) as Webflow.MetaDataMulti<"items", Webflow.Products.ProductAndSkus>;
            allWebflowProducts.push(...payload.items);
            offset = allWebflowProducts.length;
            if (allWebflowProducts.length >= (payload?.pagination?.total ?? 0)) break;
        } while (opt.forceAll);
        return allWebflowProducts;
    }

    async get(webflowProductId: string): Promise<Webflow.Products.ProductAndSkus | undefined> {
        const res = await fetch(`${this.creds.apiSitesUrl}/products/${webflowProductId}`, {
            method: "GET",
            headers: this.creds.authHeader,
        });
        if (res.status === 404 || res.status === 400) return;
        if (!res.ok) throw new WebflowError("Failed to get Webflow product", res.status, await parsePayload(res));
        return (await res.json()) as Webflow.Products.ProductAndSkus;
    }

    async update(
        webflowProductId: string,
        webflowProduct: DeepPartial<Webflow.Products.ProductAndSku>,
    ): Promise<void> {
        const res = await fetch(`${this.creds.apiSitesUrl}/products/${webflowProductId}`, {
            method: "PATCH",
            headers: {
                "Content-Type": "application/json",
                ...this.creds.authHeader,
            },
            body: JSON.stringify(webflowProduct),
        });
        if (!res.ok) throw new WebflowError("Failed to update Webflow product", res.status, await parsePayload(res));
    }

    async remove(webflowProductId: string): Promise<void> {
        const res = await fetch(
            `${this.creds.apiCollectionsUrl}/items/${webflowProductId}`,
            {
                method: "DELETE",
                headers: {
                    "Content-Type": "application/json",
                    ...this.creds.authHeader,
                },
                body: JSON.stringify({}),
            },
        );
        if (!res.ok) throw new WebflowError("Failed to remove Webflow product", res.status, await parsePayload(res));
    }
}

class OrdersClient {
    constructor(private creds: Credentials) { }

    async list(opt: PagingOptions & { status?: Webflow.Orders.Order["status"] } = {}): Promise<Webflow.Orders.Order[]> {
        const allOrders: Webflow.Orders.Order[] = [];
        let offset = opt.offset ?? 0;
        do {
            const params = new URLSearchParams();
            if (opt.status !== undefined) params.set("status", opt.status);
            params.set("offset", String(offset));
            params.set("limit", String(opt.limit ?? 100));
            const res = await fetch(`${this.creds.apiSitesUrl}/orders?${params.toString()}`, {
                method: "GET",
                headers: this.creds.authHeader,
            });
            if (!res.ok) throw new WebflowError("Failed to list Webflow orders", res.status, await parsePayload(res));
            const payload = (await res.json()) as Webflow.MetaDataMulti<"orders", Webflow.Orders.Order>;
            allOrders.push(...payload.orders);
            offset = allOrders.length;
            if (allOrders.length >= (payload?.pagination?.total ?? 0)) break;
        } while (opt.forceAll);
        return allOrders;
    }

    async update(
        webflowOrderId: string,
        webflowOrderUpdate: {
            comment?: string;
            shippingProvider?: string;
            shippingTracking?: string;
            shippingTrackingURL: string;
        },
    ): Promise<void> {
        const res = await fetch(`${this.creds.apiSitesUrl}/orders/${webflowOrderId}`, {
            method: "PATCH",
            headers: {
                "Content-Type": "application/json",
                ...this.creds.authHeader,
            },
            body: JSON.stringify(webflowOrderUpdate),
        });
        if (!res.ok) throw new WebflowError("Failed to update Webflow order", res.status, await parsePayload(res));
    }

    async fulfill(
        webflowOrderId: string,
        opt: { sendOrderFulfilledEmail?: boolean } = {},
    ): Promise<void> {
        const res = await fetch(`${this.creds.apiSitesUrl}/orders/${webflowOrderId}/fulfill`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                ...this.creds.authHeader,
            },
            body: JSON.stringify(opt),
        });
        if (!res.ok) throw new WebflowError("Failed to fulfill Webflow order", res.status, await parsePayload(res));
    }
}

class WebhooksClient {
    constructor(private creds: Credentials) { }

    async list(opt: PagingOptions = {}): Promise<Webflow.Webhook.Config[]> {
        const allWebhooks: Webflow.Webhook.Config[] = [];
        let offset = opt.offset ?? 0;
        do {
            const res = await fetch(`${this.creds.apiSitesUrl}/webhooks?offset=${offset}&limit=${opt.limit ?? 100}`, {
                method: "GET",
                headers: this.creds.authHeader,
            });
            if (!res.ok) throw new WebflowError("Failed to list Webflow webhooks", res.status, await parsePayload(res));
            const payload = (await res.json()) as Webflow.MetaDataMulti<"webhooks", Webflow.Webhook.Config>;
            allWebhooks.push(...payload.webhooks);
            offset = allWebhooks.length;
            if (allWebhooks.length >= (payload?.pagination?.total ?? 0)) break;
        } while (opt.forceAll);
        return allWebhooks;
    }

    /**
     * Limit of 75 registrations per event
     */
    async create(url: string, event: Webflow.Webhook.Event) {
        const res = await fetch(`${this.creds.apiSitesUrl}/webhooks`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                ...this.creds.authHeader,
            },
            body: JSON.stringify({
                "url": url,
                "triggerType": event
            }),
        });
        if (!res.ok) throw new WebflowError("Failed to create Webflow webhook", res.status, await parsePayload(res));
    }

    async remove(webhookId: string): Promise<void> {
        const res = await fetch(
            `${this.creds.apiBaseUrl}/webhooks/${webhookId}`,
            {
                method: "DELETE",
                headers: {
                    "Content-Type": "application/json",
                    ...this.creds.authHeader,
                },
                body: JSON.stringify({}),
            },
        );
        if (!res.ok) throw new WebflowError("Failed to remove Webflow webhook", res.status, await parsePayload(res));
    }
}

class UtilClient {
    constructor(private webhookSecret: string) { }

    verifyWebflowSignature(request: Request, body: unknown): boolean {
        const timestamp = request.headers.get("x-webflow-timestamp");
        if (!timestamp) return false;

        const providedSignature = request.headers.get("x-webflow-signature");
        if (!providedSignature) return false;

        if (!body) return false;

        const requestTimestamp = parseInt(timestamp, 10);
        if (Date.now() - requestTimestamp > 300000) return false;

        const data = `${requestTimestamp}:${JSON.stringify(body)}`;
        const hash = crypto
            .createHmac("sha256", this.webhookSecret)
            .update(data)
            .digest("hex");

        try {
            return crypto.timingSafeEqual(
                Buffer.from(hash, "hex"),
                Buffer.from(providedSignature, "hex"),
            );
        } catch {
            return false;
        }
    }
}

export class WebflowClient {
    readonly Products: ProductsClient;
    readonly Orders: OrdersClient;
    readonly Webhooks: WebhooksClient;
    readonly Util: UtilClient;

    constructor(options: ClientOptions) {
        const creds: Credentials = {
            apiBaseUrl: `https://api.webflow.com/v2`,
            apiSitesUrl: `https://api.webflow.com/v2/sites/${options.siteId}`,
            apiCollectionsUrl: `https://api.webflow.com/v2/collections/${options.collectionsId}`,
            authHeader: { Authorization: `bearer ${options.token}` },
        };
        this.Products = new ProductsClient(creds);
        this.Orders = new OrdersClient(creds);
        this.Webhooks = new WebhooksClient(creds);
        this.Util = new UtilClient(options.webhookSecret);
    }
}
