/**
 * Immutable URL query parameters represented as ordered string pairs.
 *
 * This module is the shared query-parameter model for HTTP client request
 * queries, URL-encoded form bodies, and server-side decoding. A `UrlParams`
 * value can be built from records, iterables, or native `URLSearchParams`, then
 * inspected, appended, replaced, removed, serialized, converted to a `URL`, or
 * decoded with schemas.
 *
 * **Mental model**
 *
 * The core representation is a list of `[key, value]` string pairs. Duplicate
 * keys and pair order are preserved by `make`, `fromInput`, iteration, and
 * append-style operations. Record input is a convenience layer: primitive values
 * become strings, arrays become repeated parameters, nested records use bracket
 * notation, and `undefined` fields are skipped.
 *
 * **Common tasks**
 *
 * - Build query parameters from plain records, tuples, or `URLSearchParams`.
 * - Read the first, last, or all values for a key.
 * - Replace, append, transform, or remove keys without mutating the original.
 * - Serialize a query string, merge parameters into a URL, or decode records and
 *   JSON fields with schemas.
 *
 * **Gotchas**
 *
 * Use `getAll` when every duplicate value matters. `set` and `setAll` replace
 * existing values for matching keys, while `append` and `appendAll` preserve
 * them. Serialization through `toString` and `makeUrl` delegates to the platform
 * `URLSearchParams` / `URL` implementations, so pass decoded strings rather
 * than pre-encoded query fragments. Record-based and schema-based conversions
 * collapse repeated keys into string arrays and do not preserve the full global
 * pair ordering; `schemaJsonField` reads the first matching value for the
 * selected field.
 *
 * @since 4.0.0
 */
import * as Arr from "../../Array.js";
import * as Data from "../../Data.js";
import * as Effect from "../../Effect.js";
import * as Equal from "../../Equal.js";
import * as Equ from "../../Equivalence.js";
import { dual } from "../../Function.js";
import * as Hash from "../../Hash.js";
import { PipeInspectableProto } from "../../internal/core.js";
import * as Option from "../../Option.js";
import { hasProperty } from "../../Predicate.js";
import * as Result from "../../Result.js";
import * as Schema from "../../Schema.js";
import * as Issue from "../../SchemaIssue.js";
import * as Transformation from "../../SchemaTransformation.js";
import * as Tuple from "../../Tuple.js";
const TypeId = "~effect/http/UrlParams";
/**
 * Returns `true` when a value is a `UrlParams` instance.
 *
 * @category guards
 * @since 4.0.0
 */
export const isUrlParams = u => hasProperty(u, TypeId);
const Proto = {
  ...PipeInspectableProto,
  [TypeId]: TypeId,
  [Symbol.iterator]() {
    return this.params[Symbol.iterator]();
  },
  toJSON() {
    return {
      _id: "UrlParams",
      params: Object.fromEntries(this.params)
    };
  },
  [Equal.symbol](that) {
    return Equivalence(this, that);
  },
  [Hash.symbol]() {
    return Hash.array(this.params.flat());
  }
};
/**
 * Creates `UrlParams` from ordered string key-value pairs.
 *
 * **Details**
 *
 * The input pairs are used as-is and are not coerced or normalized.
 *
 * @category constructors
 * @since 4.0.0
 */
export const make = params => {
  const self = Object.create(Proto);
  self.params = params;
  return self;
};
/**
 * Creates `UrlParams` from a supported input shape.
 *
 * **Details**
 *
 * Primitive values are converted to strings, arrays produce repeated parameters,
 * nested records use bracket notation, and `undefined` values are omitted.
 *
 * @category constructors
 * @since 4.0.0
 */
export const fromInput = input => {
  const parsed = fromInputNested(input);
  const out = [];
  for (let i = 0; i < parsed.length; i++) {
    if (Array.isArray(parsed[i][0])) {
      const [keys, value] = parsed[i];
      out.push([`${keys[0]}[${keys.slice(1).join("][")}]`, value]);
    } else {
      out.push(parsed[i]);
    }
  }
  return make(out);
};
const fromInputNested = input => {
  const entries = typeof input[Symbol.iterator] === "function" ? Arr.fromIterable(input) : Object.entries(input);
  const out = [];
  for (const [key, value] of entries) {
    if (Array.isArray(value)) {
      for (let i = 0; i < value.length; i++) {
        if (value[i] !== undefined) {
          out.push([key, String(value[i])]);
        }
      }
    } else if (typeof value === "object") {
      const nested = fromInputNested(value);
      for (const [k, v] of nested) {
        out.push([[key, ...(typeof k === "string" ? [k] : k)], v]);
      }
    } else if (value !== undefined) {
      out.push([key, String(value)]);
    }
  }
  return out;
};
/**
 * Provides an order-sensitive `Equivalence` instance for `UrlParams`.
 *
 * **Details**
 *
 * Two values are equivalent when they contain the same key-value pairs in the same
 * order.
 *
 * @category instances
 * @since 4.0.0
 */
export const Equivalence = /*#__PURE__*/Equ.make((a, b) => arrayEquivalence(a.params, b.params));
const arrayEquivalence = /*#__PURE__*/Arr.makeEquivalence(/*#__PURE__*/Tuple.makeEquivalence([/*#__PURE__*/Equ.strictEqual(), /*#__PURE__*/Equ.strictEqual()]));
/**
 * Schema for `UrlParams`.
 *
 * **Details**
 *
 * The encoded representation is an array of string key-value tuples.
 *
 * @category schemas
 * @since 4.0.0
 */
export const UrlParamsSchema = /*#__PURE__*/Schema.declare(isUrlParams, {
  typeConstructor: {
    _tag: "effect/http/UrlParams"
  },
  generation: {
    runtime: `UrlParams.UrlParamsSchema`,
    Type: `UrlParams.UrlParams`,
    Encoded: `typeof UrlParams.UrlParamsSchema["Encoded"]`,
    importDeclaration: `import * as UrlParams from "effect/unstable/http/UrlParams"`
  },
  expected: "UrlParams",
  toEquivalence: () => Equivalence,
  toCodec: () => Schema.link()(Schema.Array(Schema.Tuple([Schema.String, Schema.String])), Transformation.transform({
    decode: make,
    encode: self => self.params
  }))
});
/**
 * An empty `UrlParams` value.
 *
 * @category constructors
 * @since 4.0.0
 */
export const empty = /*#__PURE__*/make([]);
/**
 * Returns all values for a query parameter key in insertion order.
 *
 * **Details**
 *
 * Returns an empty array when the key is absent.
 *
 * @category combinators
 * @since 4.0.0
 */
export const getAll = /*#__PURE__*/dual(2, (self, key) => Arr.reduce(self.params, [], (acc, [k, value]) => {
  if (k === key) {
    acc.push(value);
  }
  return acc;
}));
/**
 * Returns the first value for a query parameter key safely.
 *
 * **Details**
 *
 * Returns `Option.none` when the key is absent.
 *
 * @category combinators
 * @since 4.0.0
 */
export const getFirst = /*#__PURE__*/dual(2, (self, key) => Arr.findFirst(self.params, ([k]) => k === key).pipe(Option.map(([, value]) => value)));
/**
 * Returns the last value for a query parameter key safely.
 *
 * **Details**
 *
 * Returns `Option.none` when the key is absent.
 *
 * @category combinators
 * @since 4.0.0
 */
export const getLast = /*#__PURE__*/dual(2, (self, key) => Arr.findLast(self.params, ([k]) => k === key).pipe(Option.map(([, value]) => value)));
/**
 * Sets a query parameter to a single value.
 *
 * **Details**
 *
 * Existing values for the same key are removed, and the new value is appended to
 * the end.
 *
 * @category combinators
 * @since 4.0.0
 */
export const set = /*#__PURE__*/dual(3, (self, key, value) => make(Arr.append(Arr.filter(self.params, ([k]) => k !== key), [key, String(value)])));
/**
 * Transforms the underlying ordered key-value pairs of `UrlParams`.
 *
 * **Details**
 *
 * The result is wrapped in a new `UrlParams` value.
 *
 * @category combinators
 * @since 4.0.0
 */
export const transform = /*#__PURE__*/dual(2, (self, f) => make(f(self.params)));
/**
 * Sets multiple query parameters from input.
 *
 * **Details**
 *
 * Keys present in the input replace existing values for those keys, while
 * unmentioned existing parameters are preserved.
 *
 * @category combinators
 * @since 4.0.0
 */
export const setAll = /*#__PURE__*/dual(2, (self, input) => {
  const out = fromInput(input);
  const params = out.params;
  const keys = new Set();
  for (let i = 0; i < params.length; i++) {
    keys.add(params[i][0]);
  }
  for (let i = 0; i < self.params.length; i++) {
    if (keys.has(self.params[i][0])) continue;
    params.push(self.params[i]);
  }
  return out;
});
/**
 * Appends a query parameter value without removing existing values for the key.
 *
 * @category combinators
 * @since 4.0.0
 */
export const append = /*#__PURE__*/dual(3, (self, key, value) => make(Arr.append(self.params, [key, String(value)])));
/**
 * Appends all query parameters produced from the supplied input.
 *
 * **Details**
 *
 * Existing parameters are preserved.
 *
 * @category combinators
 * @since 4.0.0
 */
export const appendAll = /*#__PURE__*/dual(2, (self, input) => transform(self, Arr.appendAll(fromInput(input).params)));
/**
 * Removes all query parameter values for the specified key.
 *
 * @category combinators
 * @since 4.0.0
 */
export const remove = /*#__PURE__*/dual(2, (self, key) => transform(self, Arr.filter(([k]) => k !== key)));
/**
 * Error returned when constructing a `URL` from `UrlParams` fails.
 *
 * @category errors
 * @since 4.0.0
 */
export class UrlParamsError extends /*#__PURE__*/Data.TaggedError("UrlParamsError") {}
/**
 * Creates a `URL` safely by appending `UrlParams` and an optional hash to a URL string.
 *
 * **Details**
 *
 * Returns a `Result` that fails with `UrlParamsError` if the URL cannot be
 * constructed.
 *
 * @category converting
 * @since 4.0.0
 */
export const makeUrl = (url, params, hash) => {
  try {
    const urlInstance = new URL(url, baseUrl());
    for (let i = 0; i < params.params.length; i++) {
      const [key, value] = params.params[i];
      if (value !== undefined) {
        urlInstance.searchParams.append(key, value);
      }
    }
    if (hash !== undefined) {
      urlInstance.hash = hash;
    }
    return Result.succeed(urlInstance);
  } catch (e) {
    return Result.fail(new UrlParamsError({
      cause: e
    }));
  }
};
/**
 * Serializes `UrlParams` to a URL query string without a leading question mark.
 *
 * @category converting
 * @since 4.0.0
 */
export const toString = self => new URLSearchParams(self.params).toString();
const baseUrl = () => {
  if ("location" in globalThis && globalThis.location !== undefined && globalThis.location.origin !== undefined && globalThis.location.pathname !== undefined) {
    return location.origin + location.pathname;
  }
  return undefined;
};
/**
 * Builds a `Record` containing all the key-value pairs in the given `UrlParams`
 * as `string` (if only one value for a key) or a `NonEmptyArray<string>`
 * (when more than one value for a key)
 *
 * **Example** (Converting parameters to a record)
 *
 * ```ts
 * import { UrlParams } from "effect/unstable/http"
 * import * as assert from "node:assert"
 *
 * const urlParams = UrlParams.fromInput({
 *   a: 1,
 *   b: true,
 *   c: "string",
 *   e: [1, 2, 3]
 * })
 * const result = UrlParams.toRecord(urlParams)
 *
 * assert.deepStrictEqual(
 *   result,
 *   { "a": "1", "b": "true", "c": "string", "e": ["1", "2", "3"] }
 * )
 * ```
 *
 * @category converting
 * @since 4.0.0
 */
export const toRecord = self => {
  const out = {};
  for (const [k, value] of self.params) {
    const curr = out[k];
    if (curr === undefined) {
      out[k] = value;
    } else if (typeof curr === "string") {
      out[k] = [curr, value];
    } else {
      curr.push(value);
    }
  }
  return out;
};
/**
 * Builds a readonly record from `UrlParams`.
 *
 * **Details**
 *
 * Keys with one value map to a string, and keys with multiple values map to a
 * non-empty readonly array of strings.
 *
 * @category converting
 * @since 4.0.0
 */
export const toReadonlyRecord = toRecord;
/**
 * Extracts a JSON value from the first occurrence of the given `field` in the
 * `UrlParams`.
 *
 * **Example** (Decoding JSON parameter fields)
 *
 * ```ts
 * import { Schema } from "effect"
 * import { UrlParams } from "effect/unstable/http"
 *
 * const extractFoo = UrlParams.schemaJsonField("foo").pipe(
 *   Schema.decodeTo(Schema.Struct({
 *     some: Schema.String,
 *     number: Schema.Number
 *   }))
 * )
 *
 * console.log(
 *   Schema.decodeSync(extractFoo)(UrlParams.fromInput({
 *     foo: JSON.stringify({ some: "bar", number: 42 }),
 *     baz: "qux"
 *   }))
 * )
 * ```
 *
 * @category schemas
 * @since 4.0.0
 */
export const schemaJsonField = field => UrlParamsSchema.pipe(Schema.decodeTo(Schema.UnknownFromJsonString, Transformation.transformOrFail({
  decode: params => Option.match(getFirst(params, field), {
    onNone: () => Effect.fail(new Issue.Pointer([field], new Issue.MissingKey(undefined))),
    onSome: Effect.succeed
  }),
  encode: value => Effect.succeed(make([[field, value]]))
})));
/**
 * Schema that decodes `UrlParams` into a record of key-value pairs.
 *
 * **Details**
 *
 * Keys with one value decode to a string, and keys with multiple values decode to
 * a non-empty readonly array of strings.
 *
 * **Example** (Decoding URL parameters to a record)
 *
 * ```ts
 * import { Schema } from "effect"
 * import { UrlParams } from "effect/unstable/http"
 *
 * const toStruct = UrlParams.schemaRecord.pipe(
 *   Schema.decodeTo(Schema.Struct({
 *     some: Schema.String,
 *     number: Schema.FiniteFromString
 *   }))
 * )
 *
 * console.log(
 *   Schema.decodeSync(toStruct)(UrlParams.fromInput({
 *     some: "value",
 *     number: 42
 *   }))
 * )
 * ```
 *
 * @category schemas
 * @since 4.0.0
 */
export const schemaRecord = /*#__PURE__*/UrlParamsSchema.pipe(/*#__PURE__*/Schema.decodeTo(/*#__PURE__*/Schema.Record(Schema.String, /*#__PURE__*/Schema.Union([Schema.String, /*#__PURE__*/Schema.NonEmptyArray(Schema.String)])), /*#__PURE__*/Transformation.transform({
  decode: toReadonlyRecord,
  encode: fromInput
})));
//# sourceMappingURL=UrlParams.js.map