import { Effect, Option } from "effect";
import {
  Fetch,
  Request as FxRequest,
  Response as FxResponse
} from "fx-fetch";
import { Decimal } from "./decimal.js";
function normalizeBody(value) {
  if (value === null || value === void 0) return value;
  if (value instanceof Decimal) return value.toNumber();
  if (Option.isOption(value)) {
    return Option.isSome(value) ? normalizeBody(value.value) : null;
  }
  if (Array.isArray(value)) return value.map(normalizeBody);
  if (typeof value === "object") {
    const proto = Object.getPrototypeOf(value);
    if (proto !== Object.prototype && proto !== null) return value;
    const out = {};
    for (const key of Object.keys(value)) {
      out[key] = normalizeBody(value[key]);
    }
    return out;
  }
  return value;
}
function extractFilename(response, url) {
  const disposition = FxResponse.getHeader(response, "content-disposition");
  if (disposition) {
    const match = disposition[0]?.match(/filename\*?=(?:UTF-8''|")?([^";]+)"?/i);
    if (match?.[1]) return decodeURIComponent(match[1]);
  }
  const path = new URL(url).pathname;
  return path.slice(path.lastIndexOf("/") + 1) || "download";
}
export class HttpRequestBuilder {
  #url;
  #method;
  #headers = [];
  #queries = [];
  #body;
  // Process-wide defaults applied at `#buildRequest()` time. Each entry is
  // keyed by lowercased header name so an explicit `.header('X', …)` on the
  // builder wins regardless of casing. A getter returning `null` / `undefined`
  // / `''` skips the header for that request — lets the host source values
  // from a live ref (e.g. the current i18n locale) without baking in stale
  // snapshots.
  static #defaultHeaders = /* @__PURE__ */ new Map();
  static setDefaultHeader(name, value) {
    const get = typeof value === "function" ? value : () => value;
    HttpRequestBuilder.#defaultHeaders.set(name.toLowerCase(), { name, get });
  }
  static clearDefaultHeader(name) {
    HttpRequestBuilder.#defaultHeaders.delete(name.toLowerCase());
  }
  constructor(url, method) {
    this.#url = url;
    this.#method = method;
  }
  header(name, value) {
    this.#headers.push([name, value]);
    return this;
  }
  query(keyOrRecord, value) {
    if (typeof keyOrRecord === "string") {
      this.#queries.push([keyOrRecord, String(value)]);
      return this;
    }
    for (const [k, v] of Object.entries(keyOrRecord)) {
      this.#appendQueryValue(k, v);
    }
    return this;
  }
  #appendQueryValue(key, value) {
    if (value === null || value === void 0 || value === "") return;
    if (typeof value === "string") {
      this.#queries.push([key, value]);
      return;
    }
    if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
      this.#queries.push([key, String(value)]);
      return;
    }
    if (Array.isArray(value)) {
      for (const item of value) this.#appendQueryValue(key, item);
      return;
    }
    throw new Error(`query: nested object values are not supported (key: "${key}")`);
  }
  /**
   * Set the request body. A pre-built `FormData` / `Blob` / `ArrayBuffer` /
   * string is used verbatim; anything else is normalized (see
   * `normalizeBody`) and JSON-serialized. The `Content-Type` header is *not*
   * set here — the caller adds `.header('Content-Type', …)` explicitly.
   */
  body(data) {
    const value = normalizeBody(data);
    this.#body = value instanceof FormData || value instanceof Blob || value instanceof ArrayBuffer || typeof value === "string" ? value : JSON.stringify(value);
    return this;
  }
  /**
   * A structural snapshot of the request this builder will issue — an equal
   * snapshot means an equal HTTP request. Handy for change-detection, e.g.
   * deciding whether a refetch is warranted. Non-string bodies (`FormData`,
   * `Blob`, `ArrayBuffer`) collapse to a `'[binary]'` marker, so equal
   * snapshots do not guarantee equal bodies in that case.
   */
  describe() {
    return {
      url: this.#url,
      method: this.#method,
      headers: this.#headers.map(([k, v]) => [k, v]),
      queries: this.#queries.map(([k, v]) => [k, v]),
      body: typeof this.#body === "string" ? this.#body : this.#body === void 0 ? null : "[binary]"
    };
  }
  json() {
    return Fetch.fetchJson(this.#buildRequest());
  }
  file() {
    const url = this.#url;
    const request = this.#buildRequest();
    return Effect.gen(function* () {
      const response = yield* Fetch.fetch(request);
      yield* FxResponse.ensureOk(response);
      const blob = yield* FxResponse.readBlob(response);
      return new File([blob], extractFilename(response, url));
    });
  }
  #buildRequest() {
    const parts = {
      url: this.#url,
      method: this.#method
    };
    const explicit = new Set(this.#headers.map(([k]) => k.toLowerCase()));
    const headers = {};
    for (const [k, v] of this.#headers) headers[k] = v;
    if (this.#body !== void 0) {
      if (this.#body instanceof FormData) {
        const serialized = new Response(this.#body);
        const contentType = serialized.headers.get("content-type");
        if (contentType !== null && !explicit.has("content-type")) {
          headers["Content-Type"] = contentType;
          explicit.add("content-type");
        }
        parts.body = serialized.blob();
      } else {
        parts.body = this.#body;
      }
    }
    for (const [key, { name, get }] of HttpRequestBuilder.#defaultHeaders) {
      if (explicit.has(key)) continue;
      const v = get();
      if (v == null || v === "") continue;
      headers[name] = v;
    }
    if (Object.keys(headers).length > 0) {
      parts.headers = headers;
    }
    let request = FxRequest.unsafeMake(parts);
    for (const [k, v] of this.#queries) {
      request = FxRequest.appendUrlSearchParam(request, k, v);
    }
    return request;
  }
}
