///|
fn parse_decimal(text : StringView) -> Int? {
  if text.is_empty() {
    return None
  }
  let mut value = 0
  let mut valid = true
  text
  .iter()
  .each(char => {
    if char >= '0' && char <= '9' {
      value = value * 10 + (char.to_int() - '0'.to_int())
    } else {
      valid = false
    }
  })
  if valid {
    Some(value)
  } else {
    None
  }
}

///|
pub fn is_leap_year(year : Int) -> Bool {
  (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

///|
pub fn days_in_month(year : Int, month : Int) -> Int {
  match month {
    1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
    4 | 6 | 9 | 11 => 30
    2 => if is_leap_year(year) { 29 } else { 28 }
    _ => 0
  }
}

///|
fn validate_date(year : Int, month : Int, day : Int) -> Bool {
  month >= 1 && month <= 12 && day >= 1 && day <= days_in_month(year, month)
}

///|
fn validate_time(hour : Int, minute : Int, second : Int) -> Bool {
  hour >= 0 &&
  hour <= 23 &&
  minute >= 0 &&
  minute <= 59 &&
  second >= 0 &&
  second <= 59
}

///|
fn time_error(
  kind : SatErrorKind,
  fragment : StringView,
  message : String,
) -> SatError {
  SatError::new(kind, message, fragment=fragment.to_owned())
}

///|
pub fn parse_utc_datetime(text : String) -> Result[UtcDateTime, SatError] {
  let trimmed = text.trim()
  if trimmed.is_empty() {
    return Err(time_error(EmptyInput, text, "timestamp must not be empty"))
  }
  let without_z = trimmed.split("Z").to_array()[0]
  let date_time_parts = without_z
    .split("T")
    .filter(part => !part.is_empty())
    .to_array()
  let parts = if date_time_parts.length() == 2 {
    date_time_parts
  } else {
    without_z.split(" ").filter(part => !part.is_empty()).to_array()
  }
  if parts.length() != 2 {
    return Err(
      time_error(
        InvalidDateTime,
        text,
        "timestamp must use YYYY-MM-DDTHH:MM[:SS] format",
      ),
    )
  }
  let date_parts = parts[0].split("-").to_array()
  let time_parts = parts[1].split(":").to_array()
  if date_parts.length() != 3 {
    return Err(
      time_error(
        InvalidDateTime,
        parts[0],
        "timestamp date must use YYYY-MM-DD format",
      ),
    )
  }
  if time_parts.length() < 2 || time_parts.length() > 3 {
    return Err(
      time_error(
        InvalidDateTime,
        parts[1],
        "timestamp time must use HH:MM or HH:MM:SS format",
      ),
    )
  }
  let year = match parse_decimal(date_parts[0]) {
    Some(value) => value
    None =>
      return Err(
        time_error(
          InvalidDateTime,
          date_parts[0],
          "year must be a non-negative integer",
        ),
      )
  }
  let month = match parse_decimal(date_parts[1]) {
    Some(value) => value
    None =>
      return Err(
        time_error(
          InvalidDateTime,
          date_parts[1],
          "month must be a non-negative integer",
        ),
      )
  }
  let day = match parse_decimal(date_parts[2]) {
    Some(value) => value
    None =>
      return Err(
        time_error(
          InvalidDateTime,
          date_parts[2],
          "day must be a non-negative integer",
        ),
      )
  }
  let hour = match parse_decimal(time_parts[0]) {
    Some(value) => value
    None =>
      return Err(
        time_error(
          InvalidDateTime,
          time_parts[0],
          "hour must be a non-negative integer",
        ),
      )
  }
  let minute = match parse_decimal(time_parts[1]) {
    Some(value) => value
    None =>
      return Err(
        time_error(
          InvalidDateTime,
          time_parts[1],
          "minute must be a non-negative integer",
        ),
      )
  }
  let second = if time_parts.length() == 3 {
    match parse_decimal(time_parts[2]) {
      Some(value) => value
      None =>
        return Err(
          time_error(
            InvalidDateTime,
            time_parts[2],
            "second must be a non-negative integer",
          ),
        )
    }
  } else {
    0
  }
  if !validate_date(year, month, day) {
    return Err(
      time_error(OutOfRange, parts[0], "timestamp date is out of range"),
    )
  }
  if !validate_time(hour, minute, second) {
    return Err(
      time_error(OutOfRange, parts[1], "timestamp time is out of range"),
    )
  }
  Ok({ year, month, day, hour, minute, second })
}

///|
/// Add an integral number of seconds to a UTC civil timestamp.
///
/// This helper is intentionally calendar-based, so pass-search event times
/// remain integer-second UTC values without relying on a platform date API.
pub fn UtcDateTime::add_seconds(
  self : UtcDateTime,
  seconds : Int,
) -> UtcDateTime {
  let mut total_seconds = self.hour * 3600 +
    self.minute * 60 +
    self.second +
    seconds
  let mut day_shift = 0
  while total_seconds >= 86400 {
    total_seconds = total_seconds - 86400
    day_shift = day_shift + 1
  }
  while total_seconds < 0 {
    total_seconds = total_seconds + 86400
    day_shift = day_shift - 1
  }
  let mut year = self.year
  let mut month = self.month
  let mut day = self.day
  while day_shift > 0 {
    day_shift = day_shift - 1
    day = day + 1
    if day > days_in_month(year, month) {
      day = 1
      if month == 12 {
        month = 1
        year = year + 1
      } else {
        month = month + 1
      }
    }
  }
  while day_shift < 0 {
    day_shift = day_shift + 1
    day = day - 1
    if day == 0 {
      if month == 1 {
        month = 12
        year = year - 1
      } else {
        month = month - 1
      }
      day = days_in_month(year, month)
    }
  }
  {
    year,
    month,
    day,
    hour: total_seconds / 3600,
    minute: total_seconds % 3600 / 60,
    second: total_seconds % 60,
  }
}

///|
pub fn UtcDateTime::next_second(self : UtcDateTime) -> UtcDateTime {
  if self.second < 59 {
    {
      year: self.year,
      month: self.month,
      day: self.day,
      hour: self.hour,
      minute: self.minute,
      second: self.second + 1,
    }
  } else if self.minute < 59 {
    {
      year: self.year,
      month: self.month,
      day: self.day,
      hour: self.hour,
      minute: self.minute + 1,
      second: 0,
    }
  } else if self.hour < 23 {
    {
      year: self.year,
      month: self.month,
      day: self.day,
      hour: self.hour + 1,
      minute: 0,
      second: 0,
    }
  } else {
    let next_day = self.day + 1
    let limit = days_in_month(self.year, self.month)
    if next_day <= limit {
      {
        year: self.year,
        month: self.month,
        day: next_day,
        hour: 0,
        minute: 0,
        second: 0,
      }
    } else if self.month < 12 {
      {
        year: self.year,
        month: self.month + 1,
        day: 1,
        hour: 0,
        minute: 0,
        second: 0,
      }
    } else {
      { year: self.year + 1, month: 1, day: 1, hour: 0, minute: 0, second: 0 }
    }
  }
}

///|
/// Convert a UTC civil timestamp to the astronomical Julian Day.
///
/// The result follows the standard convention where 2000-01-01 12:00:00 UTC
/// is 2451545.0. This helper uses the proleptic Gregorian calendar.
pub fn julian_day(value : UtcDateTime) -> Double {
  let month_adjust = (14 - value.month) / 12
  let adjusted_year = value.year + 4800 - month_adjust
  let adjusted_month = value.month + 12 * month_adjust - 3
  let julian_day_number = value.day +
    (153 * adjusted_month + 2) / 5 +
    365 * adjusted_year +
    adjusted_year / 4 -
    adjusted_year / 100 +
    adjusted_year / 400 -
    32045
  let fraction = (
      value.hour.to_double() * 3600.0 +
      value.minute.to_double() * 60.0 +
      value.second.to_double()
    ) /
    86400.0
  julian_day_number.to_double() - 0.5 + fraction
}

///|
/// Return the elapsed seconds from `start` to `end`.
pub fn seconds_between(start : UtcDateTime, end : UtcDateTime) -> Double {
  (julian_day(end) - julian_day(start)) * 86400.0
}