import "urlpattern-polyfill";
import { assert, unreachable } from "../../../shared/src/asserts.js";
import { getErrorMessage } from "../../../shared/src/error.js";
import { sleep } from "../../../shared/src/sleep.js";
import "../../../shared/src/valita.js";
import { PushFailed, TransformFailed } from "../../../zero-protocol/src/error-kind-enum.js";
import { ZeroCache } from "../../../zero-protocol/src/error-origin-enum.js";
import { Internal, HTTP, Parse } from "../../../zero-protocol/src/error-reason-enum.js";
import { isProtocolError } from "../../../zero-protocol/src/error.js";
import { ProtocolErrorWithLevel } from "../types/error-with-level.js";
import { upstreamSchema } from "../types/shards.js";
import { randInt } from "../../../shared/src/rand.js";
const reservedParams = ["schema", "appID"];
function compileUrlPattern(pattern) {
  try {
    return new URLPattern(pattern);
  } catch (e) {
    throw new Error(
      `Invalid URLPattern in URL configuration: "${pattern}". Error: ${e instanceof Error ? e.message : String(e)}`
    );
  }
}
function filterCustomHeaders(headers, allowedHeaders) {
  if (!headers) {
    return {};
  }
  if (!allowedHeaders || allowedHeaders.length === 0) {
    return {};
  }
  const allowed = new Set(allowedHeaders.map((h) => h.toLowerCase()));
  const filtered = {};
  for (const [key, value] of Object.entries(headers)) {
    if (allowed.has(key.toLowerCase())) {
      filtered[key] = value;
    }
  }
  return filtered;
}
const getBodyPreview = async (res, lc) => {
  try {
    const body = await res.clone().text();
    if (body.length > 512) {
      return body.slice(0, 512) + "...";
    }
    return body;
  } catch (e) {
    lc.warn?.(
      "failed to get body preview",
      {
        url: res.url
      },
      e
    );
  }
  return void 0;
};
const MAX_ATTEMPTS = 4;
async function fetchFromAPIServer(validator, source, lc, url, allowedUrlPatterns, shard, headerOptions, body) {
  const fetchFromAPIServerID = randInt(1, Number.MAX_SAFE_INTEGER).toString(36);
  lc = lc.withContext("fetchFromAPIServerID", fetchFromAPIServerID);
  lc.debug?.("fetchFromAPIServer called", {
    url
  });
  if (!urlMatch(url, allowedUrlPatterns)) {
    throw new ProtocolErrorWithLevel(
      source === "push" ? {
        kind: PushFailed,
        origin: ZeroCache,
        reason: Internal,
        message: `URL "${url}" is not allowed by the ZERO_MUTATE_URL configuration`,
        mutationIDs: []
      } : {
        kind: TransformFailed,
        origin: ZeroCache,
        reason: Internal,
        message: `URL "${url}" is not allowed by the ZERO_QUERY_URL configuration`,
        queryIDs: []
      },
      "warn"
    );
  }
  const headers = {
    "Content-Type": "application/json"
  };
  if (headerOptions.apiKey) {
    headers["X-Api-Key"] = headerOptions.apiKey;
  }
  Object.assign(
    headers,
    filterCustomHeaders(
      headerOptions.customHeaders,
      headerOptions.allowedClientHeaders
    )
  );
  if (headerOptions.token) {
    headers["Authorization"] = `Bearer ${headerOptions.token}`;
  }
  if (headerOptions.cookie) {
    headers["Cookie"] = headerOptions.cookie;
  }
  if (headerOptions.origin) {
    headers["Origin"] = headerOptions.origin;
  }
  const urlObj = new URL(url);
  const params = new URLSearchParams(urlObj.search);
  for (const reserved of reservedParams) {
    assert(
      !params.has(reserved),
      `The push URL cannot contain the reserved query param "${reserved}"`
    );
  }
  params.append("schema", upstreamSchema(shard));
  params.append("appID", shard.appID);
  urlObj.search = params.toString();
  const finalUrl = urlObj.toString();
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
    lc = lc.withContext("fetchFromAPIServerAttempt", attempt);
    lc.debug?.("fetch from API server attempt");
    const shouldRetry = async () => {
      if (attempt < MAX_ATTEMPTS) {
        const delayMs = getBackoffDelayMs(attempt);
        lc.debug?.(`fetch from API server retrying in ${delayMs} ms`);
        await sleep(delayMs);
        return true;
      }
      lc.debug?.("fetch from API server reached max attempts, not retrying");
      return false;
    };
    try {
      const response = await fetch(finalUrl, {
        method: "POST",
        headers,
        body: JSON.stringify(body)
      });
      if (!response.ok) {
        const bodyPreview = await getBodyPreview(response, lc);
        lc.warn?.("fetch from API server returned non-OK status", {
          url: finalUrl,
          status: response.status,
          bodyPreview
        });
        if ((response.status === 502 || response.status === 504) && await shouldRetry()) {
          continue;
        }
        throw new ProtocolErrorWithLevel(
          source === "push" ? {
            kind: PushFailed,
            origin: ZeroCache,
            reason: HTTP,
            status: response.status,
            bodyPreview,
            message: `Fetch from API server returned non-OK status ${response.status}`,
            mutationIDs: []
          } : {
            kind: TransformFailed,
            origin: ZeroCache,
            reason: HTTP,
            status: response.status,
            bodyPreview,
            message: `Fetch from API server returned non-OK status ${response.status}`,
            queryIDs: []
          },
          "warn"
        );
      }
      try {
        const json = await response.json();
        const result = validator.parse(json);
        lc.debug?.("fetch from API server succeeded");
        return result;
      } catch (error) {
        lc.warn?.(
          "failed to parse response",
          {
            url: finalUrl
          },
          error
        );
        throw new ProtocolErrorWithLevel(
          source === "push" ? {
            kind: PushFailed,
            origin: ZeroCache,
            reason: Parse,
            message: `Failed to parse response from API server: ${getErrorMessage(error)}`,
            mutationIDs: []
          } : {
            kind: TransformFailed,
            origin: ZeroCache,
            reason: Parse,
            message: `Failed to parse response from API server: ${getErrorMessage(error)}`,
            queryIDs: []
          },
          "warn",
          { cause: error }
        );
      }
    } catch (error) {
      if (isProtocolError(error)) {
        throw error;
      }
      const isFetchFailed = error instanceof TypeError && error.message === "fetch failed";
      let logLevel = isFetchFailed ? "warn" : "error";
      lc[logLevel]?.(
        "fetch from API server threw error",
        { url: finalUrl },
        error
      );
      if (isFetchFailed && await shouldRetry()) {
        continue;
      }
      throw new ProtocolErrorWithLevel(
        source === "push" ? {
          kind: PushFailed,
          origin: ZeroCache,
          reason: Internal,
          message: `Fetch from API server threw error: ${getErrorMessage(error)}`,
          mutationIDs: []
        } : {
          kind: TransformFailed,
          origin: ZeroCache,
          reason: Internal,
          message: `Fetch from API server threw error: ${getErrorMessage(error)}`,
          queryIDs: []
        },
        logLevel,
        { cause: error }
      );
    }
  }
  unreachable();
}
function urlMatch(url, allowedUrlPatterns) {
  for (const pattern of allowedUrlPatterns) {
    if (pattern.test(url)) {
      return true;
    }
  }
  return false;
}
function getBackoffDelayMs(attempt) {
  return Math.min(1e3, 100 * Math.pow(2, attempt - 1) + Math.random() * 100);
}
export {
  compileUrlPattern,
  fetchFromAPIServer,
  getBodyPreview,
  urlMatch
};
//# sourceMappingURL=fetch.js.map
