import type { Printful } from "./util/types";
import { type DeepPartial, type PagingOptions } from "./util/misc";

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

type ClientOptions = {
    token: string;
    storeId: string;
};

type Credentials = {
    apiUrl: string;
    authHeaders: Record<string, string>;
};

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

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

    async get(variantId: number | string): Promise<Printful.Products.SyncVariant | undefined> {
        const res = await fetch(`${this.creds.apiUrl}/store/variants/${variantId}`, {
            method: "GET",
            headers: this.creds.authHeaders,
        });
        if (res.status === 404 || res.status === 400) return;
        if (!res.ok) throw new PrintfulError("Failed to get Printful variant", res.status, await parsePayload(res));
        const payload = (await res.json()) as Printful.MetaDataSingle<Printful.Products.SyncVariant>;
        return payload.result;
    }
}

class ProductsClient {
    readonly Variants: VariantsClient;

    constructor(private creds: Credentials) {
        this.Variants = new VariantsClient(creds);
    }

    async list(opt: PagingOptions = {}): Promise<Printful.Products.SyncProduct[]> {
        const allSyncProducts: Printful.Products.SyncProduct[] = [];
        let offset = opt.offset ?? 0;
        do {
            const res = await fetch(`${this.creds.apiUrl}/store/products?offset=${offset}&limit=${opt.limit ?? 100}`, {
                method: "GET",
                headers: this.creds.authHeaders,
            });
            if (!res.ok) throw new PrintfulError("Failed to list Printful products", res.status, await parsePayload(res));
            const payload = (await res.json()) as Printful.MetaDataMulti<Printful.Products.SyncProduct>;
            allSyncProducts.push(...payload.result);
            offset = allSyncProducts.length;
            if (allSyncProducts.length >= payload.paging.total) break;
        } while (opt.forceAll);
        return allSyncProducts;
    }

    async get(productId: number | string): Promise<Printful.Products.Product | undefined> {
        const res = await fetch(`${this.creds.apiUrl}/store/products/${productId}`, {
            method: "GET",
            headers: this.creds.authHeaders,
        });
        if (res.status === 404 || res.status === 400) return;
        if (!res.ok) throw new PrintfulError("Failed to get Printful product", res.status, await parsePayload(res));
        const payload = (await res.json()) as Printful.MetaDataSingle<Printful.Products.Product>;
        return payload.result;
    }

    async update(pProductId: number, pProduct: DeepPartial<Printful.Products.Product>) {
        const res = await fetch(`${this.creds.apiUrl}/store/products/${pProductId}`, {
            method: "PUT",
            headers: {
                "Content-Type": "application/json",
                ...this.creds.authHeaders,
            },
            body: JSON.stringify(pProduct),
        });
        if (!res.ok) throw new PrintfulError("Failed to update Printful product", res.status, await parsePayload(res));
    }
}

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

    async create(pOrder: Printful.Orders.Order) {
        const res = await fetch(`${this.creds.apiUrl}/orders`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                ...this.creds.authHeaders,
            },
            body: JSON.stringify(pOrder),
        });
        if (!res.ok) throw new PrintfulError("Failed to create Printful order", res.status, await parsePayload(res));
    }

    async list(opt: PagingOptions = {}): Promise<Printful.Orders.Order[]> {
        const allPOrders: Printful.Orders.Order[] = [];
        let offset = opt.offset ?? 0;
        do {
            const res = await fetch(`${this.creds.apiUrl}/orders?offset=${offset}&limit=${opt.limit ?? 100}`, {
                method: "GET",
                headers: this.creds.authHeaders,
            });
            if (!res.ok) throw new PrintfulError("Failed to list Printful orders", res.status, await parsePayload(res));
            const payload = (await res.json()) as Printful.MetaDataMulti<Printful.Orders.Order>;
            allPOrders.push(...payload.result);
            offset = allPOrders.length;
            if (allPOrders.length >= payload.paging.total) break;
        } while (opt.forceAll);
        return allPOrders;
    }
}

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

    async get(): Promise<Printful.Webhook.Config | undefined> {
        const res = await fetch(`${this.creds.apiUrl}/webhooks`, {
            method: "GET",
            headers: {
                "Content-Type": "application/json",
                ...this.creds.authHeaders,
            }
        });
        if (res.status === 404 || res.status === 400) return;
        if (!res.ok) throw new PrintfulError("Failed to get webhook configuration", res.status, await parsePayload(res));
        const payload = (await res.json()) as Printful.Webhook.Config;
        return payload;
    }


    /**
     * Printful only supports one URL at a time, so subsequent calls will overwrite the previous webhook
     */
    async create(url: string, events: Printful.Webhook.Event[]) {
        const res = await fetch(`${this.creds.apiUrl}/webhooks`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                ...this.creds.authHeaders,
            },
            body: JSON.stringify({
                "url": url,
                "types": events
            }),
        });
        if (!res.ok) throw new PrintfulError("Failed to register webhook URL", res.status, await parsePayload(res));
    }
}

export class PrintfulClient {
    readonly Products: ProductsClient;
    readonly Orders: OrdersClient;
    readonly Webhooks: WebhooksClient;

    constructor(options: ClientOptions) {
        const creds: Credentials = {
            apiUrl: "https://api.printful.com",
            authHeaders: {
                Authorization: `Bearer ${options.token}`,
                "X-PF-Store-Id": options.storeId,
            },
        };
        this.Products = new ProductsClient(creds);
        this.Orders = new OrdersClient(creds);
        this.Webhooks = new WebhooksClient(creds);
    }

    static Util = {
        getVariantMainImage(syncVariant: Printful.Products.SyncVariant): string {
            const previewFile = syncVariant.files.find((f) => f.type === "preview");
            return previewFile?.preview_url ?? syncVariant.product.image;
        },
    };
}
