// Minimal RFC 3339 date-time handling for the security.txt `Expires` field.
// Parses/renders the RFC 3339 `date-time` production required by RFC 9116
// plus epoch arithmetic for freshness checks. No tz database, no civil math
// beyond day conversion.

///|
/// A civil date-time with a UTC offset (0 for `Z`); epoch math uses the UTC instant.
pub struct DateTime {
  year : Int
  month : Int
  day : Int
  hour : Int
  minute : Int
  second : Int
  nanos : Int64
  offset_minutes : Int
} derive(Eq, Debug)

///|
/// Maximal fractional digits accepted (nanosecond precision).
pub const RFC3339_MAX_FRACTIONAL_DIGITS : Int = 9

///|
/// Make a UTC date-time without fractional seconds. Range-checked.
pub fn make_utc(
  year : Int,
  month : Int,
  day : Int,
  hour : Int,
  minute : Int,
  second : Int,
) -> Result[DateTime, SecurityTxtError] {
  make_utc_nanos(year, month, day, hour, minute, second, 0L)
}

///|
/// Make a UTC date-time with fractional seconds as nanoseconds.
pub fn make_utc_nanos(
  year : Int,
  month : Int,
  day : Int,
  hour : Int,
  minute : Int,
  second : Int,
  nanos : Int64,
) -> Result[DateTime, SecurityTxtError] {
  if year < 0 || year > 9999 {
    return Err(invalid_datetime(0, "year must be between 0 and 9999"))
  }
  if month < 1 || month > 12 {
    return Err(invalid_datetime(0, "month must be between 1 and 12"))
  }
  if day < 1 || day > days_in_month(year, month) {
    return Err(invalid_datetime(0, "day is out of range for the given month"))
  }
  if hour < 0 || hour > 23 {
    return Err(invalid_datetime(0, "hour must be between 0 and 23"))
  }
  if minute < 0 || minute > 59 {
    return Err(invalid_datetime(0, "minute must be between 0 and 59"))
  }
  if second < 0 || second > 60 {
    return Err(invalid_datetime(0, "second must be between 0 and 60"))
  }
  if nanos < 0L || nanos >= 1_000_000_000L {
    return Err(invalid_datetime(0, "nanos must be between 0 and 999999999"))
  }
  Ok({ year, month, day, hour, minute, second, nanos, offset_minutes: 0 })
}

///|
/// Year component.
pub fn DateTime::year(self : DateTime) -> Int {
  self.year
}

///|
/// Month component (1-12).
pub fn DateTime::month(self : DateTime) -> Int {
  self.month
}

///|
/// Day component (1-31).
pub fn DateTime::day(self : DateTime) -> Int {
  self.day
}

///|
/// Hour component (0-23).
pub fn DateTime::hour(self : DateTime) -> Int {
  self.hour
}

///|
/// Minute component (0-59).
pub fn DateTime::minute(self : DateTime) -> Int {
  self.minute
}

///|
/// Second component (0-60; 60 is the leap second).
pub fn DateTime::second(self : DateTime) -> Int {
  self.second
}

///|
/// Fractional seconds in nanoseconds (0-999999999).
pub fn DateTime::nanos(self : DateTime) -> Int64 {
  self.nanos
}

///|
/// UTC offset in minutes (0 for `Z`).
pub fn DateTime::offset_minutes(self : DateTime) -> Int {
  self.offset_minutes
}

///|
/// This instant as nanoseconds since the Unix epoch (1970-01-01T00:00:00Z).
pub fn DateTime::to_epoch_nanos(self : DateTime) -> Int64 {
  let days = days_from_civil(self.year, self.month, self.day)
  let day_seconds = self.hour.to_int64() * 3600L +
    self.minute.to_int64() * 60L +
    self.second.to_int64() -
    self.offset_minutes.to_int64() * 60L
  days * 86_400L * 1_000_000_000L + day_seconds * 1_000_000_000L + self.nanos
}

///|
/// True when this instant is strictly before `other`.
pub fn DateTime::before(self : DateTime, other : DateTime) -> Bool {
  self.to_epoch_nanos() < other.to_epoch_nanos()
}

///|
/// True when this instant is strictly after `other`.
pub fn DateTime::after(self : DateTime, other : DateTime) -> Bool {
  self.to_epoch_nanos() > other.to_epoch_nanos()
}

///|
/// True when both instants coincide.
pub fn DateTime::equals(self : DateTime, other : DateTime) -> Bool {
  self.to_epoch_nanos() == other.to_epoch_nanos()
}

///|
/// True when this instant has passed relative to `now`.
pub fn is_expired(expires : DateTime, now : DateTime) -> Bool {
  expires.to_epoch_nanos() < now.to_epoch_nanos()
}

///|
/// Seconds from `now` until expiry; negative when already expired.
pub fn time_until_expiry(expires : DateTime, now : DateTime) -> Int64 {
  div_floor_i64(expires.to_epoch_nanos() - now.to_epoch_nanos(), 1_000_000_000L)
}

///|
/// Floor division for `Int64` (truncation breaks negative durations).
pub fn div_floor_i64(a : Int64, b : Int64) -> Int64 {
  let q = a / b
  if a % b != 0L && (a < 0L) != (b < 0L) {
    q - 1L
  } else {
    q
  }
}

///|
/// Slice `s[start:end]` as a String (avoids the deprecated `String::substring`).
fn slice(s : String, start : Int, end : Int) -> String {
  s[start:end].to_owned()
}

///|
/// Format as canonical RFC 3339: date, time, optional fraction, `Z` or ±HH:MM.
pub fn DateTime::format_rfc3339(self : DateTime) -> String {
  let sb = StringBuilder::new(size_hint=32)
  sb.write_string(pad4(self.year))
  sb.write_char('-')
  sb.write_string(pad2(self.month))
  sb.write_char('-')
  sb.write_string(pad2(self.day))
  sb.write_char('T')
  sb.write_string(pad2(self.hour))
  sb.write_char(':')
  sb.write_string(pad2(self.minute))
  sb.write_char(':')
  sb.write_string(pad2(self.second))
  if self.nanos != 0L {
    let frac = self.nanos.to_string()
    let mut padded = frac
    let mut _pad_i = frac.length()
    while _pad_i < RFC3339_MAX_FRACTIONAL_DIGITS {
      padded = "0\{padded}"
      _pad_i += 1
    }
    let mut trimmed = padded
    while trimmed.has_suffix("0") {
      trimmed = slice(trimmed, 0, trimmed.length() - 1)
    }
    sb.write_char('.')
    sb.write_string(trimmed)
  }
  if self.offset_minutes == 0 {
    sb.write_char('Z')
  } else {
    let sign = if self.offset_minutes < 0 { '-' } else { '+' }
    let total = if self.offset_minutes < 0 {
      -self.offset_minutes
    } else {
      self.offset_minutes
    }
    sb.write_char(sign)
    sb.write_string(pad2(total / 60))
    sb.write_char(':')
    sb.write_string(pad2(total % 60))
  }
  sb.to_string()
}

///|
/// Parse the RFC 3339 `date-time` production used by RFC 9116.
pub fn parse_rfc3339(value : String) -> Result[DateTime, SecurityTxtError] {
  Ok(parse_rfc3339_inner(value)) catch {
    e => Err(unwrap_security_txt_error(e))
  }
}

///|
/// Raise-based core of `parse_rfc3339`.
fn parse_rfc3339_inner(value : String) -> DateTime raise {
  let len = value.length()
  if len < 20 {
    raise invalid_datetime(0, "value too short for an RFC 3339 date-time")
  }
  // Full-date: YYYY-MM-DD
  let year = parse_digits(value, 0, 4, "year")
  expect_char(value, 4, '-')
  let month = parse_digits(value, 5, 2, "month")
  expect_char(value, 7, '-')
  let day = parse_digits(value, 8, 2, "day")
  if month < 1 || month > 12 {
    raise invalid_datetime(5, "month must be between 01 and 12")
  }
  if day < 1 || day > days_in_month(year, month) {
    raise invalid_datetime(8, "day is out of range for the given month")
  }
  // RFC 3339 permits lower-case `t`, but RFC 9116 imports the date-time
  // production rather than the prose-only readability substitution of SP.
  let sep = char_at(value, 10)
  if sep != 'T' && sep != 't' {
    raise invalid_datetime(10, "expected 'T' between date and time")
  }
  if len < 19 {
    raise invalid_datetime(10, "value too short for a full-time")
  }
  let hour = parse_digits(value, 11, 2, "hour")
  expect_char(value, 13, ':')
  let minute = parse_digits(value, 14, 2, "minute")
  expect_char(value, 16, ':')
  let second = parse_digits(value, 17, 2, "second")
  if hour > 23 {
    raise invalid_datetime(11, "hour must be between 00 and 23")
  }
  if minute > 59 {
    raise invalid_datetime(14, "minute must be between 00 and 59")
  }
  if second > 60 {
    raise invalid_datetime(17, "second must be between 00 and 60")
  }
  let mut pos = 19
  // Optional fractional seconds.
  let mut nanos = 0L
  if pos < len && char_at(value, pos) == '.' {
    pos += 1
    let frac_start = pos
    let mut digits = 0
    while pos < len && char_at(value, pos).is_digit(10) {
      digits += 1
      pos += 1
    }
    if digits == 0 {
      raise invalid_datetime(
        frac_start, "expected digits after the decimal point",
      )
    }
    if digits > RFC3339_MAX_FRACTIONAL_DIGITS {
      raise invalid_datetime(
        frac_start, "more than 9 fractional digits are not supported",
      )
    }
    let mut scaled = 0L
    let mut _f = frac_start
    while _f < frac_start + digits {
      scaled = scaled * 10L + (char_at(value, _f).to_int() - 48).to_int64()
      _f += 1
    }
    let mut mult = 1_000_000_000L
    let mut _scale_i = 0
    while _scale_i < digits {
      mult /= 10L
      _scale_i += 1
    }
    nanos = scaled * mult
  }
  // Time offset: Z / z / ±HH:MM.
  if pos >= len {
    raise invalid_datetime(pos, "missing time offset (expected Z or ±HH:MM)")
  }
  let offset_sign = match char_at(value, pos) {
    'Z' | 'z' => {
      pos += 1
      0
    }
    '+' => {
      pos += 1
      1
    }
    '-' => {
      pos += 1
      -1
    }
    _ => raise invalid_datetime(pos, "expected time offset (Z or ±HH:MM)")
  }
  let mut offset_minutes = 0
  if offset_sign != 0 {
    if len < pos + 5 {
      raise invalid_datetime(pos, "numeric offset must be ±HH:MM")
    }
    let oh = parse_digits(value, pos, 2, "offset hour")
    expect_char(value, pos + 2, ':')
    let om = parse_digits(value, pos + 3, 2, "offset minute")
    if oh > 23 {
      raise invalid_datetime(pos, "offset hour must be between 00 and 23")
    }
    if om > 59 {
      raise invalid_datetime(pos + 3, "offset minute must be between 00 and 59")
    }
    offset_minutes = offset_sign * (oh * 60 + om)
    pos += 5
  }
  if pos != len {
    raise invalid_datetime(pos, "unexpected trailing characters")
  }
  { year, month, day, hour, minute, second, nanos, offset_minutes }
}

///|
fn invalid_datetime(offset : Int, message : String) -> SecurityTxtError {
  security_txt_error(DateTime, InvalidDateTime, 0, offset + 1, offset, message)
}

///|
/// Parse `length` ASCII digits at `start` into an Int.
fn parse_digits(
  value : String,
  start : Int,
  length : Int,
  what : String,
) -> Int raise {
  let mut n = 0
  let mut i = start
  while i < start + length {
    let c = char_at(value, i)
    if c.is_digit(10) {
      n = n * 10 + (c.to_int() - 48)
    } else {
      raise invalid_datetime(i, "\{what} must be ASCII digits")
    }
    i += 1
  }
  n
}

///|
fn expect_char(value : String, at : Int, expected : Char) -> Unit raise {
  let c = char_at(value, at)
  if c != expected {
    raise invalid_datetime(at, "expected '\{expected}'")
  }
}

///|
fn char_at(value : String, at : Int) -> Char raise {
  if at >= value.length() {
    raise invalid_datetime(at, "unexpected end of value")
  }
  let mut i = 0
  for c in value {
    if i == at {
      return c
    }
    i += 1
  }
  raise invalid_datetime(at, "unexpected end of value")
}

///|
fn pad2(n : Int) -> String {
  if n < 10 {
    "0\{n}"
  } else {
    n.to_string()
  }
}

///|
fn pad4(n : Int) -> String {
  if n < 10 {
    "000\{n}"
  } else if n < 100 {
    "00\{n}"
  } else if n < 1000 {
    "0\{n}"
  } else {
    n.to_string()
  }
}

///|
/// Days in the given month, respecting leap years (proleptic Gregorian).
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
  }
}

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

///|
/// Days from 1970-01-01 to the civil date (Howard Hinnant's `days_from_civil`).
pub fn days_from_civil(year : Int, month : Int, day : Int) -> Int64 {
  let y = if month <= 2 { year - 1 } else { year }
  let era = (if y >= 0 { y } else { y - 399 }) / 400
  let yoe = y - era * 400
  let mp = (month + 9) % 12
  let doy = (153 * mp + 2) / 5 + day - 1
  let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
  era.to_int64() * 146097L + doe.to_int64() - 719468L
}