import type { App, AppPlugin } from "bknd";
import { s, jsc, mergeObject, pickHeaders2 } from "bknd/utils";

/**
 * check RequestInitCfPropertiesImage
 */
const schema = s.partialObject({
   dpr: s.number({ minimum: 1, maximum: 3 }),
   fit: s.string({ enum: ["scale-down", "contain", "cover", "crop", "pad"] }),
   format: s.string({
      enum: ["auto", "avif", "webp", "jpeg", "baseline-jpeg", "json"],
      default: "auto",
   }),
   height: s.number(),
   width: s.number(),
   metadata: s.string({ enum: ["copyright", "keep", "none"] }),
   quality: s.number({ minimum: 1, maximum: 100 }),
});
export type CloudflareImageOptimizationSchema = s.Static<typeof schema>;

export type CloudflareImageOptimizationOptions = {
   /**
    * The url to access the image optimization plugin
    * @default /api/plugin/image/optimize
    */
   accessUrl?: string;
   /**
    * The path to resolve the image from
    * @default /api/media/file
    */
   resolvePath?: string;
   /**
    * Whether to explain the image optimization schema
    * @default false
    */
   explain?: boolean;
   /**
    * The default options to use
    * @default {}
    */
   defaultOptions?: CloudflareImageOptimizationSchema;
   /**
    * The fixed options to use
    * @default {}
    */
   fixedOptions?: CloudflareImageOptimizationSchema;
   /**
    * The cache control to use
    * @default public, max-age=31536000, immutable
    */
   cacheControl?: string;
   /**
    * Whether to fallback to the original image if the image optimization fails
    * @default false
    */
   fallbackRedirect?: boolean;
};

export function cloudflareImageOptimization({
   accessUrl = "/api/plugin/image/optimize",
   resolvePath = "/api/media/file",
   explain = false,
   defaultOptions = {},
   fixedOptions = {},
   fallbackRedirect = false,
}: CloudflareImageOptimizationOptions = {}): AppPlugin {
   const disallowedAccessUrls = ["/api", "/admin", "/api/plugin"];
   if (disallowedAccessUrls.includes(accessUrl) || accessUrl.length < 2) {
      throw new Error(`Disallowed accessUrl: ${accessUrl}`);
   }

   return (app: App) => ({
      name: "cf-image-optimization",
      onBuilt: () => {
         if (explain) {
            app.server.get(accessUrl, async (c) => {
               return c.json({
                  searchParams: schema.toJSON(),
               });
            });
         }
         app.server.get(`${accessUrl}/:path{.+$}`, jsc("query", schema), async (c) => {
            const request = c.req.raw;
            const url = new URL(request.url);

            const storage = app.module.media?.storage;
            if (!storage) {
               throw new Error("No media storage configured");
            }

            const path = c.req.param("path");
            if (!path) {
               throw new Error("No url provided");
            }

            const imageURL = `${url.origin}${resolvePath}/${path}`;
            //const metadata = await storage.objectMetadata(path);

            // Copy parameters from query string to request options.
            // You can implement various different parameters here.
            const options = mergeObject(
               structuredClone(defaultOptions),
               c.req.valid("query"),
               structuredClone(fixedOptions),
            );

            // Your Worker is responsible for automatic format negotiation. Check the Accept header.
            if (options.format) {
               if (options.format === "auto") {
                  const accept = request.headers.get("Accept")!;
                  if (/image\/avif/.test(accept)) {
                     options.format = "avif";
                  } else if (/image\/webp/.test(accept)) {
                     options.format = "webp";
                  }
               }
            }

            // Build a request that passes through request headers
            const imageRequest = new Request(imageURL, {
               headers: request.headers,
            });

            // Returning fetch() with resizing options will pass through response with the resized image.
            const res = await fetch(imageRequest, { cf: { image: options as any } });

            if (!res.ok && fallbackRedirect) {
               // add redirect response to the original image
               return new Response(null, { status: 307, headers: { Location: imageURL } });
            }

            const headers = pickHeaders2(res.headers, [
               "Content-Type",
               "Content-Length",
               "Age",
               "Date",
               "Last-Modified",
            ]);
            headers.set("Cache-Control", "public, max-age=31536000, immutable");

            return new Response(res.body, {
               status: res.status,
               statusText: res.statusText,
               headers,
            });
         });
      },
   });
}
