/**
 * iddaai image proxy — Cloudflare Worker in front of an R2 bucket.
 *
 * Request flow for GET /teams/<id>, /competitions/<id>, /areas/<id>:
 *   1. Cloudflare edge cache
 *   2. R2 bucket (permanent mirror)
 *   3. Upstream (file.mackolikfeeds.com) → stored in R2 on the way out
 *
 * The bucket fills itself lazily; once an image lands in R2 it is served
 * from there forever, even if the upstream removes it.
 *
 * NOTE: deployed manually via the Cloudflare dashboard on 2026-06-10
 * (worker name: iddaai-image-proxy, domain: files.iddaai.com). Keep this
 * file in sync with the dashboard copy, or deploy from here with
 * `npm run deploy`.
 */
export interface Env {
  BUCKET: R2Bucket;
}

const UPSTREAM_BASE = "https://file.mackolikfeeds.com";
const VALID_KEY = /^(teams|competitions|areas)\/[A-Za-z0-9_-]{1,64}$/;

// Browsers revalidate daily; the edge keeps hits for a week. Misses are
// cached briefly so a missing logo doesn't hammer the upstream.
const HIT_CACHE_CONTROL = "public, max-age=86400, s-maxage=604800";
const MISS_CACHE_CONTROL = "public, max-age=3600";

function imageResponse(
  body: BodyInit | null,
  contentType: string | undefined,
  etag?: string,
): Response {
  const headers = new Headers({
    "Content-Type": contentType ?? "image/png",
    "Cache-Control": HIT_CACHE_CONTROL,
    "Access-Control-Allow-Origin": "*",
  });
  if (etag) headers.set("ETag", etag);
  return new Response(body, { headers });
}

function notFound(): Response {
  return new Response("Not found", {
    status: 404,
    headers: { "Cache-Control": MISS_CACHE_CONTROL },
  });
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext,
  ): Promise<Response> {
    if (request.method !== "GET" && request.method !== "HEAD") {
      return new Response("Method not allowed", { status: 405 });
    }

    const key = new URL(request.url).pathname.slice(1);
    if (!VALID_KEY.test(key)) return notFound();

    // The Cache API only stores GET entries; use a GET key for HEAD too.
    const cache = caches.default;
    const cacheKey = new Request(new URL(request.url).toString());
    const cached = await cache.match(cacheKey);
    if (cached) {
      return request.method === "HEAD"
        ? new Response(null, cached)
        : cached;
    }

    // 2. Permanent mirror in R2
    const object = await env.BUCKET.get(key);
    if (object) {
      const response = imageResponse(
        object.body,
        object.httpMetadata?.contentType,
        object.httpEtag,
      );
      ctx.waitUntil(cache.put(cacheKey, response.clone()));
      return request.method === "HEAD"
        ? new Response(null, response)
        : response;
    }

    // 3. Upstream fetch + mirror into R2 (images are small, buffer them)
    const upstream = await fetch(`${UPSTREAM_BASE}/${key}`, {
      cf: { cacheTtl: 0 },
    });
    if (!upstream.ok) {
      const response = notFound();
      ctx.waitUntil(cache.put(cacheKey, response.clone()));
      return response;
    }

    const contentType =
      upstream.headers.get("Content-Type") ?? "image/png";
    const buffer = await upstream.arrayBuffer();

    ctx.waitUntil(
      env.BUCKET.put(key, buffer, {
        httpMetadata: { contentType },
      }),
    );

    const response = imageResponse(buffer, contentType);
    ctx.waitUntil(cache.put(cacheKey, response.clone()));
    return request.method === "HEAD"
      ? new Response(null, response)
      : response;
  },
} satisfies ExportedHandler<Env>;
