/// One millisecond.

///|
pub let millisecond : Duration = Duration(1L)

/// One second (1,000 ms).

///|
pub let second : Duration = Duration(1_000L)

/// One minute (60,000 ms).

///|
pub let minute : Duration = Duration(60_000L)

/// One hour (3,600,000 ms).

///|
pub let hour : Duration = Duration(3_600_000L)

/// One day (86,400,000 ms).

///|
pub let day : Duration = Duration(86_400_000L)

/// One week (604,800,000 ms).

///|
pub let week : Duration = Duration(604_800_000L)

/// Adds two durations.

///|
pub impl Add for Duration with fn add(a, b) {
  Duration(a.0 + b.0)
}

/// Subtracts one duration from another.

///|
pub impl Sub for Duration with fn sub(a, b) {
  Duration(a.0 - b.0)
}

/// Negates a duration, reversing its direction.

///|
pub impl Neg for Duration with fn neg(self) {
  Duration(-self.0)
}

/// Multiplies a duration by a scalar value.
///
/// Parameters:
///
/// * `self` : The duration to scale.
/// * `n` : The multiplier.
///
/// Returns a new `Duration` whose millisecond value is `self * n`.
///
/// Note: This function does not perform overflow checking. For extremely
/// large values, the result may silently wrap around.

///|
pub fn Duration::scale(self : Duration, n : Int64) -> Duration {
  Duration(self.0 * n)
}

/// Adds a duration to an epoch time, producing a new epoch time.
///
/// Parameters:
///
/// * `self` : The base epoch time.
/// * `d` : The duration to add (may be negative to subtract).
///
/// Returns a new `EpochTime` offset by `d` milliseconds.
///
/// Note: This function does not perform overflow checking. For extremely
/// large values, the result may silently wrap around. The parse functions
/// (`parse_duration`, `parse_timespec`, etc.) use internal overflow-checked
/// arithmetic and are safe.

///|
pub fn EpochTime::add_duration(self : EpochTime, d : Duration) -> EpochTime {
  EpochTime(self.0 + d.0)
}

/// Adds two `Int64` values with overflow detection.
/// Raises `ParseError` with the given message if overflow occurs.

///|
fn checked_add(
  a : Int64,
  b : Int64,
  msg? : String = "overflow",
) -> Int64 raise ParseError {
  let result = a + b
  // Overflow detection: sign has flipped
  if (a > 0L && b > 0L && result < 0L) || (a < 0L && b < 0L && result > 0L) {
    raise ParseError(msg)
  }
  result
}

/// Multiplies two `Int64` values with overflow detection.
/// Raises `ParseError` with the given message if overflow occurs.

///|
fn checked_mul(
  a : Int64,
  b : Int64,
  msg? : String = "overflow",
) -> Int64 raise ParseError {
  if a == 0L || b == 0L {
    return 0L
  }
  // Guard against Int64::min_value / -1 trap:
  // -1 * Int64::min_value overflows, and the subsequent result / a division
  // triggers a WASM trap (integer overflow) instead of producing a wrong result.
  let min_value = -9223372036854775807L - 1L
  if (a == -1L && b == min_value) || (b == -1L && a == min_value) {
    raise ParseError(msg)
  }
  let result = a * b
  if result / a != b {
    raise ParseError(msg)
  }
  result
}

/// Parses a duration string into a `Duration`.
///
/// Supports integers, decimals, underscore separators, and compound expressions.
/// Units: `w`/`week(s)`, `d`/`day(s)`, `h`/`hour(s)`, `m`/`min`/`minute(s)`,
/// `s`/`sec`/`second(s)`, `ms`/`millisecond(s)`.
/// The `ago` modifier reverses the sign of the preceding segment group.
///
/// Parameters:
///
/// * `input` : The duration string to parse (e.g. `"5m"`, `"1h30m"`, `"3_600_000ms"`,
///   `"1.5h"`, `"5 minutes ago"`).
/// * `default_sign` : How to interpret input without an explicit `+`/`-` sign.
///   Defaults to `Plus`.
///
/// Returns a `Duration` in milliseconds.
///
/// Raises `ParseError` if the input is empty, contains unknown units, or overflows.

///|
pub fn parse_duration(
  input : String,
  default_sign? : Sign = Plus,
) -> Duration raise ParseError {
  let s = input.trim()
  if s.length() == 0 {
    raise ParseError("empty duration string")
  }
  let mut pos = 0
  // Determine the leading sign
  let negate : Bool = match s.get_char(pos) {
    Some('+') => {
      pos += 1
      false
    }
    Some('-') => {
      pos += 1
      true
    }
    _ =>
      match default_sign {
        Minus => true
        Plus => false
        Reject => raise ParseError("missing sign in duration: " + input)
      }
  }
  // Skip spaces
  pos = skip_spaces(s, pos)
  // Group-based parsing
  // A group = consecutive segments delimited by explicit +/-
  // "ago" reverses the entire group and starts a new group
  let mut total_ms = 0L
  let mut group_ms = 0L
  let mut group_sign = 1L
  let mut parsed_any = false
  while pos < s.length() {
    // Skip spaces
    pos = skip_spaces(s, pos)
    if pos >= s.length() {
      break
    }
    // Delimit groups by explicit +/-
    let saved_pos = pos
    let mut new_group = false
    match s.get_char(pos) {
      Some('+') => {
        total_ms = checked_add(
          total_ms,
          checked_mul(group_sign, group_ms, msg="duration overflow"),
          msg="duration overflow",
        )
        group_ms = 0L
        group_sign = 1L
        pos += 1
        new_group = true
      }
      Some('-') => {
        total_ms = checked_add(
          total_ms,
          checked_mul(group_sign, group_ms, msg="duration overflow"),
          msg="duration overflow",
        )
        group_ms = 0L
        group_sign = -1L
        pos += 1
        new_group = true
      }
      _ => ()
    }
    pos = skip_spaces(s, pos)
    if pos >= s.length() {
      if new_group {
        pos = saved_pos
      }
      break
    }
    // If not a digit, check for "ago"
    match s.get_char(pos) {
      Some(ch) =>
        if !ch.is_ascii_digit() {
          // Check for "ago" modifier
          if try_match_word(s, pos, "ago") {
            group_ms = -group_ms
            total_ms = checked_add(
              total_ms,
              checked_mul(group_sign, group_ms, msg="duration overflow"),
              msg="duration overflow",
            )
            group_ms = 0L
            group_sign = 1L
            pos += 3
            continue
          }
          if new_group {
            pos = saved_pos // Undo sign consumption
          }
          break
        }
      None => {
        if new_group {
          pos = saved_pos
        }
        break
      }
    }
    let (integer_part, fractional_part, new_pos) = parse_number(s, pos)
    pos = new_pos
    pos = skip_spaces(s, pos)
    // Read the unit
    let (ms_mul, unit_end) = parse_unit(s, pos)
    if unit_end == pos {
      raise ParseError("missing unit after number in: " + input)
    }
    pos = unit_end
    // Compute the integer part
    let mut seg_ms = checked_mul(integer_part, ms_mul, msg="duration overflow")
    // Fractional part (convert to ms)
    if fractional_part > 0.0 {
      let frac_ms = (fractional_part * ms_mul.to_double()).to_int64()
      seg_ms = checked_add(seg_ms, frac_ms, msg="duration overflow")
    }
    // Accumulate into the current group
    group_ms = checked_add(group_ms, seg_ms, msg="duration overflow")
    parsed_any = true
    // Check for "ago" immediately after the unit
    pos = skip_spaces(s, pos)
    if try_match_word(s, pos, "ago") {
      group_ms = -group_ms
      total_ms = checked_add(
        total_ms,
        checked_mul(group_sign, group_ms, msg="duration overflow"),
        msg="duration overflow",
      )
      group_ms = 0L
      group_sign = 1L
      pos += 3
    }
  }
  if !parsed_any {
    raise ParseError("no duration segments found in: " + input)
  }
  // Flush the remaining group
  total_ms = checked_add(
    total_ms,
    checked_mul(group_sign, group_ms, msg="duration overflow"),
    msg="duration overflow",
  )
  // Error if there are trailing characters
  pos = skip_spaces(s, pos)
  if pos < s.length() {
    raise ParseError("unexpected characters after duration: " + input)
  }
  // Apply the overall sign (from leading +/- or default_sign)
  if negate {
    total_ms = -total_ms
  }
  Duration(total_ms)
}

/// Skips whitespace characters (space, tab) starting from `start`.
/// Returns the position of the first non-whitespace character.

///|
fn skip_spaces(s : StringView, start : Int) -> Int {
  let mut pos = start
  while pos < s.length() {
    match s.get_char(pos) {
      Some(' ') | Some('\t') => pos += 1
      _ => break
    }
  }
  pos
}

/// Parses a numeric value with optional fractional part and underscore separators.
///
/// Returns a tuple of `(integer_part, fractional_part, new_pos)`:
/// - `integer_part` : The whole number portion as `Int64`.
/// - `fractional_part` : The decimal portion as `Double` (0.0 if absent).
/// - `new_pos` : The position immediately after the parsed number.

///|
fn parse_number(
  s : StringView,
  start : Int,
) -> (Int64, Double, Int) raise ParseError {
  let mut pos = start
  let mut integer_part = 0L
  // Integer part
  let max_div = 922_337_203_685_477_580L // Int64::max_value / 10
  let max_mod = 7 // Int64::max_value % 10
  while pos < s.length() {
    match s.get_char(pos) {
      Some('_') => pos += 1
      Some(c) =>
        if c.is_ascii_digit() {
          let digit = c.to_int() - '0'.to_int()
          if integer_part > max_div ||
            (integer_part == max_div && digit > max_mod) {
            raise ParseError("number too large")
          }
          integer_part = integer_part * 10L + digit.to_int64()
          pos += 1
        } else {
          break
        }
      None => break
    }
  }
  // Fractional part
  let mut frac_part = 0.0
  match s.get_char(pos) {
    Some('.') => {
      pos += 1
      let mut frac_digits = 0
      while pos < s.length() {
        match s.get_char(pos) {
          Some('_') => pos += 1
          Some(c) =>
            if c.is_ascii_digit() {
              frac_digits += 1
              frac_part = frac_part * 10.0 +
                (c.to_int() - '0'.to_int()).to_double()
              pos += 1
            } else {
              break
            }
          None => break
        }
      }
      // scale fractional part
      let mut scale = 1.0
      for _i in 0.. ()
  }
  (integer_part, frac_part, pos)
}

/// Checks if `s[pos..]` starts with `word` and is followed by a non-alphabetic
/// character or EOF (to prevent partial matches like "min" matching "minutes").

///|
fn try_match_word(s : StringView, pos : Int, word : String) -> Bool {
  let wlen = word.length()
  if pos + wlen > s.length() {
    return false
  }
  for i in 0.. if a != b { return false }
      _ => return false
    }
  }
  // Must be followed by a non-alphabetic character or EOF (prevent partial matches)
  match s.get_char(pos + wlen) {
    Some(c) => !(c.is_ascii_lowercase() || c.is_ascii_uppercase())
    None => true
  }
}

/// Reads a duration unit starting at `pos` and returns its millisecond multiplier.
///
/// Returns a tuple of `(ms_multiplier, end_pos)`:
/// - `ms_multiplier` : The number of milliseconds per one unit (e.g. 3,600,000 for `h`).
/// - `end_pos` : The position immediately after the consumed unit string.
///
/// Raises `ParseError` for unknown or sub-millisecond units (`us`, `ns`).

///|
fn parse_unit(s : StringView, pos : Int) -> (Int64, Int) raise ParseError {
  match s.get_char(pos) {
    Some('w') =>
      match s.get_char(pos + 1) {
        Some('e') =>
          if try_match_word(s, pos, "weeks") {
            (week.0, pos + 5)
          } else if try_match_word(s, pos, "week") {
            (week.0, pos + 4)
          } else {
            raise ParseError(
              "unknown unit at position " +
              pos.to_string() +
              " in: " +
              s.to_owned(),
            )
          }
        _ => (week.0, pos + 1) // 'w'
      }
    Some('d') =>
      match s.get_char(pos + 1) {
        Some('a') =>
          if try_match_word(s, pos, "days") {
            (day.0, pos + 4)
          } else if try_match_word(s, pos, "day") {
            (day.0, pos + 3)
          } else {
            raise ParseError(
              "unknown unit at position " +
              pos.to_string() +
              " in: " +
              s.to_owned(),
            )
          }
        _ => (day.0, pos + 1) // 'd'
      }
    Some('h') =>
      match s.get_char(pos + 1) {
        Some('o') =>
          if try_match_word(s, pos, "hours") {
            (hour.0, pos + 5)
          } else if try_match_word(s, pos, "hour") {
            (hour.0, pos + 4)
          } else {
            raise ParseError(
              "unknown unit at position " +
              pos.to_string() +
              " in: " +
              s.to_owned(),
            )
          }
        _ => (hour.0, pos + 1) // 'h'
      }
    Some('m') =>
      match s.get_char(pos + 1) {
        Some('s') => (1L, pos + 2) // 'ms'
        Some('i') =>
          if try_match_word(s, pos, "minutes") {
            (minute.0, pos + 7)
          } else if try_match_word(s, pos, "minute") {
            (minute.0, pos + 6)
          } else if try_match_word(s, pos, "min") {
            (minute.0, pos + 3)
          } else if try_match_word(s, pos, "milliseconds") {
            (1L, pos + 12)
          } else if try_match_word(s, pos, "millisecond") {
            (1L, pos + 11)
          } else {
            raise ParseError(
              "unknown unit at position " +
              pos.to_string() +
              " in: " +
              s.to_owned(),
            )
          }
        _ => (minute.0, pos + 1) // 'm'
      }
    Some('s') =>
      match s.get_char(pos + 1) {
        Some('e') =>
          if try_match_word(s, pos, "seconds") {
            (second.0, pos + 7)
          } else if try_match_word(s, pos, "second") {
            (second.0, pos + 6)
          } else if try_match_word(s, pos, "sec") {
            (second.0, pos + 3)
          } else {
            raise ParseError(
              "unknown unit at position " +
              pos.to_string() +
              " in: " +
              s.to_owned(),
            )
          }
        _ => (second.0, pos + 1) // 's'
      }
    Some('u') =>
      match s.get_char(pos + 1) {
        Some('s') =>
          raise ParseError("sub-millisecond unit 'us' is not supported")
        _ =>
          raise ParseError(
            "unknown unit 'u' at position " +
            pos.to_string() +
            " (did you mean 'us'? Note: sub-ms units are not supported)",
          )
      }
    Some('\u03BC') =>
      // μ (U+03BC)
      match s.get_char(pos + 1) {
        Some('s') =>
          raise ParseError("sub-millisecond unit '\u03BCs' is not supported")
        _ =>
          raise ParseError(
            "unknown unit '\u03BC' at position " +
            pos.to_string() +
            " (did you mean '\u03BCs'? Note: sub-ms units are not supported)",
          )
      }
    Some('n') =>
      match s.get_char(pos + 1) {
        Some('s') =>
          raise ParseError("sub-millisecond unit 'ns' is not supported")
        _ =>
          raise ParseError(
            "unknown unit 'n' at position " +
            pos.to_string() +
            " (did you mean 'ns'? Note: sub-ms units are not supported)",
          )
      }
    Some(c) =>
      raise ParseError(
        "unknown unit '" + c.to_string() + "' at position " + pos.to_string(),
      )
    None => (0L, pos) // no unit found
  }
}