// Datetime -- an RFC 3339 timestamp, the `createdAt` on every record and the
// `indexedAt` on every view.
//
// https://atproto.com/specs/lexicon#datetime
// Ported from @atproto/syntax packages/syntax/src/datetime.ts.
//
// The value keeps the ORIGINAL STRING and re-serializes it unchanged. That is
// not laziness, it is a correctness requirement: a record's CID is the hash of
// its DAG-CBOR encoding, so a client that reads a record, changes one field and
// writes it back must not also rewrite `createdAt` into its own preferred
// precision. rsky stores `chrono::DateTime` and re-serializes at chrono's
// precision, which silently changes the CID of every record it round-trips.
//
// Four things atproto requires that RFC 3339 alone does not:
//
//   - The timezone is mandatory. A bare `1985-04-12T23:20:50` is not a moment.
//   - `-00:00` is forbidden. RFC 3339 gives it the meaning "UTC, but the local
//     offset is unknown", which is not a thing a record should claim.
//   - Uppercase `T` and `Z` only. Lowercase is legal ISO 8601 and is rejected.
//   - Four-digit years, zero-padded throughout.

///|
/// Generous: the longest sensible value is about 35 characters. The cap exists
/// to bound work on hostile input.
const DATETIME_MAX_LENGTH : Int = 64

///|
/// A syntactically and semantically valid RFC 3339 datetime, holding the exact
/// bytes it was parsed from.
pub struct Datetime(String) derive(Eq, Debug)

///|
pub impl Show for Datetime with fn output(self, logger) {
  logger.write_string(self.0)
}

///|
pub fn Datetime::to_string(self : Self) -> String {
  self.0
}

///|
pub fn Datetime::unchecked(value : String) -> Datetime {
  Datetime(value)
}

///|
pub fn Datetime::is_valid(value : String) -> Bool {
  try {
    Datetime::parse(value) |> ignore
    true
  } catch {
    _ => false
  }
}

///|
/// The parsed pieces. Kept alongside the string rather than replacing it, so
/// callers can do arithmetic without the value losing its original spelling.
pub struct DatetimeParts {
  year : Int
  month : Int
  day : Int
  hour : Int
  minute : Int
  second : Int
  /// The fractional digits, without the dot, exactly as written -- so
  /// `.120` and `.12` stay distinguishable.
  fraction : String
  /// Minutes east of UTC. `Z` is zero.
  offset_minutes : Int
} derive(Eq, Debug)

///|
pub fn Datetime::parts(self : Self) -> DatetimeParts {
  // Unreachable failure: `parse` accepted this string already.
  match scan(self.0) {
    Some(parts) => parts
    None =>
      {
        year: 1970,
        month: 1,
        day: 1,
        hour: 0,
        minute: 0,
        second: 0,
        fraction: "",
        offset_minutes: 0,
      }
  }
}

///|
/// Seconds since the Unix epoch, with the offset applied. Sub-second precision
/// is dropped; `parts().fraction` still has it.
pub fn Datetime::to_epoch_seconds(self : Self) -> Int64 {
  epoch_seconds_of(self.parts())
}

///|
/// Split out from the method so `parse` can use it before it has a `Datetime`
/// to call it on -- and because `Datetime(...)` as a constructor is ambiguous
/// with `SyntaxKind::Datetime`.
fn epoch_seconds_of(p : DatetimeParts) -> Int64 {
  days_from_civil(p.year, p.month, p.day) * 86400L +
  (p.hour * 3600 + p.minute * 60 + p.second).to_int64() -
  (p.offset_minutes * 60).to_int64()
}

///|
/// Builds a UTC datetime with millisecond precision -- `1985-04-12T23:20:50.123Z`,
/// the spelling the spec calls "preferred" and the one to write when creating a
/// record.
///
/// The clock is an argument, as everywhere else in this package.
pub fn Datetime::from_epoch_millis(millis : Int64) -> Datetime {
  let mut secs = millis / 1000L
  let mut ms = millis % 1000L
  // Truncation rounds toward zero, so a negative remainder must be carried.
  if ms < 0L {
    ms = ms + 1000L
    secs = secs - 1L
  }
  let mut day = secs / 86400L
  let mut rem = secs % 86400L
  if rem < 0L {
    rem = rem + 86400L
    day = day - 1L
  }
  let (year, month, mday) = civil_from_days(day)
  let hour = (rem / 3600L).to_int()
  let minute = (rem % 3600L / 60L).to_int()
  let second = (rem % 60L).to_int()
  let s = pad(year, 4) +
    "-" +
    pad(month, 2) +
    "-" +
    pad(mday, 2) +
    "T" +
    pad(hour, 2) +
    ":" +
    pad(minute, 2) +
    ":" +
    pad(second, 2) +
    "." +
    pad(ms.to_int(), 3) +
    "Z"
  Datetime(s)
}

///|
pub fn Datetime::parse(value : String) -> Datetime raise SyntaxError {
  fn bad(reason : String) -> SyntaxError {
    SyntaxError(kind=Datetime, input=value, reason~)
  }

  guard value.length() <= DATETIME_MAX_LENGTH else {
    raise bad("datetime is too long (\{DATETIME_MAX_LENGTH} chars max)")
  }
  // Checked before the pattern, and separately from it, because `-00:00` is
  // well-formed RFC 3339 -- it is refused for what it means, not how it looks.
  guard !value.has_suffix("-00:00") else {
    raise bad("datetime can not use \"-00:00\" for UTC timezone")
  }
  guard scan(value) is Some(parts) else {
    raise bad(
      "datetime is not in a valid format (must match RFC 3339 & ISO 8601 with 'Z' or ±hh:mm timezone)",
    )
  }
  // The pattern admits day 00 and 31 February; the calendar does not. Upstream
  // gets this by handing the string to `new Date` and seeing what comes back.
  guard parts.day >= 1 && parts.day <= days_in_month(parts.year, parts.month) else {
    raise bad("datetime did not parse as ISO 8601")
  }
  // `0000-01-01T00:00:00+01:00` is a valid-looking string for an instant before
  // year zero. The offset is what makes it so, which is why this cannot be a
  // check on the year alone.
  guard epoch_seconds_of(parts) >= EPOCH_SECONDS_AT_YEAR_ZERO else {
    raise bad("datetime normalized to a negative time")
  }
  Datetime::Datetime(value)
}

///|
/// Seconds from the Unix epoch back to 0000-01-01T00:00:00Z. Negative, and the
/// floor below which a datetime is considered to have gone negative.
const EPOCH_SECONDS_AT_YEAR_ZERO : Int64 = -62167219200L

///|
/// Recognises the grammar and returns the pieces, or `None`.
///
/// Written as a scan rather than a regex so that each field's permitted range
/// is stated where it is read -- `[0-1][0-9]|2[0-3]` for the hour, `0[1-9]|1[012]`
/// for the month -- which is also what makes the ranges reviewable against the
/// spec.
fn scan(s : String) -> DatetimeParts? {
  // YYYY-MM-DDTHH:MM:SS is 19 characters, and the timezone adds at least one.
  guard s.length() >= 20 else { return None }
  guard digits(s, 0, 4) is Some(year) else { return None }
  guard code_unit_is(s[4], '-') else { return None }
  guard digits(s, 5, 2) is Some(month) else { return None }
  guard month >= 1 && month <= 12 else { return None }
  guard code_unit_is(s[7], '-') else { return None }
  guard digits(s, 8, 2) is Some(day) else { return None }
  // `[0-2][0-9]|3[01]`: the pattern allows day 00, and the calendar check in
  // `parse` is what rejects it.
  guard day <= 31 else { return None }
  guard code_unit_is(s[10], 'T') else { return None }
  guard digits(s, 11, 2) is Some(hour) else { return None }
  guard hour <= 23 else { return None }
  guard code_unit_is(s[13], ':') else { return None }
  guard digits(s, 14, 2) is Some(minute) else { return None }
  guard minute <= 59 else { return None }
  guard code_unit_is(s[16], ':') else { return None }
  guard digits(s, 17, 2) is Some(second) else { return None }
  // 60 is a leap second, which RFC 3339 permits.
  guard second <= 60 else { return None }
  let mut i = 19
  let mut fraction = ""
  if i < s.length() && code_unit_is(s[i], '.') {
    let start = i + 1
    let mut end = start
    while end < s.length() && code_unit_is_ascii_digit(s[end]) {
      end = end + 1
    }
    // A dot with no digits after it is not a fraction.
    guard end > start else { return None }
    fraction = s[start:end].to_owned()
    i = end
  }
  // The timezone is mandatory, and is either `Z` or `±HH:MM`.
  guard i < s.length() else { return None }
  let offset_minutes = if code_unit_is(s[i], 'Z') {
    guard i + 1 == s.length() else { return None }
    0
  } else {
    let sign = if code_unit_is(s[i], '+') {
      1
    } else if code_unit_is(s[i], '-') {
      -1
    } else {
      return None
    }
    guard i + 6 == s.length() else { return None }
    guard digits(s, i + 1, 2) is Some(oh) else { return None }
    guard oh <= 23 else { return None }
    guard code_unit_is(s[i + 3], ':') else { return None }
    guard digits(s, i + 4, 2) is Some(om) else { return None }
    guard om <= 59 else { return None }
    sign * (oh * 60 + om)
  }
  Some({ year, month, day, hour, minute, second, fraction, offset_minutes })
}

///|
/// Exactly `count` ASCII digits starting at `start`, as a number. `None` if any
/// of them is not a digit or the string is too short -- which is what enforces
/// the zero-padding the spec requires.
fn digits(s : String, start : Int, count : Int) -> Int? {
  guard start + count <= s.length() else { return None }
  let mut acc = 0
  for i = start; i < start + count; i = i + 1 {
    guard code_unit_is_ascii_digit(s[i]) else { return None }
    acc = acc * 10 + (s[i].to_int() - '0'.to_int())
  }
  Some(acc)
}

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

///|
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
  }
}

///|
/// Days from 1970-01-01 to the given proleptic Gregorian date. Howard
/// Hinnant's `days_from_civil`, which is exact for every year this type can
/// hold and needs no table.
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
}

///|
/// The inverse of `days_from_civil`.
fn civil_from_days(days : Int64) -> (Int, Int, Int) {
  let z = days + 719468L
  let era = (if z >= 0L { z } else { z - 146096L }) / 146097L
  let doe = (z - era * 146097L).to_int()
  let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
  let y = yoe.to_int64() + era * 400L
  let doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
  let mp = (5 * doy + 2) / 153
  let d = doy - (153 * mp + 2) / 5 + 1
  let m = if mp < 10 { mp + 3 } else { mp - 9 }
  ((if m <= 2 { y + 1L } else { y }).to_int(), m, d)
}

///|
fn pad(value : Int, width : Int) -> String {
  let digits = value.to_string()
  if digits.length() >= width {
    return digits
  }
  let b = StringBuilder::new(size_hint=width)
  for _ in 0..<(width - digits.length()) {
    b.write_char('0')
  }
  b.write_string(digits)
  b.to_string()
}