// nucleic-updates — a surgical redirect Worker for the Sparkle update host.
//
// `updates.nucleic.blakeslee.xyz` is served *directly* by the `nucleic-updates` R2 bucket
// (custom domain, no Worker in the path — fully CDN-cacheable; docs/CLOUD_INFRA.md §1). This
// Worker is layered on top for ONE job: resolve a channel's "latest" alias to the current
// versioned DMG, so anything elsewhere (the website download button, docs, a chat link) can point
// at a single stable URL that never needs updating:
//
//   GET https://updates.nucleic.blakeslee.xyz/<channel>/latest  →  302  →  …/<channel>/Nucleic-….dmg
//
// Worker routes take precedence over the R2 custom domain on the same hostname, so ONLY the
// path-scoped alias paths reach this Worker — the appcasts (`appcast-*.xml` at the bucket root)
// and the DMGs (`<channel>/*.dmg`) keep being served straight from R2 and stay fully cacheable.
// "Latest" is read from the channel appcast on each request (the same XML the app's updater
// consumes), and we redirect to that appcast's newest enclosure URL verbatim — which already
// carries the `<channel>/` folder — so there is no per-release config to touch: ship a release,
// the alias follows.
//
// The Worker also fronts a second family of aliases for the iOS companion (NucleicRemote), which
// ships through TestFlight rather than Sparkle/R2:
//
//   GET https://updates.nucleic.blakeslee.xyz/testflight/<channel>  →  302  →  testflight.apple.com/join/…
//
// These are fixed public invite links, not release-derived — no appcast, no R2 read — so a single
// stable URL on the updates host can front the TestFlight join page and be re-pointed here if a
// link is ever rotated.

import { latestDmgUrl } from "./appcast";

export interface Env {
  // The `nucleic-updates` R2 bucket — the same one served at updates.nucleic.blakeslee.xyz.
  // Bound directly so we read the appcast without a network hop or a dependency on the public path.
  UPDATES: R2Bucket;
}

// channel → its appcast object key (flat layout, matching scripts/upload-r2.sh).
const APPCAST: Record<string, string> = {
  canary: "appcast-canary.xml",
  beta: "appcast-beta.xml",
  rc: "appcast-rc.xml",
  stable: "appcast-stable.xml",
};

// iOS channel → its fixed TestFlight public invite link. Static (not release-derived): the target
// only changes if an invite link is deliberately regenerated, so edit it here and redeploy.
const TESTFLIGHT: Record<string, string> = {
  canary: "https://testflight.apple.com/join/nFnHdmAU",
  beta: "https://testflight.apple.com/join/AaBZpbkp",
};

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { pathname } = new URL(request.url);

    const latest = /^\/(canary|beta|rc|stable)\/latest$/.exec(pathname);
    const testflight = /^\/testflight\/(canary|beta)$/.exec(pathname);
    if (!latest && !testflight) return new Response("not found\n", { status: 404 });
    if (request.method !== "GET" && request.method !== "HEAD") {
      return new Response("method not allowed\n", { status: 405, headers: { allow: "GET, HEAD" } });
    }

    // Static TestFlight invite redirect — a fixed link, so no appcast/R2 lookup. Cached longer than
    // the "latest" aliases below (an hour vs. 5 min) since the target only moves on a manual link
    // rotation, not on every release.
    if (testflight) {
      return new Response(null, {
        status: 302,
        headers: {
          location: TESTFLIGHT[testflight[1]],
          "cache-control": "public, max-age=3600",
        },
      });
    }

    const appcast = await env.UPDATES.get(APPCAST[latest![1]]);
    if (!appcast) return new Response("no appcast for channel\n", { status: 404 });

    const target = latestDmgUrl(await appcast.text());
    if (!target) return new Response("no release in appcast\n", { status: 404 });

    // 302, never 301: the target moves every release, so the alias must not be cached as a
    // permanent redirect. The short max-age matches the appcast's own 5-min TTL (upload-r2.sh) so
    // a freshly published release surfaces here promptly without hammering R2 on every hit.
    return new Response(null, {
      status: 302,
      headers: { location: target, "cache-control": "public, max-age=300" },
    });
  },
} satisfies ExportedHandler<Env>;
