/**
 * Parses and persists HTTP `multipart/form-data` request bodies.
 *
 * `Multipart` turns incoming byte streams into typed {@link Part} values. Text
 * parts become decoded {@link Field} values, while upload parts remain streamed
 * {@link File} values until they are collected or written to scoped temporary
 * files. The persisted representation can then be decoded with schemas for
 * request handlers that receive fields and uploaded files together.
 *
 * **Mental model**
 *
 * Multipart parsing is incremental. {@link makeChannel} consumes request body
 * chunks and emits fields or files as soon as the parser reaches each part. A
 * `File` owns a one-shot byte stream for that upload; {@link toPersisted}
 * drains those file streams into scoped paths and collects text fields into a
 * {@link Persisted} record.
 *
 * **Common tasks**
 *
 * - Parse a request body stream into {@link Part} values with {@link makeChannel}.
 * - Persist parsed parts with {@link toPersisted} before schema decoding.
 * - Decode persisted forms with {@link schemaPersisted}, {@link schemaJson},
 *   {@link PersistedFileSchema}, or {@link SingleFileSchema}.
 * - Configure parser limits with {@link limitsServices} or the `Max*` context
 *   references.
 *
 * **Gotchas**
 *
 * Multipart request bodies are usually one-shot streams. Read each file stream
 * once, and use `contentEffect` only when the file is small enough to hold in
 * memory. Paths produced by {@link toPersisted} are scoped resources and stop
 * being valid when the scope closes. Client-provided file names are metadata,
 * not trusted filesystem paths.
 *
 * **See also**
 *
 * {@link Part}, {@link Field}, {@link File}, {@link Persisted},
 * {@link makeChannel}, {@link toPersisted}.
 *
 * @since 4.0.0
 */
import * as Arr from "../../Array.js";
import * as Cause from "../../Cause.js";
import * as Channel from "../../Channel.js";
import * as Context from "../../Context.js";
import * as Data from "../../Data.js";
import * as Effect from "../../Effect.js";
import * as Exit from "../../Exit.js";
import * as FileSystem from "../../FileSystem.js";
import { constant, dual } from "../../Function.js";
import * as Inspectable from "../../Inspectable.js";
import * as Option from "../../Option.js";
import * as Path from "../../Path.js";
import * as Predicate from "../../Predicate.js";
import * as Pull from "../../Pull.js";
import * as Schema from "../../Schema.js";
import * as Transformation from "../../SchemaTransformation.js";
import * as Stream from "../../Stream.js";
import * as UndefinedOr from "../../UndefinedOr.js";
import * as IncomingMessage from "./HttpIncomingMessage.js";
import * as MP from "./Multipasta.js";
/**
 * Type identifier used to brand multipart part values.
 *
 * @category type IDs
 * @since 4.0.0
 */
export const TypeId = "~effect/http/Multipart";
/**
 * Returns `true` when a value is a multipart `Part`.
 *
 * @category guards
 * @since 4.0.0
 */
export const isPart = u => Predicate.hasProperty(u, TypeId);
/**
 * Returns `true` when a value is a multipart text `Field`.
 *
 * @category guards
 * @since 4.0.0
 */
export const isField = u => isPart(u) && u._tag === "Field";
/**
 * Returns `true` when a value is a multipart `File`.
 *
 * @category guards
 * @since 4.0.0
 */
export const isFile = u => isPart(u) && u._tag === "File";
/**
 * Returns `true` when a value is a persisted multipart file.
 *
 * @category guards
 * @since 4.0.0
 */
export const isPersistedFile = u => Predicate.hasProperty(u, TypeId) && Predicate.isTagged(u, "PersistedFile");
const MultipartErrorTypeId = "~effect/http/Multipart/MultipartError";
/**
 * Error reason carried by a `MultipartError`.
 *
 * **Details**
 *
 * It identifies parser and limit failures such as oversized files or fields, too
 * many parts, total body size limits, parse errors, and internal errors.
 *
 * @category errors
 * @since 4.0.0
 */
export class MultipartErrorReason extends Data.Error {}
/**
 * Error raised while parsing, streaming, or persisting multipart form data.
 *
 * **Details**
 *
 * The `reason` field contains the concrete `MultipartErrorReason`.
 *
 * @category errors
 * @since 4.0.0
 */
export class MultipartError extends /*#__PURE__*/Data.TaggedError("MultipartError") {
  /**
   * Creates a multipart error from a reason tag and optional cause.
   *
   * @since 4.0.0
   */
  static fromReason(reason, cause) {
    return new MultipartError({
      reason: new MultipartErrorReason({
        _tag: reason,
        cause
      })
    });
  }
  /**
   * Marks this value as a multipart error for runtime guards.
   *
   * @since 4.0.0
   */
  [MultipartErrorTypeId] = MultipartErrorTypeId;
  /**
   * Uses the concrete multipart error reason as the public message.
   *
   * @since 4.0.0
   */
  get message() {
    return this.reason._tag;
  }
}
/**
 * Schema for persisted multipart files.
 *
 * **Details**
 *
 * The encoded form contains the field key, original file name, content type, and
 * filesystem path.
 *
 * @category schemas
 * @since 4.0.0
 */
export const PersistedFileSchema = /*#__PURE__*/Schema.declare(isPersistedFile, {
  typeConstructor: {
    _tag: "effect/http/PersistedFile"
  },
  generation: {
    runtime: `Multipart.PersistedFileSchema`,
    Type: `Multipart.PersistedFile`,
    importDeclaration: `import * as Multipart from "effect/unstable/http/Multipart"`
  },
  expected: "PersistedFile",
  toCodecJson: () => Schema.link()(Schema.Struct({
    key: Schema.String,
    name: Schema.String,
    contentType: Schema.String.annotate({
      contentEncoding: "binary"
    }),
    path: Schema.String
  }), Transformation.transform({
    decode: ({
      contentType,
      key,
      name,
      path
    }) => new PersistedFileImpl(key, name, contentType, path),
    encode: file => ({
      key: file.key,
      name: file.name,
      contentType: file.contentType,
      path: file.path
    })
  }))
});
/**
 * Schema for an array of persisted multipart files.
 *
 * @category schemas
 * @since 4.0.0
 */
export const FilesSchema = /*#__PURE__*/Schema.Array(PersistedFileSchema);
/**
 * Schema for exactly one persisted multipart file.
 *
 * **Details**
 *
 * The encoded form is a one-element file array, while the decoded value is the
 * single `PersistedFile`.
 *
 * @category schemas
 * @since 4.0.0
 */
export const SingleFileSchema = /*#__PURE__*/FilesSchema.check(Schema.isLengthBetween(1, 1)).pipe(/*#__PURE__*/Schema.decodeTo(PersistedFileSchema, /*#__PURE__*/Transformation.transform({
  decode: ([file]) => file,
  encode: file => [file]
})));
/**
 * Creates a decoder for persisted multipart data using the supplied schema.
 *
 * **Details**
 *
 * The returned function decodes an unknown input into the schema output and fails
 * with `SchemaError` when validation fails.
 *
 * @category schemas
 * @since 4.0.0
 */
export const schemaPersisted = schema => Schema.decodeUnknownEffect(schema);
/**
 * Creates a decoder for a JSON-encoded field in persisted multipart data.
 *
 * **Details**
 *
 * The selected field is parsed from a JSON string and decoded with the supplied
 * schema.
 *
 * @category schemas
 * @since 4.0.0
 */
export const schemaJson = (schema, options) => {
  const fromJson = Schema.fromJsonString(schema);
  return dual(2, (persisted, field) => Effect.map(Schema.decodeUnknownEffect(Schema.Struct({
    [field]: fromJson
  }))(persisted, options), _ => _[field]));
};
/**
 * Builds the low-level multipart parser configuration from request headers and
 * the current fiber context.
 *
 * **Details**
 *
 * Parser limits are read from the multipart references, including maximum parts,
 * field size, file size, total body size, and field MIME type overrides.
 *
 * @category configuration
 * @since 4.0.0
 */
export const makeConfig = headers => Effect.withFiber(fiber => {
  const mimeTypes = Context.get(fiber.context, FieldMimeTypes);
  return Effect.succeed({
    headers,
    maxParts: fiber.getRef(MaxParts),
    maxFieldSize: Number(fiber.getRef(MaxFieldSize)),
    maxPartSize: UndefinedOr.map(fiber.getRef(MaxFileSize), Number),
    maxTotalSize: UndefinedOr.map(fiber.getRef(IncomingMessage.MaxBodySize), Number),
    isFile: mimeTypes.length === 0 ? undefined : info => !mimeTypes.some(_ => info.contentType.includes(_)) && MP.defaultIsFile(info)
  });
});
/**
 * Creates a channel that parses multipart byte chunks into multipart parts.
 *
 * **Details**
 *
 * The channel consumes non-empty batches of `Uint8Array` chunks and emits
 * non-empty batches of parsed `Part` values, failing with `MultipartError` for
 * parser and limit failures.
 *
 * @category Parsers
 * @since 4.0.0
 */
export const makeChannel = headers => Channel.fromTransform(upstream => Effect.map(makeConfig(headers), config => {
  let partsBuffer = [];
  let exit = Option.none();
  const parser = MP.make({
    ...config,
    onField(info, value) {
      partsBuffer.push(new FieldImpl(info.name, info.contentType, MP.decodeField(info, value)));
    },
    onFile(info) {
      let chunks = [];
      let finished = false;
      const pullChunks = Channel.fromPull(Effect.succeed(Effect.suspend(function loop() {
        if (!Arr.isReadonlyArrayNonEmpty(chunks)) {
          return finished ? Cause.done() : Effect.flatMap(pump, loop);
        }
        const chunk = chunks;
        chunks = [];
        return Effect.succeed(chunk);
      })));
      partsBuffer.push(new FileImpl(info, pullChunks));
      return function (chunk) {
        if (chunk === null) {
          finished = true;
        } else {
          chunks.push(chunk);
        }
      };
    },
    onError(error_) {
      exit = Option.some(Exit.fail(convertError(error_)));
    },
    onDone() {
      exit = Option.some(Exit.fail(Cause.Done()));
    }
  });
  const pump = upstream.pipe(Effect.flatMap(chunk => {
    for (let i = 0; i < chunk.length; i++) {
      parser.write(chunk[i]);
    }
    return Effect.void;
  }), Effect.catchCause(cause => {
    if (Pull.isDoneCause(cause)) {
      parser.end();
    } else {
      exit = Option.some(Exit.failCause(cause));
    }
    return Effect.void;
  }));
  return pump.pipe(Effect.flatMap(function loop() {
    if (!Arr.isReadonlyArrayNonEmpty(partsBuffer)) {
      if (Option.isSome(exit)) {
        return exit.value;
      }
      return Effect.flatMap(pump, loop);
    }
    const parts = partsBuffer;
    partsBuffer = [];
    return Effect.succeed(parts);
  }));
}));
function convertError(cause) {
  switch (cause._tag) {
    case "ReachedLimit":
      {
        switch (cause.limit) {
          case "MaxParts":
            {
              return MultipartError.fromReason("TooManyParts", cause);
            }
          case "MaxFieldSize":
            {
              return MultipartError.fromReason("FieldTooLarge", cause);
            }
          case "MaxPartSize":
            {
              return MultipartError.fromReason("FileTooLarge", cause);
            }
          case "MaxTotalSize":
            {
              return MultipartError.fromReason("BodyTooLarge", cause);
            }
        }
      }
    default:
      {
        return MultipartError.fromReason("Parse", cause);
      }
  }
}
class PartBase extends Inspectable.Class {
  [TypeId];
  constructor() {
    super();
    this[TypeId] = TypeId;
  }
}
class FieldImpl extends PartBase {
  _tag = "Field";
  key;
  contentType;
  value;
  constructor(key, contentType, value) {
    super();
    this.key = key;
    this.contentType = contentType;
    this.value = value;
  }
  toJSON() {
    return {
      _id: "@effect/platform/Multipart/Part",
      _tag: "Field",
      key: this.key,
      contentType: this.contentType,
      value: this.value
    };
  }
}
class FileImpl extends PartBase {
  _tag = "File";
  key;
  name;
  contentType;
  content;
  contentEffect;
  constructor(info, channel) {
    super();
    this.key = info.name;
    this.name = info.filename ?? info.name;
    this.contentType = info.contentType;
    this.content = Stream.fromChannel(channel);
    this.contentEffect = channel.pipe(collectUint8Array, Effect.mapError(cause => MultipartError.fromReason("InternalError", cause)));
  }
  toJSON() {
    return {
      _id: "@effect/platform/Multipart/Part",
      _tag: "File",
      key: this.key,
      name: this.name,
      contentType: this.contentType
    };
  }
}
const defaultWriteFile = (path, file) => Effect.flatMap(FileSystem.FileSystem, fs => Effect.mapError(Stream.run(file.content, fs.sink(path)), cause => MultipartError.fromReason("InternalError", cause)));
/**
 * Runs a channel of byte chunks and collects all output into a single
 * `Uint8Array`.
 *
 * **Gotchas**
 *
 * This materializes the full content in memory.
 *
 * @category converting
 * @since 4.0.0
 */
export const collectUint8Array = self => Channel.runFold(self, constant(new Uint8Array(0)), (accumulator, chunk) => {
  const totalLength = chunk.reduce((sum, element) => sum + element.length, accumulator.length);
  const newAccumulator = new Uint8Array(totalLength);
  newAccumulator.set(accumulator, 0);
  let offset = accumulator.length;
  for (const element of chunk) {
    newAccumulator.set(element, offset);
    offset += element.length;
  }
  return newAccumulator;
});
/**
 * Persists a stream of multipart parts into a record.
 *
 * **Details**
 *
 * Text fields are collected as strings, and file parts are written to files in a
 * scoped temporary directory.
 *
 * **Gotchas**
 *
 * Persisted file paths remain valid for the lifetime of the scope.
 *
 * @category converting
 * @since 4.0.0
 */
export const toPersisted = (stream, writeFile = defaultWriteFile) => Effect.gen(function* () {
  const fs = yield* FileSystem.FileSystem;
  const path_ = yield* Path.Path;
  const dir = yield* fs.makeTempDirectoryScoped();
  const persisted = Object.create(null);
  yield* Stream.runForEach(stream, part => {
    if (part._tag === "Field") {
      if (!(part.key in persisted)) {
        persisted[part.key] = part.value;
      } else if (typeof persisted[part.key] === "string") {
        persisted[part.key] = [persisted[part.key], part.value];
      } else {
        ;
        persisted[part.key].push(part.value);
      }
      return Effect.void;
    } else if (part.name === "") {
      return Effect.void;
    }
    const file = part;
    const path = path_.join(dir, path_.basename(file.name).slice(-128));
    const filePart = new PersistedFileImpl(file.key, file.name, file.contentType, path);
    if (Array.isArray(persisted[part.key])) {
      ;
      persisted[part.key].push(filePart);
    } else {
      persisted[part.key] = [filePart];
    }
    return writeFile(path, file);
  });
  return persisted;
}).pipe(Effect.catchTag("PlatformError", cause => Effect.fail(MultipartError.fromReason("InternalError", cause))));
class PersistedFileImpl extends PartBase {
  _tag = "PersistedFile";
  key;
  name;
  contentType;
  path;
  constructor(key, name, contentType, path) {
    super();
    this.key = key;
    this.name = name;
    this.contentType = contentType;
    this.path = path;
  }
  toJSON() {
    return {
      _id: "@effect/platform/Multipart/Part",
      _tag: "PersistedFile",
      key: this.key,
      name: this.name,
      contentType: this.contentType,
      path: this.path
    };
  }
}
/**
 * Creates a context containing multipart parser limit settings.
 *
 * **Details**
 *
 * The context can provide maximum part count, field size, file size, total body
 * size, and MIME types that should be parsed as fields.
 *
 * @category references
 * @since 4.0.0
 */
export const limitsServices = options => {
  const map = new Map();
  if (options.maxParts !== undefined) {
    map.set(MaxParts.key, options.maxParts);
  }
  if (options.maxFieldSize !== undefined) {
    map.set(MaxFieldSize.key, FileSystem.Size(options.maxFieldSize));
  }
  if (options.maxFileSize !== undefined) {
    map.set(MaxFileSize.key, UndefinedOr.map(options.maxFileSize, FileSystem.Size));
  }
  if (options.maxTotalSize !== undefined) {
    map.set(IncomingMessage.MaxBodySize.key, UndefinedOr.map(options.maxTotalSize, FileSystem.Size));
  }
  if (options.fieldMimeTypes !== undefined) {
    map.set(FieldMimeTypes.key, options.fieldMimeTypes);
  }
  return Context.makeUnsafe(map);
};
/**
 * Context reference for the maximum number of multipart parts allowed.
 *
 * **Details**
 *
 * The default is `undefined`, meaning no explicit part-count limit.
 *
 * @category references
 * @since 4.0.0
 */
export const MaxParts = /*#__PURE__*/Context.Reference("effect/http/Multipart/MaxParts", {
  defaultValue: () => undefined
});
/**
 * Context reference for the maximum size of a multipart field value.
 *
 * **Details**
 *
 * The default limit is 10 MiB.
 *
 * @category references
 * @since 4.0.0
 */
export const MaxFieldSize = /*#__PURE__*/Context.Reference("effect/http/Multipart/MaxFieldSize", {
  defaultValue: /*#__PURE__*/constant(/*#__PURE__*/FileSystem.Size(10 * 1024 * 1024))
});
/**
 * Context reference for the maximum size of a multipart file part.
 *
 * **Details**
 *
 * The default is `undefined`, meaning no explicit per-file limit.
 *
 * @category references
 * @since 4.0.0
 */
export const MaxFileSize = /*#__PURE__*/Context.Reference("effect/http/Multipart/MaxFileSize", {
  defaultValue: () => undefined
});
/**
 * Context reference for MIME type fragments that should be parsed as multipart
 * fields instead of files.
 *
 * **Details**
 *
 * The default treats `application/json` parts as fields.
 *
 * @category references
 * @since 4.0.0
 */
export const FieldMimeTypes = /*#__PURE__*/Context.Reference("effect/http/Multipart/FieldMimeTypes", {
  defaultValue: /*#__PURE__*/constant(["application/json"])
});
//# sourceMappingURL=Multipart.js.map