// We place this file in the /api/common folder so that it can be used by
// the /api/openai and /api/suno files, as well as our dev server.
//
// Vercel won't deploy files in sub-folders of /api as functions, so our
// common code for deployment can live here.

const { SUNO_BASE_URL, OPENAI_BASE_URL, SUNO_API_KEY, OPENAI_API_KEY } =
  envVariables();

export async function handler(req: Request) {
  try {
    if (req.method === "OPTIONS") {
      // 1️⃣  CORS pre-flight: reply locally
      return new Response(null, {
        status: 204,
        headers: corsHeaders(req.headers),
      });
    }

    // By the time this code runs, Vercel's runtime has already read from
    // `req.body` once (to calculate/verify Content-Length, etc.).
    // That leaves the original ReadableStream **locked** – any second
    // consumer will always get zero bytes.

    // Buffering the payload exactly once with `await req.text()` gives us
    // a reusable string. We can forward it to the upstream API, log it,
    // retry it, or hash it without ever touching the locked stream again.

    const rawBuffer =
      req.method === "GET" || req.method === "HEAD"
        ? undefined
        : await readRawBody(req);
    const rawBody = rawBuffer && rawBuffer.toString("utf8");

    const relay = formatRelay(req);
    if (!relay) return new Response(null, { status: 404 });
    const [path, headers] = relay;

    // We have to provide an accurate Content-Length header when we
    // forward the request. OpenAI rejects requests whose TCP body length
    // doesn't match the declared length (they read the body once too).
    if (rawBody !== undefined) {
      headers.set(
        "Content-Length",
        Buffer.byteLength(rawBody, "utf8").toString()
      );
    }

    const upstream = await fetch(path, {
      method: req.method,
      headers,
      body: rawBody,
    });

    return new Response(upstream.body, {
      status: upstream.status,
      statusText: upstream.statusText,
      headers: corsHeaders(upstream.headers),
    });
  } catch (error) {
    console.error(
      "Error making request:",
      error,
      JSON.stringify(
        {
          url: req.url,
          method: req.method,
          body: req.body,
          headers: req.headers,
        },
        null,
        "  "
      )
    );
    return new Response(
      JSON.stringify({
        error: "Failed to connect to API",
        stack:
          typeof error === "object" && error !== null
            ? (error as Error).stack
            : undefined,
        method: req.method,
        url: req.url,
        message: error instanceof Error ? error.message : "Unknown error",
      }),
      {
        status: 500,
        headers: corsHeaders({ "Content-Type": "application/json" }),
      }
    );
  }
}

// All we need to do is format the request headers with the correct API keys.
function formatRelay(req: Request): [string, Headers] | null {
  // We don't actually care about the 'localhost' base here, we're just
  // using URL() for parsing the pathname and search params.
  // It will throw an error if not given an absolute URL, which happens
  // when deployed to Vercel.
  const url = new URL(req.url, "http://localhost");
  // This is a helper param that Vercel adds to the URL during deployment.
  // We need to remove it so that it doesn't interfere with our relayed requests.
  // The string used here must match the place holder in vercel.json.
  url.searchParams.delete("match");
  const { pathname, search } = url;

  const suno = "/suno/";
  const openai = "/openai/";

  // Formatting completely new request headers below. Passing original headers
  // (with cookies etc.) causes connection issues with the upstream API.

  // We also need to add the Accept-Encoding: identity header, because Bun
  // is de-compressing the response body automatically. If the browser sees
  // a Content-Encoding, it will try to decompress it, which will fail.
  // Accept-Encoding: identity is a way to tell the browser to not decompress
  // the response body.

  if (pathname.startsWith(suno)) {
    const headers = new Headers();
    headers.set("Authorization", `Bearer ${SUNO_API_KEY}`);
    headers.set("Content-Type", "application/json");
    headers.set("Accept-Encoding", "identity");
    return [
      SUNO_BASE_URL +
        (pathname.slice(suno.length).startsWith("/") ? "" : "/") +
        pathname.slice(suno.length) +
        search,
      headers,
    ];
  } else if (pathname.startsWith(openai)) {
    const headers = new Headers();
    headers.set("Authorization", `Bearer ${OPENAI_API_KEY}`);
    headers.set("Content-Type", "application/json");
    headers.set("Accept-Encoding", "identity");
    return [
      OPENAI_BASE_URL +
        (pathname.slice(openai.length).startsWith("/") ? "" : "/") +
        pathname.slice(openai.length) +
        search,
      headers,
    ];
  }

  return null;
}

async function readRawBody(req: Request): Promise<Buffer> {
  // If body is already parsed JSON (Vercel environment)
  if (typeof req.body === "object" && req.body !== null) {
    return Buffer.from(JSON.stringify(req.body));
  }

  // If body is a stream
  const body = req.body as unknown;
  if (body && typeof body === "object" && "getReader" in body) {
    const chunks: Buffer[] = [];
    const reader = (body as ReadableStream).getReader();
    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        chunks.push(Buffer.from(value));
      }
    } finally {
      reader.releaseLock();
    }
    return Buffer.concat(chunks);
  }

  // Fallback for empty or unknown body types
  return Buffer.from([]);
}

// We need to check that the environment variables are set.
function envVariables(): typeof Bun.env {
  const { SUNO_BASE_URL, OPENAI_BASE_URL, SUNO_API_KEY, OPENAI_API_KEY } =
    process.env;

  if (!SUNO_BASE_URL || !OPENAI_BASE_URL || !SUNO_API_KEY || !OPENAI_API_KEY) {
    const missing = [];
    if (!SUNO_BASE_URL) missing.push("SUNO_BASE_URL");
    if (!OPENAI_BASE_URL) missing.push("OPENAI_BASE_URL");
    if (!SUNO_API_KEY) missing.push("SUNO_API_KEY");
    if (!OPENAI_API_KEY) missing.push("OPENAI_API_KEY");
    throw new Error(`Missing environment variables: ${missing.join(", ")}`);
  }

  return {
    SUNO_BASE_URL,
    OPENAI_BASE_URL,
    SUNO_API_KEY,
    OPENAI_API_KEY,
    ...process.env,
  };
}

// --- helper ---
function corsHeaders(src: HeadersInit = {}): Headers {
  const h = new Headers(src);
  h.set("Access-Control-Allow-Origin", "*");
  h.set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH,OPTIONS");
  h.set("Access-Control-Allow-Headers", "Content-Type,Authorization");
  h.set("Access-Control-Max-Age", "86400");
  return h;
}
