/**
 * Defines helpers for small immutable data models.
 *
 * This module helps create plain classes, tagged classes, tagged unions, and
 * typed errors with readonly fields. Tagged values carry a `_tag` field, which
 * makes them easy to narrow with pattern matching or simple checks. These
 * helpers are commonly used for domain values and errors in Effect programs.
 *
 * @since 2.0.0
 */
import type * as Cause from "./Cause.ts"
import * as core from "./internal/core.ts"
import * as Pipeable from "./Pipeable.ts"
import * as Predicate from "./Predicate.ts"
import type * as Types from "./Types.ts"
import type { Unify } from "./Unify.ts"

/**
 * Provides a base class for immutable data types.
 *
 * **When to use**
 *
 * Use when you need a lightweight immutable value type with `.pipe()` support.
 *
 * **Details**
 *
 * Extend `Class` with a type parameter to declare fields. The constructor
 * accepts those fields as a single object argument. When there are no fields
 * the argument is optional. Instances are `Readonly` and `Pipeable`.
 *
 * **Example** (Defining a value class)
 *
 * ```ts
 * import { Data, Equal } from "effect"
 *
 * class Person extends Data.Class<{ readonly name: string }> {}
 *
 * const mike1 = new Person({ name: "Mike" })
 * const mike2 = new Person({ name: "Mike" })
 *
 * console.log(Equal.equals(mike1, mike2))
 * // true
 * ```
 *
 * @see {@link TaggedClass} — adds a `_tag` field
 * @see {@link Error} — yieldable error variant
 *
 * @category constructors
 * @since 2.0.0
 */
export const Class: new<A extends Record<string, any> = {}>(
  args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }>
) => Readonly<A> & Pipeable.Pipeable = class extends Pipeable.Class {
  constructor(props: any) {
    super()
    if (props) {
      Object.assign(this, props)
    }
  }
} as any

/**
 * Provides a base class for immutable data types with a `_tag` discriminator.
 *
 * **When to use**
 *
 * Use when you need a single-variant tagged type or an ad-hoc discriminator.
 *
 * **Details**
 *
 * Like {@link Class}, but the resulting instances also carry a
 * `readonly _tag: Tag` property. The `_tag` is excluded from the constructor
 * argument.
 *
 * **Example** (Defining a tagged class)
 *
 * ```ts
 * import { Data } from "effect"
 *
 * class Person extends Data.TaggedClass("Person")<{
 *   readonly name: string
 * }> {}
 *
 * const mike = new Person({ name: "Mike" })
 * console.log(mike._tag)
 * // "Person"
 * ```
 *
 * @see {@link Class} — without a `_tag`
 * @see {@link TaggedError} — tagged error variant
 * @see {@link TaggedEnum} — multi-variant unions
 *
 * @category constructors
 * @since 2.0.0
 */
export const TaggedClass = <Tag extends string>(
  tag: Tag
): new<A extends Record<string, any> = {}>(
  args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>
) => Readonly<A> & { readonly _tag: Tag } & Pipeable.Pipeable =>
  class extends Class {
    readonly _tag = tag
  } as any

/**
 * Transforms a record of variant definitions into a discriminated union type.
 *
 * **When to use**
 *
 * Use when you have two or more variants that share a common `_tag` discriminator.
 *
 * **Details**
 *
 * Each key in the record becomes a variant with `readonly _tag` set to that
 * key. Use with {@link taggedEnum} to get constructors and matchers.
 *
 * **Gotchas**
 *
 * Variant records must **not** include a `_tag` property; it is added automatically.
 *
 * **Example** (Defining a tagged enum)
 *
 * ```ts
 * import { Data } from "effect"
 *
 * type HttpError = Data.TaggedEnum<{
 *   BadRequest: { readonly status: 400; readonly message: string }
 *   NotFound: { readonly status: 404 }
 * }>
 *
 * // Equivalent to:
 * // | { readonly _tag: "BadRequest"; readonly status: 400; readonly message: string }
 * // | { readonly _tag: "NotFound"; readonly status: 404 }
 *
 * const { BadRequest, NotFound } = Data.taggedEnum<HttpError>()
 *
 * const err = BadRequest({ status: 400, message: "missing id" })
 * console.log(err._tag)
 * // "BadRequest"
 * ```
 *
 * @see {@link taggedEnum} — constructors and matchers for a `TaggedEnum`
 * @see {@link TaggedEnum.WithGenerics} — generic tagged enums
 * @see {@link TaggedEnum.Constructor} — the constructor object type
 *
 * @category models
 * @since 2.0.0
 */
export type TaggedEnum<
  A extends Record<string, Record<string, any>> & UntaggedChildren<A>
> = keyof A extends infer Tag ? Tag extends keyof A ? Types.Simplify<
      { readonly _tag: Tag } & { readonly [K in keyof A[Tag]]: A[Tag][K] }
    >
  : never
  : never

type ChildrenAreTagged<A> = keyof A extends infer K ? K extends keyof A ? "_tag" extends keyof A[K] ? true
    : false
  : never
  : never

type UntaggedChildren<A> = true extends ChildrenAreTagged<A>
  ? "It looks like you're trying to create a tagged enum, but one or more of its members already has a `_tag` property."
  : unknown

/**
 * Namespace for `TaggedEnum` utility types.
 *
 * **When to use**
 *
 * Use to reference utility types for constructing, extracting, and matching
 * `TaggedEnum` variants.
 *
 * **Details**
 *
 * Provides helper types for:
 * - Generic tagged enums ({@link TaggedEnum.WithGenerics}, {@link TaggedEnum.Kind})
 * - Extracting constructor arguments ({@link TaggedEnum.Args}) and variant
 *   values ({@link TaggedEnum.Value})
 * - Full constructor objects ({@link TaggedEnum.Constructor})
 *
 * @since 2.0.0
 */
export declare namespace TaggedEnum {
  /**
   * Defines a tagged enum shape that accepts generic type parameters.
   *
   * **When to use**
   *
   * Use when variant payloads need to be parameterized, such as `Result<E, A>`.
   *
   * **Details**
   *
   * Extend this interface and set `taggedEnum` to your union type, using
   * `this["A"]`, `this["B"]`, etc. as placeholders for the generics. The
   * `Count` parameter declares how many generics are used (up to 4).
   *
   * **Example** (Defining a generic tagged enum)
   *
   * ```ts
   * import { Data } from "effect"
   *
   * type MyResult<E, A> = Data.TaggedEnum<{
   *   Failure: { readonly error: E }
   *   Success: { readonly value: A }
   * }>
   *
   * interface MyResultDef extends Data.TaggedEnum.WithGenerics<2> {
   *   readonly taggedEnum: MyResult<this["A"], this["B"]>
   * }
   *
   * const { Failure, Success } = Data.taggedEnum<MyResultDef>()
   *
   * const ok = Success({ value: 42 })
   * // ok: { readonly _tag: "Success"; readonly value: number }
   * ```
   *
   * @see {@link Kind} — apply concrete types to a `WithGenerics` definition
   * @see {@link taggedEnum} — constructors and matchers
   *
   * @category models
   * @since 2.0.0
   */
  export interface WithGenerics<Count extends number> {
    readonly taggedEnum: { readonly _tag: string }
    readonly numberOfGenerics: Count

    readonly A: unknown
    readonly B: unknown
    readonly C: unknown
    readonly D: unknown
  }

  /**
   * Applies concrete type arguments to a `WithGenerics` definition, producing
   * the resulting tagged union type.
   *
   * **When to use**
   *
   * Use to refer to a specific instantiation of a generic tagged enum in type signatures.
   *
   * **Example** (Applying generics)
   *
   * ```ts
   * import type { Data } from "effect"
   *
   * type Option<A> = Data.TaggedEnum<{
   *   None: {}
   *   Some: { readonly value: A }
   * }>
   * interface OptionDef extends Data.TaggedEnum.WithGenerics<1> {
   *   readonly taggedEnum: Option<this["A"]>
   * }
   *
   * // Resolve to the concrete union for `string`
   * type StringOption = Data.TaggedEnum.Kind<OptionDef, string>
   * // { _tag: "None" } | { _tag: "Some"; value: string }
   * ```
   *
   * @see {@link WithGenerics} — define the generic shape
   *
   * @category utility types
   * @since 2.0.0
   */
  export type Kind<
    Z extends WithGenerics<number>,
    A = unknown,
    B = unknown,
    C = unknown,
    D = unknown
  > = (Z & {
    readonly A: A
    readonly B: B
    readonly C: C
    readonly D: D
  })["taggedEnum"]

  /**
   * Extracts the constructor argument type for a specific variant of a tagged
   * union.
   *
   * **When to use**
   *
   * Use to derive the argument object expected by a constructor for one tagged
   * union variant.
   *
   * **Details**
   *
   * Returns `void` if the variant has no fields beyond `_tag`.
   *
   * **Example** (Extracting variant args)
   *
   * ```ts
   * import type { Data } from "effect"
   *
   * type Result =
   *   | { readonly _tag: "Ok"; readonly value: number }
   *   | { readonly _tag: "Err"; readonly error: string }
   *
   * type OkArgs = Data.TaggedEnum.Args<Result, "Ok">
   * // { readonly value: number }
   *
   * type ErrArgs = Data.TaggedEnum.Args<Result, "Err">
   * // { readonly error: string }
   * ```
   *
   * @see {@link Value} — extracts the full variant type (including `_tag`)
   *
   * @category utility types
   * @since 2.0.0
   */
  export type Args<
    A extends { readonly _tag: string },
    K extends A["_tag"],
    E = Extract<A, { readonly _tag: K }>
  > = {
    readonly [K in keyof E as K extends "_tag" ? never : K]: E[K]
  } extends infer T ? Types.VoidIfEmpty<T>
    : never

  /**
   * Extracts the full variant type (including `_tag`) for a specific tag.
   *
   * **When to use**
   *
   * Use to select one full tagged-union variant by its `_tag` value.
   *
   * **Example** (Extracting a variant type)
   *
   * ```ts
   * import type { Data } from "effect"
   *
   * type Result =
   *   | { readonly _tag: "Ok"; readonly value: number }
   *   | { readonly _tag: "Err"; readonly error: string }
   *
   * type OkVariant = Data.TaggedEnum.Value<Result, "Ok">
   * // { readonly _tag: "Ok"; readonly value: number }
   * ```
   *
   * @see {@link Args} — extracts fields without `_tag`
   *
   * @category utility types
   * @since 2.0.0
   */
  export type Value<
    A extends { readonly _tag: string },
    K extends A["_tag"]
  > = Extract<A, { readonly _tag: K }>

  /**
   * The full constructors-and-matchers object type returned by {@link taggedEnum}.
   *
   * **When to use**
   *
   * Use when you want to annotate an exported constructor bundle so downstream
   * code keeps exact variant constructors and exhaustive matching.
   *
   * **Details**
   *
   * Includes:
   * - A constructor function for each variant (keyed by tag name)
   * - `$is(tag)` — returns a type-guard that checks only the `_tag` field;
   *   safe when the tag is globally unique and the value was produced by your
   *   constructors. For untrusted input, validate with the `Schema` module first.
   * - `$match` — exhaustive pattern matching (data-last or data-first)
   *
   * **Example** (Using the constructor object)
   *
   * ```ts
   * import { Data } from "effect"
   *
   * type Shape =
   *   | { readonly _tag: "Circle"; readonly radius: number }
   *   | { readonly _tag: "Rect"; readonly w: number; readonly h: number }
   *
   * const { Circle, Rect, $is, $match } = Data.taggedEnum<Shape>()
   *
   * const shape = Circle({ radius: 10 })
   *
   * // Type guard
   * if ($is("Circle")(shape)) {
   *   console.log(shape.radius)
   * }
   *
   * // Pattern matching
   * const label = $match(shape, {
   *   Circle: (s) => `circle r=${s.radius}`,
   *   Rect: (s) => `rect ${s.w}x${s.h}`
   * })
   * ```
   *
   * @see {@link taggedEnum} — creates constructors and matchers
   *
   * @category types
   * @since 3.1.0
   */
  export type Constructor<A extends { readonly _tag: string }> = Types.Simplify<
    {
      readonly [Tag in A["_tag"]]: ConstructorFrom<
        Extract<A, { readonly _tag: Tag }>,
        "_tag"
      >
    } & {
      readonly $is: <Tag extends A["_tag"]>(
        tag: Tag
      ) => (u: unknown) => u is Extract<A, { readonly _tag: Tag }>
      readonly $match: {
        <
          Cases extends {
            readonly [Tag in A["_tag"]]: (
              args: Extract<A, { readonly _tag: Tag }>
            ) => any
          }
        >(
          cases: Cases
        ): (value: A) => Unify<ReturnType<Cases[A["_tag"]]>>
        <
          Cases extends {
            readonly [Tag in A["_tag"]]: (
              args: Extract<A, { readonly _tag: Tag }>
            ) => any
          }
        >(
          value: A,
          cases: Cases
        ): Unify<ReturnType<Cases[A["_tag"]]>>
      }
    }
  >

  /**
   * Function type that constructs a tagged-union variant from its fields,
   * excluding the keys listed in `Tag`.
   *
   * **When to use**
   *
   * Use to type an individual constructor for one tagged-union variant.
   *
   * **Details**
   *
   * The constructor returns the full variant type `A`. If no fields remain
   * after excluding `Tag` keys, the constructor argument type becomes `void`.
   *
   * @category utility types
   * @since 4.0.0
   */
  export type ConstructorFrom<A, Tag extends keyof A = never> = (
    args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends Tag ? never : P]: A[P] }>
  ) => A

  /**
   * Type-guard and pattern-matching interface for generic tagged enums.
   *
   * **When to use**
   *
   * Use to type the `$is` and `$match` helpers for generic tagged enums.
   *
   * **Details**
   *
   * This is the `$is` / `$match` portion of the object returned by
   * {@link taggedEnum} when used with a {@link WithGenerics} definition.
   *
   * @see {@link Constructor} — the non-generic equivalent
   *
   * @category models
   * @since 3.2.0
   */
  export interface GenericMatchers<Z extends WithGenerics<number>> {
    readonly $is: <Tag extends Z["taggedEnum"]["_tag"]>(
      tag: Tag
    ) => {
      <T extends TaggedEnum.Kind<Z, any, any, any, any>>(
        u: T
      ): u is T & { readonly _tag: Tag }
      (u: unknown): u is Extract<TaggedEnum.Kind<Z>, { readonly _tag: Tag }>
    }
    readonly $match: {
      <
        A,
        B,
        C,
        D,
        Cases extends {
          readonly [Tag in Z["taggedEnum"]["_tag"]]: (
            args: Extract<
              TaggedEnum.Kind<Z, A, B, C, D>,
              { readonly _tag: Tag }
            >
          ) => any
        }
      >(
        cases: Cases
      ): (
        self: TaggedEnum.Kind<Z, A, B, C, D>
      ) => Unify<ReturnType<Cases[Z["taggedEnum"]["_tag"]]>>
      <
        A,
        B,
        C,
        D,
        Cases extends {
          readonly [Tag in Z["taggedEnum"]["_tag"]]: (
            args: Extract<
              TaggedEnum.Kind<Z, A, B, C, D>,
              { readonly _tag: Tag }
            >
          ) => any
        }
      >(
        self: TaggedEnum.Kind<Z, A, B, C, D>,
        cases: Cases
      ): Unify<ReturnType<Cases[Z["taggedEnum"]["_tag"]]>>
    }
  }
}

/**
 * Creates constructors and matchers for a `TaggedEnum` type.
 *
 * **When to use**
 *
 * Use when you model a closed union with plain data objects and want
 * construction, tag checks, and exhaustive matching from the same definition.
 *
 * **Details**
 *
 * Returns an object with:
 * - One constructor per variant (keyed by tag name)
 * - `$is(tag)` — returns a type-guard function that checks only the `_tag` field
 * - `$match` — exhaustive pattern matching (data-first or data-last)
 *
 * **Gotchas**
 *
 * - Constructors produce **plain objects**, not class instances.
 * - `$is(tag)` only checks the `_tag` field, not the full structure. It relies
 *   on the tag being globally unique and the value being produced by your
 *   constructors. For untrusted input, validate with the `Schema` module first.
 *
 * **Example** (Creating and matching tagged enum values)
 *
 * ```ts
 * import { Data } from "effect"
 *
 * type HttpError = Data.TaggedEnum<{
 *   BadRequest: { readonly message: string }
 *   NotFound: { readonly url: string }
 * }>
 *
 * const { BadRequest, NotFound, $is, $match } = Data.taggedEnum<HttpError>()
 *
 * const err = NotFound({ url: "/missing" })
 *
 * // Type guard
 * console.log($is("NotFound")(err)) // true
 *
 * // Pattern matching
 * const msg = $match(err, {
 *   BadRequest: (e) => e.message,
 *   NotFound: (e) => `${e.url} not found`
 * })
 * console.log(msg) // "/missing not found"
 * ```
 *
 * **Example** (Defining a generic tagged enum)
 *
 * ```ts
 * import { Data } from "effect"
 *
 * type MyResult<E, A> = Data.TaggedEnum<{
 *   Failure: { readonly error: E }
 *   Success: { readonly value: A }
 * }>
 * interface MyResultDef extends Data.TaggedEnum.WithGenerics<2> {
 *   readonly taggedEnum: MyResult<this["A"], this["B"]>
 * }
 * const { Failure, Success } = Data.taggedEnum<MyResultDef>()
 *
 * const ok = Success({ value: 42 })
 * // ok: { readonly _tag: "Success"; readonly value: number }
 * ```
 *
 * @see {@link TaggedEnum} — the type-level companion
 * @see {@link TaggedEnum.Constructor} — the returned object type
 * @see {@link TaggedEnum.WithGenerics} — generic enum support
 *
 * @category constructors
 * @since 2.0.0
 */
export const taggedEnum: {
  <Z extends TaggedEnum.WithGenerics<1>>(): Types.Simplify<
    {
      readonly [Tag in Z["taggedEnum"]["_tag"]]: <A>(
        args: TaggedEnum.Args<
          TaggedEnum.Kind<Z, A>,
          Tag,
          Extract<TaggedEnum.Kind<Z, A>, { readonly _tag: Tag }>
        >
      ) => TaggedEnum.Value<TaggedEnum.Kind<Z, A>, Tag>
    } & TaggedEnum.GenericMatchers<Z>
  >

  <Z extends TaggedEnum.WithGenerics<2>>(): Types.Simplify<
    {
      readonly [Tag in Z["taggedEnum"]["_tag"]]: <A, B>(
        args: TaggedEnum.Args<
          TaggedEnum.Kind<Z, A, B>,
          Tag,
          Extract<TaggedEnum.Kind<Z, A, B>, { readonly _tag: Tag }>
        >
      ) => TaggedEnum.Value<TaggedEnum.Kind<Z, A, B>, Tag>
    } & TaggedEnum.GenericMatchers<Z>
  >

  <Z extends TaggedEnum.WithGenerics<3>>(): Types.Simplify<
    {
      readonly [Tag in Z["taggedEnum"]["_tag"]]: <A, B, C>(
        args: TaggedEnum.Args<
          TaggedEnum.Kind<Z, A, B, C>,
          Tag,
          Extract<TaggedEnum.Kind<Z, A, B, C>, { readonly _tag: Tag }>
        >
      ) => TaggedEnum.Value<TaggedEnum.Kind<Z, A, B, C>, Tag>
    } & TaggedEnum.GenericMatchers<Z>
  >

  <Z extends TaggedEnum.WithGenerics<4>>(): Types.Simplify<
    {
      readonly [Tag in Z["taggedEnum"]["_tag"]]: <A, B, C, D>(
        args: TaggedEnum.Args<
          TaggedEnum.Kind<Z, A, B, C, D>,
          Tag,
          Extract<TaggedEnum.Kind<Z, A, B, C, D>, { readonly _tag: Tag }>
        >
      ) => TaggedEnum.Value<TaggedEnum.Kind<Z, A, B, C, D>, Tag>
    } & TaggedEnum.GenericMatchers<Z>
  >

  <A extends { readonly _tag: string }>(): TaggedEnum.Constructor<A>
} = () =>
  new Proxy(
    {},
    {
      get(_target, tag, _receiver) {
        if (tag === "$is") {
          return Predicate.isTagged
        } else if (tag === "$match") {
          return taggedMatch
        }
        return (props: any) => ({ ...props, _tag: tag })
      }
    }
  ) as any

function taggedMatch<
  A extends { readonly _tag: string },
  Cases extends {
    readonly [K in A["_tag"]]: (args: Extract<A, { readonly _tag: K }>) => any
  }
>(self: A, cases: Cases): ReturnType<Cases[A["_tag"]]>
function taggedMatch<
  A extends { readonly _tag: string },
  Cases extends {
    readonly [K in A["_tag"]]: (args: Extract<A, { readonly _tag: K }>) => any
  }
>(cases: Cases): (value: A) => ReturnType<Cases[A["_tag"]]>
function taggedMatch<
  A extends { readonly _tag: string },
  Cases extends {
    readonly [K in A["_tag"]]: (args: Extract<A, { readonly _tag: K }>) => any
  }
>(): any {
  if (arguments.length === 1) {
    const cases = arguments[0] as Cases
    return function(value: A): ReturnType<Cases[A["_tag"]]> {
      return cases[value._tag as A["_tag"]](value as any)
    }
  }
  const value = arguments[0] as A
  const cases = arguments[1] as Cases
  return cases[value._tag as A["_tag"]](value as any)
}

/**
 * Provides a base class for yieldable errors.
 *
 * **When to use**
 *
 * Use when you need yieldable errors that do **not** need tag-based
 * discrimination.
 *
 * **Details**
 *
 * Extends `Cause.YieldableError`, so instances can be yielded inside
 * `Effect.gen` to fail the enclosing effect. Fields are passed as a single
 * object; when there are no fields the argument is optional. If a `message`
 * field is provided, it becomes the error's `.message`.
 *
 * **Example** (Defining a yieldable error)
 *
 * ```ts
 * import { Data, Effect } from "effect"
 *
 * class NetworkError extends Data.Error<{
 *   readonly code: number
 *   readonly message: string
 * }> {}
 *
 * const program = Effect.gen(function*() {
 *   return yield* new NetworkError({ code: 500, message: "timeout" })
 * })
 *
 * // The effect fails with a NetworkError
 * Effect.runSync(Effect.exit(program))
 * ```
 *
 * @see {@link TaggedError} — adds a `_tag` for `Effect.catchTag`
 * @see {@link Class} — non-error data class
 *
 * @category constructors
 * @since 2.0.0
 */
export const Error: new<A extends Record<string, any> = {}>(
  args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }>
) => Cause.YieldableError & Readonly<A> = core.Error

/**
 * Creates a tagged error class with a `_tag` discriminator.
 *
 * **When to use**
 *
 * Use when you need domain errors with discriminated-union handling.
 *
 * **Details**
 *
 * Like {@link Error}, but instances also carry a `readonly _tag` property,
 * enabling `Effect.catchTag` and `Effect.catchTags` for tag-based recovery.
 * The `_tag` is excluded from the constructor argument. Yielding an instance
 * inside `Effect.gen` fails the effect with this error.
 *
 * **Example** (Recovering by tag)
 *
 * ```ts
 * import { Data, Effect } from "effect"
 *
 * class NotFound extends Data.TaggedError("NotFound")<{
 *   readonly resource: string
 * }> {}
 *
 * class Forbidden extends Data.TaggedError("Forbidden")<{
 *   readonly reason: string
 * }> {}
 *
 * const program = Effect.gen(function*() {
 *   return yield* new NotFound({ resource: "/users/42" })
 * })
 *
 * const recovered = program.pipe(
 *   Effect.catchTag("NotFound", (e) =>
 *     Effect.succeed(`missing: ${e.resource}`))
 * )
 * ```
 *
 * @see {@link Error} — without a `_tag`
 * @see {@link TaggedClass} — tagged class that is not an error
 *
 * @category constructors
 * @since 2.0.0
 */
export const TaggedError: <Tag extends string>(
  tag: Tag
) => new<A extends Record<string, any> = {}>(
  args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>
) => Cause.YieldableError & { readonly _tag: Tag } & Readonly<A> = core.TaggedError as any
