/**
 * Represents immutable spans of time.
 *
 * A `Duration` can be finite, positive infinity, or negative infinity. It is
 * the standard representation for delays, timeouts, intervals, and
 * time-to-live values across Effect APIs. This module includes constructors
 * from common input shapes, unit conversions, comparisons, arithmetic,
 * formatting, and reusable reducer or combiner helpers.
 *
 * @since 2.0.0
 */
import * as Combiner from "./Combiner.ts"
import * as Equal from "./Equal.ts"
import type * as Equ from "./Equivalence.ts"
import { dual, identity } from "./Function.ts"
import * as Hash from "./Hash.ts"
import type * as Inspectable from "./Inspectable.ts"
import { NodeInspectSymbol } from "./Inspectable.ts"
import * as Option from "./Option.ts"
import * as order from "./Order.ts"
import type { Pipeable } from "./Pipeable.ts"
import { pipeArguments } from "./Pipeable.ts"
import { hasProperty, isNumber } from "./Predicate.ts"
import * as Reducer from "./Reducer.ts"

const TypeId = "~effect/time/Duration"

const bigint0 = BigInt(0)
const bigint1 = BigInt(1)
const bigint24 = BigInt(24)
const bigint60 = BigInt(60)
const bigint1e3 = BigInt(1_000)
const bigint1e6 = BigInt(1_000_000)
const bigint1e9 = BigInt(1_000_000_000)

const roundTiesAwayFromZero = (input: number): bigint =>
  BigInt(input < 0 ? Math.ceil(input - 0.5) : Math.floor(input + 0.5))

const roundMillisToNanos = (millis: number): bigint => roundTiesAwayFromZero(millis * 1_000_000)

const parseNanos = (input: string, scale: bigint): bigint =>
  input.includes(".") ? roundTiesAwayFromZero(Number(input) * Number(scale)) : BigInt(input) * scale

const nanosToHrTime = (nanos: bigint): [seconds: number, nanos: number] => {
  const sign = nanos < bigint0 ? -bigint1 : bigint1
  const absolute = nanos < bigint0 ? -nanos : nanos
  return [
    Number(sign * (absolute / bigint1e9)),
    Number(sign * (absolute % bigint1e9))
  ]
}

/**
 * Represents a span of time with high precision, supporting operations from
 * nanoseconds to weeks.
 *
 * **When to use**
 *
 * Use to model elapsed time, delays, timeouts, schedule intervals, and cache
 * TTLs as immutable duration values.
 *
 * @see {@link Input} for values accepted by APIs that decode duration-like
 * inputs
 * @see {@link DurationValue} for the tagged representation exposed by the
 * `value` field
 *
 * @category models
 * @since 2.0.0
 */
export interface Duration extends Equal.Equal, Pipeable, Inspectable.Inspectable {
  readonly [TypeId]: typeof TypeId
  readonly value: DurationValue
}

/**
 * Tagged representation of a `Duration` value.
 *
 * **When to use**
 *
 * Use when modeling or inspecting the exact tagged representation stored in a
 * `Duration`, including finite millisecond or nanosecond values and infinite
 * sentinels.
 *
 * **Details**
 *
 * A duration is represented as milliseconds, nanoseconds, positive infinity,
 * or negative infinity.
 *
 * @see {@link Duration} for the public type whose `value` field contains this
 * representation
 * @see {@link match} for pattern matching without reading `value` directly
 *
 * @category models
 * @since 2.0.0
 */
export type DurationValue =
  | { _tag: "Millis"; millis: number }
  | { _tag: "Nanos"; nanos: bigint }
  | { _tag: "Infinity" }
  | { _tag: "NegativeInfinity" }

/**
 * Valid time units that can be used in duration string representations.
 *
 * **When to use**
 *
 * Use when typing the unit portion of duration string inputs accepted by
 * `Duration.Input`.
 *
 * @see {@link Input} for the full duration input union
 *
 * @category models
 * @since 2.0.0
 */
export type Unit =
  | "nano"
  | "nanos"
  | "micro"
  | "micros"
  | "milli"
  | "millis"
  | "second"
  | "seconds"
  | "minute"
  | "minutes"
  | "hour"
  | "hours"
  | "day"
  | "days"
  | "week"
  | "weeks"

/**
 * Valid input types that can be converted to a Duration.
 *
 * **When to use**
 *
 * Use when an API should accept any value that Effect can convert into a
 * `Duration`, including existing durations, millisecond numbers, nanosecond
 * bigints, high-resolution tuples, duration strings, infinity strings, or
 * duration objects.
 *
 * **Details**
 *
 * String inputs accept values like `"10 seconds"`, `"500 millis"`,
 * `"Infinity"`, and `"-Infinity"`. Finite fractional values that are
 * normalized to nanoseconds are rounded to the nearest nanosecond, with ties
 * away from zero.
 *
 * @see {@link fromInput} for safe conversion to `Option`
 * @see {@link fromInputUnsafe} for throwing conversion
 * @see {@link DurationObject} for object-shaped duration input
 * @see {@link Unit} for supported string units
 *
 * @category models
 * @since 4.0.0
 */
export type Input =
  | Duration
  | number // millis
  | bigint // nanos
  | readonly [seconds: number, nanos: number]
  | `${number} ${Unit}`
  | "Infinity"
  | "-Infinity"
  | DurationObject

/**
 * An object with optional duration components that can be combined to create
 * a Duration. All fields are optional and additive.
 *
 * **Details**
 *
 * Compatible with Temporal.Duration-like objects.
 *
 * **Example** (Combining duration object fields)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * Duration.fromInputUnsafe({ seconds: 30 })
 * Duration.fromInputUnsafe({ days: 1 })
 * Duration.fromInputUnsafe({ seconds: 1, nanoseconds: 500 })
 * ```
 *
 * @category models
 * @since 4.0.0
 */
export interface DurationObject {
  readonly weeks?: number | undefined
  readonly days?: number | undefined
  readonly hours?: number | undefined
  readonly minutes?: number | undefined
  readonly seconds?: number | undefined
  readonly milliseconds?: number | undefined
  readonly microseconds?: number | undefined
  readonly nanoseconds?: number | undefined
}

const DURATION_REGEXP = /^(-?\d+(?:\.\d+)?)\s+(nanos?|micros?|millis?|seconds?|minutes?|hours?|days?|weeks?)$/

/**
 * Decodes a `Duration.Input` into a `Duration`.
 *
 * **When to use**
 *
 * Use when the input has already been validated or comes from a trusted source
 * and throwing is acceptable for invalid duration syntax.
 *
 * **Gotchas**
 *
 * If the input is not a valid `Duration.Input`, it throws an error.
 *
 * **Example** (Decoding duration inputs)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const duration1 = Duration.fromInputUnsafe(1000) // 1000 milliseconds
 * const duration2 = Duration.fromInputUnsafe("5 seconds")
 * const duration3 = Duration.fromInputUnsafe("Infinity")
 * const duration4 = Duration.fromInputUnsafe([2, 500_000_000]) // 2 seconds and 500ms
 * ```
 *
 * @category constructors
 * @since 4.0.0
 */
export const fromInputUnsafe = (input: Input): Duration => {
  switch (typeof input) {
    case "number":
      return millis(input)
    case "bigint":
      return nanos(input)
    case "string": {
      if (input === "Infinity") {
        return infinity
      }
      if (input === "-Infinity") {
        return negativeInfinity
      }
      const match = DURATION_REGEXP.exec(input)
      if (!match) break
      const [_, valueStr, unit] = match
      if (unit === "nano" || unit === "nanos") {
        return nanos(parseNanos(valueStr, bigint1))
      }
      if (unit === "micro" || unit === "micros") {
        return nanos(parseNanos(valueStr, bigint1e3))
      }
      const value = Number(valueStr)
      switch (unit) {
        case "milli":
        case "millis":
          return millis(value)
        case "second":
        case "seconds":
          return seconds(value)
        case "minute":
        case "minutes":
          return minutes(value)
        case "hour":
        case "hours":
          return hours(value)
        case "day":
        case "days":
          return days(value)
        case "week":
        case "weeks":
          return weeks(value)
      }
      break
    }
    case "object": {
      if (input === null) break
      if (TypeId in input) return input as Duration
      if (Array.isArray(input)) {
        if (input.length !== 2 || !input.every(isNumber)) {
          return invalid(input)
        }
        if (Number.isNaN(input[0]) || Number.isNaN(input[1])) {
          return zero
        }
        if (input[0] === -Infinity || input[1] === -Infinity) {
          return negativeInfinity
        }
        if (input[0] === Infinity || input[1] === Infinity) {
          return infinity
        }
        return make(roundTiesAwayFromZero(input[0] * 1_000_000_000 + input[1]))
      }
      const obj = input as DurationObject
      let millis = 0
      // we can use truthy checks here, because 0 can be ignored
      if (obj.weeks) millis += obj.weeks * 604_800_000
      if (obj.days) millis += obj.days * 86_400_000
      if (obj.hours) millis += obj.hours * 3_600_000
      if (obj.minutes) millis += obj.minutes * 60_000
      if (obj.seconds) millis += obj.seconds * 1_000
      if (obj.milliseconds) millis += obj.milliseconds
      if (!obj.microseconds && !obj.nanoseconds) return make(millis)
      return make(roundTiesAwayFromZero(
        millis * 1_000_000 + (obj.microseconds ?? 0) * 1_000 + (obj.nanoseconds ?? 0)
      ))
    }
  }
  return invalid(input)
}

const invalid = (input: unknown): never => {
  throw new Error(`Invalid Input: ${input}`)
}

/**
 * Decodes a `Input` value into a `Duration` safely, returning
 * `Option.none()` if decoding fails.
 *
 * **Example** (Safely decoding duration inputs)
 *
 * ```ts
 * import { Duration, Option } from "effect"
 *
 * Duration.fromInput(1000).pipe(Option.map(Duration.toSeconds)) // Some(1)
 *
 * Duration.fromInput("invalid" as any) // None
 * ```
 *
 * @category constructors
 * @since 4.0.0
 */
export const fromInput: (u: Input) => Option.Option<Duration> = Option.liftThrowable(
  fromInputUnsafe
)

const zeroDurationValue: DurationValue = { _tag: "Millis", millis: 0 }
const infinityDurationValue: DurationValue = { _tag: "Infinity" }
const negativeInfinityDurationValue: DurationValue = { _tag: "NegativeInfinity" }

const DurationProto: Omit<Duration, "value"> = {
  [TypeId]: TypeId,
  [Hash.symbol](this: Duration) {
    return Hash.structure(this.value)
  },
  [Equal.symbol](this: Duration, that: unknown): boolean {
    return isDuration(that) && equals(this, that)
  },
  toString(this: Duration) {
    switch (this.value._tag) {
      case "Infinity":
        return "Infinity"
      case "NegativeInfinity":
        return "-Infinity"
      case "Nanos":
        return `${this.value.nanos} nanos`
      case "Millis":
        return `${this.value.millis} millis`
    }
  },
  toJSON(this: Duration) {
    switch (this.value._tag) {
      case "Millis":
        return { _id: "Duration", _tag: "Millis", millis: this.value.millis }
      case "Nanos":
        return { _id: "Duration", _tag: "Nanos", nanos: String(this.value.nanos) }
      case "Infinity":
        return { _id: "Duration", _tag: "Infinity" }
      case "NegativeInfinity":
        return { _id: "Duration", _tag: "NegativeInfinity" }
    }
  },
  [NodeInspectSymbol]() {
    return this.toJSON()
  },
  pipe() {
    return pipeArguments(this, arguments)
  }
} as const

const make = (input: number | bigint): Duration => {
  const duration = Object.create(DurationProto)
  if (typeof input === "number") {
    if (isNaN(input) || input === 0 || Object.is(input, -0)) {
      duration.value = zeroDurationValue
    } else if (!Number.isFinite(input)) {
      duration.value = input > 0 ? infinityDurationValue : negativeInfinityDurationValue
    } else if (!Number.isInteger(input)) {
      duration.value = { _tag: "Nanos", nanos: roundMillisToNanos(input) }
    } else {
      duration.value = { _tag: "Millis", millis: input }
    }
  } else if (input === bigint0) {
    duration.value = zeroDurationValue
  } else {
    duration.value = { _tag: "Nanos", nanos: input }
  }
  return duration
}

/**
 * Checks whether a value is a Duration.
 *
 * **Example** (Checking for durations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.isDuration(Duration.seconds(1))) // true
 * console.log(Duration.isDuration(1000)) // false
 * ```
 *
 * @category guards
 * @since 2.0.0
 */
export const isDuration = (u: unknown): u is Duration => hasProperty(u, TypeId)

/**
 * Checks whether a Duration is finite (not infinite).
 *
 * **Example** (Checking finite durations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.isFinite(Duration.seconds(5))) // true
 * console.log(Duration.isFinite(Duration.infinity)) // false
 * ```
 *
 * @category guards
 * @since 2.0.0
 */
export const isFinite = (self: Duration): boolean =>
  self.value._tag !== "Infinity" && self.value._tag !== "NegativeInfinity"

/**
 * Checks whether a Duration is zero.
 *
 * **Example** (Checking for zero durations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.isZero(Duration.zero)) // true
 * console.log(Duration.isZero(Duration.seconds(1))) // false
 * ```
 *
 * @category guards
 * @since 3.5.0
 */
export const isZero = (self: Duration): boolean => {
  switch (self.value._tag) {
    case "Millis":
      return self.value.millis === 0
    case "Nanos":
      return self.value.nanos === bigint0
    case "Infinity":
    case "NegativeInfinity":
      return false
  }
}

/**
 * Returns `true` if the duration is negative (strictly less than zero).
 *
 * **Example** (Checking for negative durations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.isNegative(Duration.seconds(-5))) // true
 * console.log(Duration.isNegative(Duration.zero)) // false
 * console.log(Duration.isNegative(Duration.negativeInfinity)) // true
 * ```
 *
 * @category guards
 * @since 4.0.0
 */
export const isNegative = (self: Duration): boolean => {
  switch (self.value._tag) {
    case "Millis":
      return self.value.millis < 0
    case "Nanos":
      return self.value.nanos < bigint0
    case "NegativeInfinity":
      return true
    case "Infinity":
      return false
  }
}

/**
 * Returns `true` if the duration is positive (strictly greater than zero).
 *
 * **Example** (Checking for positive durations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.isPositive(Duration.seconds(5))) // true
 * console.log(Duration.isPositive(Duration.zero)) // false
 * console.log(Duration.isPositive(Duration.infinity)) // true
 * ```
 *
 * @category guards
 * @since 4.0.0
 */
export const isPositive = (self: Duration): boolean => {
  switch (self.value._tag) {
    case "Millis":
      return self.value.millis > 0
    case "Nanos":
      return self.value.nanos > bigint0
    case "Infinity":
      return true
    case "NegativeInfinity":
      return false
  }
}

/**
 * Returns the absolute value of the duration.
 *
 * **Example** (Taking absolute duration values)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * Duration.toMillis(Duration.abs(Duration.seconds(-5))) // 5000
 * Duration.abs(Duration.negativeInfinity) === Duration.infinity // true
 * ```
 *
 * @category math
 * @since 4.0.0
 */
export const abs = (self: Duration): Duration => {
  switch (self.value._tag) {
    case "Infinity":
    case "NegativeInfinity":
      return infinity
    case "Millis":
      return self.value.millis < 0 ? make(-self.value.millis) : self
    case "Nanos":
      return self.value.nanos < bigint0 ? make(-self.value.nanos) : self
  }
}

/**
 * Returns the negated duration.
 *
 * **Example** (Negating durations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * Duration.toMillis(Duration.negate(Duration.seconds(5))) // -5000
 * Duration.negate(Duration.infinity) === Duration.negativeInfinity // true
 * ```
 *
 * @category math
 * @since 4.0.0
 */
export const negate = (self: Duration): Duration => {
  switch (self.value._tag) {
    case "Infinity":
      return negativeInfinity
    case "NegativeInfinity":
      return infinity
    case "Millis":
      return self.value.millis === 0 ? self : make(-self.value.millis)
    case "Nanos":
      return self.value.nanos === bigint0 ? self : make(-self.value.nanos)
  }
}

/**
 * A Duration representing zero time.
 *
 * **Example** (Referencing the zero duration)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.toMillis(Duration.zero)) // 0
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const zero: Duration = make(0)

/**
 * A Duration representing infinite time.
 *
 * **Example** (Referencing infinite duration)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.toMillis(Duration.infinity)) // Infinity
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const infinity: Duration = make(Infinity)

/**
 * A Duration representing negative infinite time.
 *
 * **Example** (Referencing negative infinite duration)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.toMillis(Duration.negativeInfinity)) // -Infinity
 * ```
 *
 * @category constructors
 * @since 4.0.0
 */
export const negativeInfinity: Duration = make(-Infinity)

/**
 * Creates a Duration from nanoseconds.
 *
 * **Example** (Creating durations from nanoseconds)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const duration = Duration.nanos(BigInt(500_000_000))
 * console.log(Duration.toMillis(duration)) // 500
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const nanos = (nanos: bigint): Duration => make(nanos)

/**
 * Creates a Duration from microseconds.
 *
 * **Example** (Creating durations from microseconds)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const duration = Duration.micros(BigInt(500_000))
 * console.log(Duration.toMillis(duration)) // 500
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const micros = (micros: bigint): Duration => make(micros * bigint1e3)

/**
 * Creates a Duration from milliseconds.
 *
 * **Example** (Creating durations from milliseconds)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const duration = Duration.millis(1000)
 * console.log(Duration.toMillis(duration)) // 1000
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const millis = (millis: number): Duration => make(millis)

/**
 * Creates a Duration from seconds.
 *
 * **Example** (Creating durations from seconds)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const duration = Duration.seconds(30)
 * console.log(Duration.toMillis(duration)) // 30000
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const seconds = (seconds: number): Duration => make(seconds * 1000)

/**
 * Creates a Duration from minutes.
 *
 * **Example** (Creating durations from minutes)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const duration = Duration.minutes(5)
 * console.log(Duration.toMillis(duration)) // 300000
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const minutes = (minutes: number): Duration => make(minutes * 60_000)

/**
 * Creates a Duration from hours.
 *
 * **Example** (Creating durations from hours)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const duration = Duration.hours(2)
 * console.log(Duration.toMillis(duration)) // 7200000
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const hours = (hours: number): Duration => make(hours * 3_600_000)

/**
 * Creates a Duration from days.
 *
 * **Example** (Creating durations from days)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const duration = Duration.days(1)
 * console.log(Duration.toMillis(duration)) // 86400000
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const days = (days: number): Duration => make(days * 86_400_000)

/**
 * Creates a Duration from weeks.
 *
 * **Example** (Creating durations from weeks)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const duration = Duration.weeks(1)
 * console.log(Duration.toMillis(duration)) // 604800000
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const weeks = (weeks: number): Duration => make(weeks * 604_800_000)

/**
 * Converts a Duration to milliseconds.
 *
 * **Example** (Converting durations to milliseconds)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.toMillis(Duration.seconds(5))) // 5000
 * console.log(Duration.toMillis(Duration.minutes(2))) // 120000
 * ```
 *
 * @category getters
 * @since 2.0.0
 */
export const toMillis = (self: Input): number =>
  match(fromInputUnsafe(self), {
    onMillis: identity,
    onNanos: (nanos) => Number(nanos) / 1_000_000,
    onInfinity: () => Infinity,
    onNegativeInfinity: () => -Infinity
  })

/**
 * Converts a Duration to seconds.
 *
 * **Example** (Converting durations to seconds)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.toSeconds(Duration.millis(5000))) // 5
 * console.log(Duration.toSeconds(Duration.minutes(2))) // 120
 * ```
 *
 * @category getters
 * @since 2.0.0
 */
export const toSeconds = (self: Input): number =>
  match(fromInputUnsafe(self), {
    onMillis: (millis) => millis / 1_000,
    onNanos: (nanos) => Number(nanos) / 1_000_000_000,
    onInfinity: () => Infinity,
    onNegativeInfinity: () => -Infinity
  })

/**
 * Converts a Duration to minutes.
 *
 * **Example** (Converting durations to minutes)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.toMinutes(Duration.seconds(120))) // 2
 * console.log(Duration.toMinutes(Duration.hours(1))) // 60
 * ```
 *
 * @category getters
 * @since 3.8.0
 */
export const toMinutes = (self: Input): number =>
  match(fromInputUnsafe(self), {
    onMillis: (millis) => millis / 60_000,
    onNanos: (nanos) => Number(nanos) / 60_000_000_000,
    onInfinity: () => Infinity,
    onNegativeInfinity: () => -Infinity
  })

/**
 * Converts a Duration to hours.
 *
 * **Example** (Converting durations to hours)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.toHours(Duration.minutes(120))) // 2
 * console.log(Duration.toHours(Duration.days(1))) // 24
 * ```
 *
 * @category getters
 * @since 3.8.0
 */
export const toHours = (self: Input): number =>
  match(fromInputUnsafe(self), {
    onMillis: (millis) => millis / 3_600_000,
    onNanos: (nanos) => Number(nanos) / 3_600_000_000_000,
    onInfinity: () => Infinity,
    onNegativeInfinity: () => -Infinity
  })

/**
 * Converts a Duration to days.
 *
 * **Example** (Converting durations to days)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.toDays(Duration.hours(48))) // 2
 * console.log(Duration.toDays(Duration.weeks(1))) // 7
 * ```
 *
 * @category getters
 * @since 3.8.0
 */
export const toDays = (self: Input): number =>
  match(fromInputUnsafe(self), {
    onMillis: (millis) => millis / 86_400_000,
    onNanos: (nanos) => Number(nanos) / 86_400_000_000_000,
    onInfinity: () => Infinity,
    onNegativeInfinity: () => -Infinity
  })

/**
 * Converts a Duration to weeks.
 *
 * **Example** (Converting durations to weeks)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * console.log(Duration.toWeeks(Duration.days(14))) // 2
 * console.log(Duration.toWeeks(Duration.days(7))) // 1
 * ```
 *
 * @category getters
 * @since 3.8.0
 */
export const toWeeks = (self: Input): number =>
  match(fromInputUnsafe(self), {
    onMillis: (millis) => millis / 604_800_000,
    onNanos: (nanos) => Number(nanos) / 604_800_000_000_000,
    onInfinity: () => Infinity,
    onNegativeInfinity: () => -Infinity
  })

/**
 * Gets the duration in nanoseconds as a bigint.
 *
 * **When to use**
 *
 * Use when the duration is known to be finite and you need the nanosecond value
 * as a `bigint`.
 *
 * **Details**
 *
 * Millisecond-backed fractional durations are rounded to the nearest
 * nanosecond, with ties away from zero.
 *
 * **Gotchas**
 *
 * If the duration is infinite, it throws an error.
 *
 * **Example** (Reading nanoseconds unsafely)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const duration = Duration.seconds(2)
 * const nanos = Duration.toNanosUnsafe(duration)
 * console.log(nanos) // 2000000000n
 *
 * // Duration.toNanosUnsafe(Duration.infinity)
 * // throws Error: "Cannot convert infinite duration to nanos"
 * ```
 *
 * @category getters
 * @since 4.0.0
 */
export const toNanosUnsafe = (input: Input): bigint => {
  const self = fromInputUnsafe(input)
  switch (self.value._tag) {
    case "Infinity":
    case "NegativeInfinity":
      throw new Error("Cannot convert infinite duration to nanos")
    case "Nanos":
      return self.value.nanos
    case "Millis":
      return roundMillisToNanos(self.value.millis)
  }
}

/**
 * Gets the duration in nanoseconds safely as an `Option<bigint>`.
 *
 * **Details**
 *
 * If the duration is infinite, returns `Option.none()`.
 *
 * **Example** (Safely reading nanoseconds)
 *
 * ```ts
 * import { Duration, Option } from "effect"
 *
 * Duration.toNanos(Duration.seconds(1)) // Some(1000000000n)
 *
 * Duration.toNanos(Duration.infinity) // None
 * Option.getOrUndefined(Duration.toNanos(Duration.infinity)) // undefined
 * ```
 *
 * @category getters
 * @since 2.0.0
 */
export const toNanos: (self: Input) => Option.Option<bigint> = Option.liftThrowable(toNanosUnsafe)

/**
 * Converts a Duration to high-resolution time format [seconds, nanoseconds].
 *
 * **Example** (Converting durations to high-resolution time)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const duration = Duration.millis(1500)
 * const hrtime = Duration.toHrTime(duration)
 * console.log(hrtime) // [1, 500000000]
 * ```
 *
 * @category getters
 * @since 2.0.0
 */
export const toHrTime = (input: Input): [seconds: number, nanos: number] => {
  const self = fromInputUnsafe(input)
  switch (self.value._tag) {
    case "Infinity":
      return [Infinity, 0]
    case "NegativeInfinity":
      return [-Infinity, 0]
    case "Nanos":
      return nanosToHrTime(self.value.nanos)
    case "Millis":
      return nanosToHrTime(roundMillisToNanos(self.value.millis))
  }
}

/**
 * Pattern matches on the representation of a `Duration`.
 *
 * **Details**
 *
 * Provide handlers for millisecond-backed values, nanosecond-backed values,
 * and positive infinity. Use `onNegativeInfinity` to handle negative infinity
 * separately; otherwise negative infinity is handled by `onInfinity`.
 *
 * **Example** (Pattern matching on duration representations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const result = Duration.match(Duration.seconds(5), {
 *   onMillis: (millis) => `${millis} milliseconds`,
 *   onNanos: (nanos) => `${nanos} nanoseconds`,
 *   onInfinity: () => "infinite"
 * })
 * console.log(result) // "5000 milliseconds"
 * ```
 *
 * @category pattern matching
 * @since 2.0.0
 */
export const match: {
  <A, B, C, D = C>(
    options: {
      readonly onMillis: (millis: number) => A
      readonly onNanos: (nanos: bigint) => B
      readonly onInfinity: () => C
      readonly onNegativeInfinity?: () => D
    }
  ): (self: Duration) => A | B | C | D
  <A, B, C, D = C>(
    self: Duration,
    options: {
      readonly onMillis: (millis: number) => A
      readonly onNanos: (nanos: bigint) => B
      readonly onInfinity: () => C
      readonly onNegativeInfinity?: () => D
    }
  ): A | B | C | D
} = dual(2, <A, B, C, D = C>(
  self: Duration,
  options: {
    readonly onMillis: (millis: number) => A
    readonly onNanos: (nanos: bigint) => B
    readonly onInfinity: () => C
    readonly onNegativeInfinity?: () => D
  }
): A | B | C | D => {
  switch (self.value._tag) {
    case "Millis":
      return options.onMillis(self.value.millis)
    case "Nanos":
      return options.onNanos(self.value.nanos)
    case "Infinity":
      return options.onInfinity()
    case "NegativeInfinity":
      return (options.onNegativeInfinity ?? options.onInfinity as unknown as () => D)()
  }
})

/**
 * Pattern matches on two `Duration`s, providing handlers that receive both values.
 *
 * **Example** (Pattern matching on duration pairs)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const sum = Duration.matchPair(Duration.seconds(3), Duration.seconds(2), {
 *   onMillis: (a, b) => a + b,
 *   onNanos: (a, b) => Number(a + b),
 *   onInfinity: () => Infinity
 * })
 * console.log(sum) // 5000
 * ```
 *
 * @category pattern matching
 * @since 4.0.0
 */
export const matchPair: {
  <A, B, C>(
    that: Duration,
    options: {
      readonly onMillis: (self: number, that: number) => A
      readonly onNanos: (self: bigint, that: bigint) => B
      readonly onInfinity: (self: Duration, that: Duration) => C
    }
  ): (self: Duration) => A | B | C
  <A, B, C>(
    self: Duration,
    that: Duration,
    options: {
      readonly onMillis: (self: number, that: number) => A
      readonly onNanos: (self: bigint, that: bigint) => B
      readonly onInfinity: (self: Duration, that: Duration) => C
    }
  ): A | B | C
} = dual(3, <A, B, C>(
  self: Duration,
  that: Duration,
  options: {
    readonly onMillis: (self: number, that: number) => A
    readonly onNanos: (self: bigint, that: bigint) => B
    readonly onInfinity: (self: Duration, that: Duration) => C
  }
): A | B | C => {
  if (
    self.value._tag === "Infinity" || self.value._tag === "NegativeInfinity" ||
    that.value._tag === "Infinity" || that.value._tag === "NegativeInfinity"
  ) return options.onInfinity(self, that)
  if (self.value._tag === "Millis") {
    return that.value._tag === "Millis"
      ? options.onMillis(self.value.millis, that.value.millis)
      : options.onNanos(toNanosUnsafe(self), that.value.nanos)
  } else {
    return options.onNanos(self.value.nanos, toNanosUnsafe(that))
  }
})

/**
 * Provides an `Order` instance for comparing `Duration` values.
 *
 * **Details**
 *
 * `NegativeInfinity` < any finite value < `Infinity`.
 *
 * **Example** (Sorting durations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const durations = [
 *   Duration.seconds(3),
 *   Duration.seconds(1),
 *   Duration.seconds(2)
 * ]
 * const sorted = durations.sort((a, b) => Duration.Order(a, b))
 * console.log(sorted.map(Duration.toSeconds)) // [1, 2, 3]
 * ```
 *
 * @category instances
 * @since 2.0.0
 */
export const Order: order.Order<Duration> = order.make((self, that) =>
  matchPair(self, that, {
    onMillis: (self, that) => (self < that ? -1 : self > that ? 1 : 0),
    onNanos: (self, that) => (self < that ? -1 : self > that ? 1 : 0),
    onInfinity: (self, that) => {
      if (self.value._tag === that.value._tag) return 0
      if (self.value._tag === "Infinity") return 1
      if (self.value._tag === "NegativeInfinity") return -1
      // self is finite
      if (that.value._tag === "Infinity") return -1
      // that is NegativeInfinity
      return 1
    }
  })
)

/**
 * Returns `true` if a `Duration` is greater than or equal to `minimum` and
 * less than or equal to `maximum`, according to `Duration.Order`.
 *
 * **When to use**
 *
 * Use to test whether a duration is inside an inclusive range.
 *
 * **Details**
 *
 * Both bounds are inclusive and compared with `Duration.Order`.
 *
 * **Gotchas**
 *
 * The bounds are not normalized. If `minimum` is greater than `maximum`, the
 * predicate returns `false` for every duration.
 *
 * **Example** (Checking duration ranges)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const isInRange = Duration.between(Duration.seconds(3), {
 *   minimum: Duration.seconds(2),
 *   maximum: Duration.seconds(5)
 * })
 * console.log(isInRange) // true
 * ```
 *
 * @see {@link clamp} for constraining a duration to a range
 * @see {@link isGreaterThanOrEqualTo} for checking only the lower bound
 * @see {@link isLessThanOrEqualTo} for checking only the upper bound
 *
 * @category predicates
 * @since 2.0.0
 */
export const between: {
  (options: { minimum: Duration; maximum: Duration }): (self: Duration) => boolean
  (self: Duration, options: { minimum: Duration; maximum: Duration }): boolean
} = order.isBetween(Order)

/**
 * Provides an `Equivalence` instance for comparing `Duration` values.
 *
 * **Example** (Comparing durations for equivalence)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const isEqual = Duration.Equivalence(Duration.seconds(5), Duration.millis(5000))
 * console.log(isEqual) // true
 * ```
 *
 * @category instances
 * @since 2.0.0
 */
export const Equivalence: Equ.Equivalence<Duration> = (self, that) =>
  matchPair(self, that, {
    onMillis: (self, that) => self === that,
    onNanos: (self, that) => self === that,
    onInfinity: (self, that) => self.value._tag === that.value._tag
  })

/**
 * Returns the smaller of two Durations.
 *
 * **Example** (Selecting the shorter duration)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const shorter = Duration.min(Duration.seconds(5), Duration.seconds(3))
 * console.log(Duration.toSeconds(shorter)) // 3
 * ```
 *
 * @category ordering
 * @since 2.0.0
 */
export const min: {
  (that: Duration): (self: Duration) => Duration
  (self: Duration, that: Duration): Duration
} = order.min(Order)

/**
 * Returns the larger of two Durations.
 *
 * **Example** (Selecting the longer duration)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const longer = Duration.max(Duration.seconds(5), Duration.seconds(3))
 * console.log(Duration.toSeconds(longer)) // 5
 * ```
 *
 * @category ordering
 * @since 2.0.0
 */
export const max: {
  (that: Duration): (self: Duration) => Duration
  (self: Duration, that: Duration): Duration
} = order.max(Order)

/**
 * Returns a `Duration` constrained between a minimum and maximum value.
 *
 * **Example** (Clamping durations to a range)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const clamped = Duration.clamp(Duration.seconds(10), {
 *   minimum: Duration.seconds(2),
 *   maximum: Duration.seconds(5)
 * })
 * console.log(Duration.toSeconds(clamped)) // 5
 * ```
 *
 * @category ordering
 * @since 2.0.0
 */
export const clamp: {
  (options: { minimum: Duration; maximum: Duration }): (self: Duration) => Duration
  (self: Duration, options: { minimum: Duration; maximum: Duration }): Duration
} = order.clamp(Order)

/**
 * Divides a `Duration` by a finite, non-zero number safely.
 *
 * **Details**
 *
 * Returns `Option.none()` for zero, negative zero, or non-finite divisors. For
 * nanosecond-backed durations, also returns `Option.none()` when the divisor
 * cannot be converted to a `bigint`, such as a fractional divisor.
 *
 * **Example** (Safely dividing durations)
 *
 * ```ts
 * import { Duration, Option } from "effect"
 *
 * const d = Duration.divide(Duration.seconds(10), 2)
 * console.log(Option.map(d, Duration.toSeconds)) // Some(5)
 *
 * Duration.divide(Duration.seconds(10), 0) // None
 * ```
 *
 * @category math
 * @since 2.4.19
 */
export const divide: {
  (by: number): (self: Duration) => Option.Option<Duration>
  (self: Duration, by: number): Option.Option<Duration>
} = dual(
  2,
  (self: Duration, by: number): Option.Option<Duration> => {
    if (!Number.isFinite(by)) return Option.none()
    if (by === 0 || Object.is(by, -0)) return Option.none()
    return match(self, {
      onMillis: (millis) => Option.some(make(millis / by)),
      onNanos: (nanos) => {
        try {
          return Option.some(make(nanos / BigInt(by)))
        } catch {
          return Option.none()
        }
      },
      onInfinity: () => Option.some(by > 0 ? infinity : negativeInfinity),
      onNegativeInfinity: () => Option.some(by > 0 ? negativeInfinity : infinity)
    })
  }
)

/**
 * Divides a `Duration` by a number using fallback rules instead of returning
 * an `Option`.
 *
 * **When to use**
 *
 * Use when dividing a `Duration` should return `Duration.zero` or signed
 * infinity for invalid cases instead of forcing callers to handle `Option.none`.
 *
 * **Details**
 *
 * Non-finite divisors return `Duration.zero`. Division by positive or negative
 * zero can produce signed infinity for non-zero finite durations, while zero
 * or infinite durations divided by zero produce `Duration.zero`.
 * Nanosecond-backed durations return `Duration.zero` when the divisor cannot
 * be converted to a `bigint`.
 *
 * **Example** (Dividing durations unsafely)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const half = Duration.divideUnsafe(Duration.seconds(10), 2)
 * console.log(Duration.toSeconds(half)) // 5
 *
 * const infinite = Duration.divideUnsafe(Duration.seconds(10), 0)
 * console.log(Duration.toMillis(infinite)) // Infinity
 * ```
 *
 * @category math
 * @since 4.0.0
 */
export const divideUnsafe: {
  (by: number): (self: Duration) => Duration
  (self: Duration, by: number): Duration
} = dual(
  2,
  (self: Duration, by: number): Duration => {
    if (!Number.isFinite(by)) return zero
    return match(self, {
      onMillis: (millis) => make(millis / by),
      onNanos: (nanos) => {
        if (Object.is(by, 0) || Object.is(by, -0)) {
          if (nanos === bigint0) return zero
          // match IEEE 754: same sign → +infinity, different sign → -infinity
          const positiveNanos = nanos > bigint0
          const positiveZero = Object.is(by, 0)
          return (positiveNanos === positiveZero) ? infinity : negativeInfinity
        }
        try {
          return make(nanos / BigInt(by))
        } catch {
          return zero
        }
      },
      onInfinity: () => by > 0 ? infinity : by < 0 ? negativeInfinity : zero,
      onNegativeInfinity: () => by > 0 ? negativeInfinity : by < 0 ? infinity : zero
    })
  }
)

/**
 * Returns a `Duration` multiplied by a number.
 *
 * **Details**
 *
 * For nanosecond-backed durations, the multiplier must be convertible to a
 * `bigint`; fractional or non-finite multipliers can throw. Infinite
 * durations return positive infinity, negative infinity, or zero depending on
 * the multiplier sign.
 *
 * **Example** (Multiplying durations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const doubled = Duration.times(Duration.seconds(5), 2)
 * console.log(Duration.toSeconds(doubled)) // 10
 * ```
 *
 * @category math
 * @since 2.0.0
 */
export const times: {
  (times: number): (self: Duration) => Duration
  (self: Duration, times: number): Duration
} = dual(
  2,
  (self: Duration, times: number): Duration =>
    match(self, {
      onMillis: (millis) => make(millis * times),
      onNanos: (nanos) => make(nanos * BigInt(times)),
      onInfinity: () => times > 0 ? infinity : times < 0 ? negativeInfinity : zero,
      onNegativeInfinity: () => times > 0 ? negativeInfinity : times < 0 ? infinity : zero
    })
)

/**
 * Subtracts one Duration from another. The result can be negative.
 *
 * **Details**
 *
 * Infinity subtraction follows signed-infinity arithmetic. Subtracting the
 * same infinity from itself returns zero. Positive infinity minus negative
 * infinity or any finite duration remains positive infinity. Negative infinity
 * minus positive infinity or any finite duration remains negative infinity.
 * Finite durations minus positive infinity produce negative infinity, and
 * finite durations minus negative infinity produce positive infinity.
 *
 * **Example** (Subtracting durations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const result = Duration.subtract(Duration.seconds(10), Duration.seconds(3))
 * console.log(Duration.toSeconds(result)) // 7
 * ```
 *
 * @category math
 * @since 2.0.0
 */
export const subtract: {
  (that: Duration): (self: Duration) => Duration
  (self: Duration, that: Duration): Duration
} = dual(
  2,
  (self: Duration, that: Duration): Duration =>
    matchPair(self, that, {
      onMillis: (self, that) => make(self - that),
      onNanos: (self, that) => make(self - that),
      onInfinity: (self, that) => {
        const s = self.value._tag
        const t = that.value._tag
        if (s === "Infinity") return t === "Infinity" ? zero : infinity
        if (s === "NegativeInfinity") return t === "NegativeInfinity" ? zero : negativeInfinity
        return t === "Infinity" ? negativeInfinity : infinity
      }
    })
)

/**
 * Adds two Durations together.
 *
 * **Details**
 *
 * Infinity addition follows these rules:
 *
 * - infinity + infinity = infinity
 * - infinity + negativeInfinity = zero
 * - infinity + finite = infinity
 * - negativeInfinity + negativeInfinity = negativeInfinity
 * - negativeInfinity + finite = negativeInfinity
 *
 * **Example** (Adding durations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const total = Duration.sum(Duration.seconds(5), Duration.seconds(3))
 * console.log(Duration.toSeconds(total)) // 8
 * ```
 *
 * @category math
 * @since 2.0.0
 */
export const sum: {
  (that: Duration): (self: Duration) => Duration
  (self: Duration, that: Duration): Duration
} = dual(
  2,
  (self: Duration, that: Duration): Duration =>
    matchPair(self, that, {
      onMillis: (self, that) => make(self + that),
      onNanos: (self, that) => make(self + that),
      onInfinity: (self, that) => {
        const s = self.value._tag
        const t = that.value._tag
        if (s === "Infinity" && t === "NegativeInfinity") return zero
        if (s === "NegativeInfinity" && t === "Infinity") return zero
        if (s === "Infinity" || t === "Infinity") return infinity
        if (s === "NegativeInfinity" || t === "NegativeInfinity") return negativeInfinity
        // unreachable, but satisfy TS
        return zero
      }
    })
)

/**
 * Checks whether the first Duration is less than the second.
 *
 * **Example** (Comparing durations with less than)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const isLess = Duration.isLessThan(Duration.seconds(3), Duration.seconds(5))
 * console.log(isLess) // true
 * ```
 *
 * @category predicates
 * @since 4.0.0
 */
export const isLessThan: {
  (that: Duration): (self: Duration) => boolean
  (self: Duration, that: Duration): boolean
} = order.isLessThan(Order)

/**
 * Checks whether the first Duration is less than or equal to the second.
 *
 * **Example** (Comparing durations with less than or equal)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const isLessOrEqual = Duration.isLessThanOrEqualTo(
 *   Duration.seconds(5),
 *   Duration.seconds(5)
 * )
 * console.log(isLessOrEqual) // true
 * ```
 *
 * @category predicates
 * @since 4.0.0
 */
export const isLessThanOrEqualTo: {
  (that: Duration): (self: Duration) => boolean
  (self: Duration, that: Duration): boolean
} = order.isLessThanOrEqualTo(Order)

/**
 * Checks whether the first Duration is greater than the second.
 *
 * **Example** (Comparing durations with greater than)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const isGreater = Duration.isGreaterThan(Duration.seconds(5), Duration.seconds(3))
 * console.log(isGreater) // true
 * ```
 *
 * @category predicates
 * @since 4.0.0
 */
export const isGreaterThan: {
  (that: Duration): (self: Duration) => boolean
  (self: Duration, that: Duration): boolean
} = order.isGreaterThan(Order)

/**
 * Checks whether the first Duration is greater than or equal to the second.
 *
 * **Example** (Comparing durations with greater than or equal)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const isGreaterOrEqual = Duration.isGreaterThanOrEqualTo(
 *   Duration.seconds(5),
 *   Duration.seconds(5)
 * )
 * console.log(isGreaterOrEqual) // true
 * ```
 *
 * @category predicates
 * @since 4.0.0
 */
export const isGreaterThanOrEqualTo: {
  (that: Duration): (self: Duration) => boolean
  (self: Duration, that: Duration): boolean
} = order.isGreaterThanOrEqualTo(Order)

/**
 * Checks whether two Durations are equal.
 *
 * **Example** (Checking duration equality)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * const isEqual = Duration.equals(Duration.seconds(5), Duration.millis(5000))
 * console.log(isEqual) // true
 * ```
 *
 * @category predicates
 * @since 2.0.0
 */
export const equals: {
  (that: Duration): (self: Duration) => boolean
  (self: Duration, that: Duration): boolean
} = dual(2, (self: Duration, that: Duration): boolean => Equivalence(self, that))

/**
 * Decomposes a `Duration` into normalized signed components.
 *
 * **Details**
 *
 * Finite durations are returned as `{ days, hours, minutes, seconds, millis,
 * nanos }`. Infinite durations return every component as `Infinity` or
 * `-Infinity`.
 *
 * **Example** (Decomposing durations into parts)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * // Create a complex duration by adding multiple parts
 * const duration = Duration.sum(
 *   Duration.sum(
 *     Duration.sum(Duration.days(1), Duration.hours(2)),
 *     Duration.sum(Duration.minutes(30), Duration.seconds(45))
 *   ),
 *   Duration.millis(123)
 * )
 * const components = Duration.parts(duration)
 * console.log(components)
 * // {
 * //   days: 1,
 * //   hours: 2,
 * //   minutes: 30,
 * //   seconds: 45,
 * //   millis: 123,
 * //   nanos: 0
 * // }
 *
 * const complex = Duration.sum(Duration.hours(25), Duration.minutes(90))
 * const complexParts = Duration.parts(complex)
 * console.log(complexParts)
 * // {
 * //   days: 1,
 * //   hours: 2,
 * //   minutes: 30,
 * //   seconds: 0,
 * //   millis: 0,
 * //   nanos: 0
 * // }
 * ```
 *
 * @category converting
 * @since 3.8.0
 */
export const parts = (self: Duration): {
  days: number
  hours: number
  minutes: number
  seconds: number
  millis: number
  nanos: number
} => {
  if (self.value._tag === "Infinity") {
    return {
      days: Infinity,
      hours: Infinity,
      minutes: Infinity,
      seconds: Infinity,
      millis: Infinity,
      nanos: Infinity
    }
  }
  if (self.value._tag === "NegativeInfinity") {
    return {
      days: -Infinity,
      hours: -Infinity,
      minutes: -Infinity,
      seconds: -Infinity,
      millis: -Infinity,
      nanos: -Infinity
    }
  }

  const n = toNanosUnsafe(self)
  const neg = n < bigint0
  const a = neg ? -n : n
  const ms = a / bigint1e6
  const sec = ms / bigint1e3
  const min = sec / bigint60
  const hr = min / bigint60
  const d = hr / bigint24
  const sign = neg ? -1 : 1

  return {
    days: sign * Number(d),
    hours: sign * Number(hr % bigint24),
    minutes: sign * Number(min % bigint60),
    seconds: sign * Number(sec % bigint60),
    millis: sign * Number(ms % bigint1e3),
    nanos: sign * Number(a % bigint1e6)
  }
}

/**
 * Converts a `Duration` to a human readable string.
 *
 * **Example** (Formatting durations)
 *
 * ```ts
 * import { Duration } from "effect"
 *
 * Duration.format(Duration.millis(1000)) // "1s"
 * Duration.format(Duration.millis(1001)) // "1s 1ms"
 * ```
 *
 * @category converting
 * @since 2.0.0
 */
export const format = (self: Duration): string => {
  if (self.value._tag === "Infinity") {
    return "Infinity"
  }
  if (self.value._tag === "NegativeInfinity") {
    return "-Infinity"
  }
  if (isZero(self)) {
    return "0"
  }
  if (isNegative(self)) {
    return "-" + format(abs(self))
  }

  const fragments = parts(self)
  const pieces = []
  if (fragments.days !== 0) {
    pieces.push(`${fragments.days}d`)
  }

  if (fragments.hours !== 0) {
    pieces.push(`${fragments.hours}h`)
  }

  if (fragments.minutes !== 0) {
    pieces.push(`${fragments.minutes}m`)
  }

  if (fragments.seconds !== 0) {
    pieces.push(`${fragments.seconds}s`)
  }

  if (fragments.millis !== 0) {
    pieces.push(`${fragments.millis}ms`)
  }

  if (fragments.nanos !== 0) {
    pieces.push(`${fragments.nanos}ns`)
  }

  return pieces.join(" ")
}

/**
 * Reducer for summing `Duration`s.
 *
 * **When to use**
 *
 * Use to sum many `Duration` values through APIs that consume a `Reducer`.
 *
 * **Details**
 *
 * `ReducerSum` uses `sum` and starts from `zero`, so `combineAll([])` returns
 * `zero`.
 *
 * @see {@link sum} for adding two duration values directly
 * @see {@link CombinerMax} for keeping the longest duration instead of summing
 * @see {@link CombinerMin} for keeping the shortest duration instead of summing
 *
 * @category math
 * @since 4.0.0
 */
export const ReducerSum: Reducer.Reducer<Duration> = Reducer.make(sum, zero)

/**
 * Combiner that returns the maximum `Duration`.
 *
 * **When to use**
 *
 * Use to keep the longest `Duration` when an API consumes a `Combiner`.
 *
 * @see {@link CombinerMin} for keeping the shortest `Duration`
 * @see {@link max} for comparing two `Duration` values directly
 *
 * @category math
 * @since 4.0.0
 */
export const CombinerMax: Combiner.Combiner<Duration> = Combiner.max(Order)

/**
 * Combiner that returns the minimum `Duration`.
 *
 * **When to use**
 *
 * Use to keep the shortest `Duration` through APIs that consume a `Combiner`.
 *
 * @see {@link CombinerMax} for keeping the longest `Duration`
 * @see {@link min} for comparing two `Duration` values directly
 *
 * @category math
 * @since 4.0.0
 */
export const CombinerMin: Combiner.Combiner<Duration> = Combiner.min(Order)
