/// Returns the system's local timezone offset.
///
/// The implementation is target-dependent:
/// - **Native**: calls a C FFI stub (`local_tz_offset_minutes`).
/// - **JS**: uses `new Date().getTimezoneOffset()` (sign-inverted).
/// - **WASM**: returns `Utc` (WASI has no timezone API).
///
/// The result is normalized to `Utc`, `Hour(n)`, or `Min(n)`.

///|
pub fn local_tz_offset() -> TzOffset {
  normalize_tz_offset(local_tz_offset_minutes_ffi())
}

/// Normalizes a minute-based offset into the canonical `TzOffset` representation.
///
/// - `0` → `Utc`
/// - Divisible by 60 → `Hour(n)`
/// - Otherwise → `Min(n)`

///|
fn normalize_tz_offset(minutes : Int) -> TzOffset {
  if minutes == 0 {
    Utc
  } else if minutes % 60 == 0 {
    Hour(minutes / 60)
  } else {
    Min(minutes)
  }
}

/// Converts a `TzOffset` to its equivalent in minutes.
///
/// `Local` is resolved to the system timezone before conversion.

///|
fn tz_offset_to_minutes(tz : TzOffset) -> Int {
  match tz {
    Utc => 0
    Hour(h) => h * 60
    Min(m) => m
    Local => tz_offset_to_minutes(local_tz_offset())
  }
}

/// Parses a timezone offset string into a `TzOffset`.
///
/// Accepted input patterns:
/// - Empty string, `"Z"`, `"z"`, `"UTC"` (case-insensitive) → `Utc`
/// - `"local"` (case-insensitive) → `Local`
/// - Numeric: `"9"`, `"+09"`, `"+0900"`, `"+09:00"` → `Hour(9)`
/// - Duration: `"5h30m"`, `"+5h30m"` → `Min(330)`
/// - Negative: `"-5h"` → `Hour(-5)`
///
/// Parameters:
///
/// * `input` : The timezone offset string.
///
/// Returns a normalized `TzOffset` (`Utc`, `Hour`, or `Min`).
///
/// Raises `ParseError` for sub-minute precision or invalid formats.

///|
pub fn parse_tz_offset(input : String) -> TzOffset raise ParseError {
  let s = input.trim()
  let len = s.length()
  if len == 0 {
    return Utc
  }
  let lower = s.to_lower()
  // Z → Utc
  if lower == "z" {
    return Utc
  }
  // UTC/GMT prefix → strip and re-parse remainder
  // Handles: "UTC", "GMT", "UTC+0900", "GMT+9", "GMT +09:00" etc.
  if len >= 3 && (lower[:3] == "utc" || lower[:3] == "gmt") {
    let rest = s[3:].to_owned().trim()
    if rest.length() == 0 {
      return Utc
    }
    return parse_tz_offset(rest.to_owned())
  }
  // local → Local
  if lower == "local" {
    return Local
  }
  // Get the leading sign
  let mut pos = 0
  let sign : Int = match s.get_char(pos) {
    Some('+') => {
      pos += 1
      1
    }
    Some('-') => {
      pos += 1
      -1
    }
    _ => 1
  }
  // Detect duration format: check if h, m, or s appears from pos onward
  let is_duration = contains_duration_unit(s, pos)
  let total_minutes : Int = if is_duration {
    // Pass the original trimmed string to parse_duration
    let trimmed = input.trim().to_owned()
    let dur = parse_duration(trimmed, default_sign=Plus)
    let ms = dur.0
    // TZ offset is in minutes. Error if not evenly divisible
    if ms % 60_000L != 0L {
      raise ParseError(
        "timezone offset must be in whole minutes, got: " + trimmed,
      )
    }
    (ms / 60_000L).to_int()
  } else {
    // Parse numeric format
    parse_numeric_offset(s, pos, sign)
  }
  // Range check: TZ offset must be within +/-24 hours (+/-1440 minutes)
  if total_minutes > 1440 || total_minutes < -1440 {
    raise ParseError(
      "timezone offset out of range (must be within ±24 hours): " + input,
    )
  }
  normalize_tz_offset(total_minutes)
}

/// Checks if the string contains a duration unit character (`h`, `m`, `s`)
/// at or after position `start`, to distinguish duration format from numeric format.

///|
fn contains_duration_unit(s : StringView, start : Int) -> Bool {
  let mut i = start
  while i < s.length() {
    match s.get_char(i) {
      Some('h') | Some('m') | Some('s') => return true
      _ => i += 1
    }
  }
  false
}

/// Parses a numeric timezone offset in `HH`, `HHMM`, or `HH:MM` format.
/// Returns the total offset in minutes, with the sign applied.

///|
fn parse_numeric_offset(
  s : StringView,
  start : Int,
  sign : Int,
) -> Int raise ParseError {
  let mut pos = start
  // Read the first numeric part (TZ offset is at most 4 digits for HHMM)
  let mut first_num = 0
  let mut first_digits = 0
  while pos < s.length() {
    match s.get_char(pos) {
      Some(c) =>
        if c.is_ascii_digit() {
          first_digits += 1
          if first_digits > 4 {
            raise ParseError("too many digits in timezone offset")
          }
          first_num = first_num * 10 + (c.to_int() - '0'.to_int())
          pos += 1
        } else {
          break
        }
      None => break
    }
  }
  if first_digits == 0 {
    raise ParseError("expected digit in timezone offset")
  }
  // Check for colon separator
  match s.get_char(pos) {
    Some(':') => {
      // HH:MM format
      pos += 1
      let mut mm = 0
      let mut mm_digits = 0
      while pos < s.length() {
        match s.get_char(pos) {
          Some(c) =>
            if c.is_ascii_digit() {
              mm = mm * 10 + (c.to_int() - '0'.to_int())
              mm_digits += 1
              pos += 1
            } else {
              break
            }
          None => break
        }
      }
      if mm_digits == 0 {
        raise ParseError("expected minutes after ':' in timezone offset")
      }
      if pos < s.length() {
        raise ParseError(
          "unexpected trailing characters in timezone offset: " + s.to_owned(),
        )
      }
      sign * (first_num * 60 + mm)
    }
    _ =>
      if first_digits <= 2 {
        // 1-2 digits: hours only
        if pos < s.length() {
          raise ParseError(
            "unexpected trailing characters in timezone offset: " + s.to_owned(),
          )
        }
        sign * first_num * 60
      } else if first_digits == 4 {
        // 4 digits: HHMM
        if pos < s.length() {
          raise ParseError(
            "unexpected trailing characters in timezone offset: " + s.to_owned(),
          )
        }
        let hh = first_num / 100
        let mm = first_num % 100
        sign * (hh * 60 + mm)
      } else {
        raise ParseError("invalid timezone offset format")
      }
  }
}