///|
fn decimal_part(text : StringView) -> Int raise {
  if text.is_empty() || !text.iter().all(ch => ch >= '0' && ch <= '9') {
    raise Conversion("temporal digits")
  }
  @string.parse_int(text)
}

///|
fn month_days(year : Int, month : Int) -> Int raise {
  match month {
    1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
    4 | 6 | 9 | 11 => 30
    2 =>
      if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
        29
      } else {
        28
      }
    _ => raise Conversion("invalid month")
  }
}

///|
fn pg_date(text : String) -> PgDate raise {
  if text == "infinity" {
    return PosInfinity
  }
  if text == "-infinity" {
    return NegInfinity
  }
  let bc = text.has_suffix(" BC")
  let value = if bc { text[:text.length() - 3] } else { text[:] }
  let parts = value.split("-").to_array()
  if parts.length() != 3 || parts[1].length() != 2 || parts[2].length() != 2 {
    raise Conversion("date syntax")
  }
  let magnitude = decimal_part(parts[0])
  if magnitude == 0 {
    raise Conversion("PostgreSQL has no year zero")
  }
  let year = if bc { 1 - magnitude } else { magnitude }
  let month = decimal_part(parts[1])
  let day = decimal_part(parts[2])
  // Match the finite calendar range of the primary reference's chrono values.
  if year < -262143 ||
    year > 262142 ||
    day <= 0 ||
    day > month_days(year, month) {
    raise Conversion("date range")
  }
  Finite(year~, month~, day~)
}

///|
fn pg_time(text : String) -> PgTime raise {
  if text == "24:00:00" ||
    (
      text.has_prefix("24:00:00.") &&
      text.length() > 9 &&
      text[9:].iter().all(ch => ch == '0')
    ) {
    return { seconds: 86400, nanoseconds: 0, }
  }
  let parts = text.split(":").to_array()
  if parts.length() != 3 || parts[0].length() > 2 || parts[1].length() > 2 {
    raise Conversion("time syntax")
  }
  let seconds = parts[2].split(".").to_array()
  if seconds.length() > 2 || seconds[0].length() > 2 {
    raise Conversion("time seconds")
  }
  let hour = decimal_part(parts[0])
  let minute = decimal_part(parts[1])
  let second = decimal_part(seconds[0])
  let mut nanoseconds = 0
  if seconds.length() == 2 {
    let fraction = seconds[1]
    if fraction.is_empty() || !fraction.iter().all(ch => ch >= '0' && ch <= '9') {
      raise Conversion("time fraction")
    }
    nanoseconds = decimal_part(fraction[:Int::min(fraction.length(), 9)])
    for _ in fraction.length()..<9 {
      nanoseconds *= 10
    }
  }
  if hour >= 24 || minute > 59 || second > 60 {
    raise Conversion("time range")
  }
  {
    seconds: hour * 3600 + minute * 60 + Int::min(second, 59),
    nanoseconds: nanoseconds + (if second == 60 { 1000000000 } else { 0 }),
  }
}

///|
fn split_pg_offset(text : String) -> (String, Int) raise {
  let mut sign_at = text.length() - 1
  while sign_at >= 0 && text[sign_at] != '+' && text[sign_at] != '-' {
    sign_at -= 1
  }
  if sign_at <= 0 {
    raise Conversion("numeric UTC offset missing")
  }
  let value = text[sign_at + 1:]
  let parts = if value.contains(":") {
    value.split(":").to_array()
  } else {
    match value.length() {
      2 => [value]
      4 => [value[:2], value[2:]]
      6 => [value[:2], value[2:4], value[4:]]
      _ => raise Conversion("UTC offset shape")
    }
  }
  if parts.length() < 1 ||
    parts.length() > 3 ||
    parts.any(part => part.length() != 2) {
    raise Conversion("UTC offset shape")
  }
  let hour = decimal_part(parts[0])
  let minute = if parts.length() >= 2 { decimal_part(parts[1]) } else { 0 }
  let second = if parts.length() == 3 { decimal_part(parts[2]) } else { 0 }
  if hour >= 16 || minute >= 60 || second >= 60 {
    raise Conversion("UTC offset range")
  }
  let offset = hour * 3600 + minute * 60 + second
  (
    text[:sign_at].to_owned(),
    if text[sign_at] == '-' {
      -offset
    } else {
      offset
    },
  )
}

///|
fn shift_date(date : PgDate, delta : Int) -> PgDate raise {
  guard date is Finite(year~, month~, day~) else {
    raise Conversion("infinite local date")
  }
  let mut year = year
  let mut month = month
  let mut day = day + delta
  if day == 0 {
    month -= 1
    if month == 0 {
      month = 12
      year -= 1
    }
    day = month_days(year, month)
  } else if day > month_days(year, month) {
    day = 1
    month += 1
    if month == 13 {
      month = 1
      year += 1
    }
  }
  if year < -262143 || year > 262142 {
    raise Conversion("UTC timestamp calendar range")
  }
  Finite(year~, month~, day~)
}

///|
fn pg_timestamp(text : String, zoned : Bool) -> PgTimestamp raise {
  if text == "infinity" {
    return PosInfinity
  }
  if text == "-infinity" {
    return NegInfinity
  }
  let bc = text.has_suffix(" BC")
  let value = if bc { text[:text.length() - 3].to_owned() } else { text }
  let (clock_text, offset) = if zoned {
    split_pg_offset(value)
  } else {
    (value, 0)
  }
  let parts = clock_text.split(" ").to_array()
  if parts.length() != 2 {
    raise Conversion("timestamp syntax")
  }
  let date = pg_date(parts[0].to_owned() + (if bc { " BC" } else { "" }))
  let time = pg_time(parts[1].to_owned())
  if time.seconds == 86400 {
    raise Conversion("timestamp requires normalized date")
  }
  let seconds = time.seconds - offset
  let delta = if seconds < 0 { -1 } else if seconds >= 86400 { 1 } else { 0 }
  Finite(date=shift_date(date, delta), time={
    seconds: seconds - delta * 86400,
    nanoseconds: time.nanoseconds,
  })
}