// tzif-engine: IANA TZif timezone conversion engine.
// Public API — load TZif data, convert between UTC and local time.

// Parse raw TZif bytes (v2 or v3) into a TzifData structure.

///|
pub fn load(data : Bytes) -> @types.TzifResult[@types.TzifData] {
  let result1 = @parser.parse_header(data, 0)
  let (v1_header, offset) = match result1 {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  if v1_header.version != b'2' && v1_header.version != b'3' {
    return Err(UnsupportedVersion(v1_header.version))
  }
  // Skip the compatibility block with its four-byte timestamps and eight-byte
  // leap records (timestamp plus correction).
  let v2_offset = match @parser.data_block_end(data, v1_header, offset, 4) {
    Ok(value) => value
    Err(error) => return Err(error)
  }

  let result2 = @parser.parse_header(data, v2_offset)
  let (v2_header, offset) = match result2 {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  if v2_header.version != v1_header.version {
    return Err(InvalidTransitionTable("TZif header versions do not match"))
  }
  let result3 = @parser.parse_body(data, v2_header, offset)
  let body = match result3 {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  // Both TZif v2 and v3 carry a newline-framed POSIX footer immediately after
  // the second data block. The parser owns framing and grammar validation.
  let posix = match
    @parser.parse_footer(
      data,
      offset=body.end_offset,
      version=v2_header.version,
    ) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let timezone = @types.TzifData::{
    version: v2_header.version,
    time_types: body.time_types,
    transitions: body.transitions,
    leap_seconds: body.leap_seconds,
    standard_indicators: body.standard_indicators,
    utc_indicators: body.utc_indicators,
    posix_rule: posix,
  }
  match validate(timezone) {
    Ok(_) => Ok(timezone)
    Err(error) => Err(error)
  }
}

// Convert a UTC timestamp to local time in the given timezone.

///|
pub fn utc_to_local(
  tz : @types.TzifData,
  utc_time : Int64,
) -> @types.TzifResult[@types.LocalDateTime] {
  @converter.utc_to_local(tz, utc_time)
}

///|
/// Return offset metadata effective at a UTC instant without decomposing the
/// timestamp into calendar fields.
pub fn active_offset(
  tz : @types.TzifData,
  utc_time : Int64,
) -> @types.TzifResult[@types.OffsetInfo] {
  @converter.active_offset(tz, utc_time)
}

///|
/// Return every UTC candidate for a local civil time in ascending order.
pub fn local_time_candidates(
  tz : @types.TzifData,
  ldt : @types.LocalDateTime,
) -> @types.TzifResult[Array[Int64]] {
  @converter.local_time_candidates(tz, ldt)
}

///|
/// Classify a local time as unique, ambiguous, or a gap without selecting a
/// candidate automatically.
pub fn resolve_local(
  tz : @types.TzifData,
  ldt : @types.LocalDateTime,
) -> @types.TzifResult[@types.LocalTimeResolution] {
  @converter.resolve_local(tz, ldt)
}

// Convert a local date-time to a UTC timestamp in the given timezone.

///|
pub fn local_to_utc(
  tz : @types.TzifData,
  ldt : @types.LocalDateTime,
  strategy : @types.AmbiguityStrategy,
) -> @types.TzifResult[Int64] {
  @converter.local_to_utc(tz, ldt, strategy)
}

///|
/// Parse a local civil timestamp in strict `YYYY-MM-DDTHH:MM:SS` form.
pub fn parse_iso_local(
  input : String,
) -> @types.TzifResult[@types.LocalDateTime] {
  @text.parse_iso_local(input)
}

///|
/// Parse an ISO-8601 date-time carrying `Z`, `+HH:MM`, or `+HH:MM:SS`.
pub fn parse_iso_offset(
  input : String,
) -> @types.TzifResult[@types.OffsetDateTime] {
  @text.parse_iso_offset(input)
}

///|
/// Parse exactly `YYYY-MM-DDTHH:MM:SSZ` into POSIX UTC seconds.
pub fn parse_iso_utc(input : String) -> @types.TzifResult[Int64] {
  @text.parse_iso_utc(input)
}

///|
/// Format civil fields as `YYYY-MM-DDTHH:MM:SS`.
pub fn format_iso_local(
  local_time : @types.LocalDateTime,
) -> @types.TzifResult[String] {
  @text.format_iso_local(local_time)
}

///|
/// Format a resolved local time with its numeric UTC offset.
pub fn format_iso_zoned(
  local_time : @types.LocalDateTime,
) -> @types.TzifResult[String] {
  @text.format_iso_zoned(local_time)
}

///|
/// Format a numeric UTC offset without losing historical seconds.
pub fn format_numeric_offset(offset : Int) -> @types.TzifResult[String] {
  @text.format_numeric_offset(offset)
}

///|
/// Format a UTC instant at an explicit ISO-8601 numeric offset.
pub fn format_iso_offset(
  utc_time : Int64,
  offset : Int,
) -> @types.TzifResult[String] {
  @text.format_iso_offset(utc_time, offset)
}

///|
/// Format a POSIX UTC timestamp in ISO-8601 UTC form.
pub fn format_iso_utc(utc_time : Int64) -> @types.TzifResult[String] {
  @text.format_iso_utc(utc_time)
}

// Return a human-readable summary of the timezone data.

///|
pub fn info(tz : @types.TzifData) -> String {
  match validate(tz) {
    Err(_) => return "Invalid in-memory TZif data"
    Ok(_) => ()
  }
  let summary = diagnostics(tz)
  let n = tz.transitions.length()
  let version = if tz.version == b'2' { "2" } else { "3" }
  let metadata = "  Metadata: \{summary.leap_second_count} leap records, \{summary.standard_indicator_count} ttisstd flags, \{summary.utc_indicator_count} ttisut flags\n" +
    "  Post-transition: " +
    (if summary.has_posix_footer {
      "POSIX footer extrapolation"
    } else {
      "last explicit type"
    })
  if n == 0 {
    let initial = tz.time_types[0]
    return "TZif v\{version} — 0 transitions\n" +
      "  Initial: \{initial.abbr} (UTC\{format_offset(initial.utoff)})\n" +
      metadata
  }
  let first = tz.transitions[0]
  let last = tz.transitions[n - 1]
  let first_type = tz.time_types[first.type_index]
  let last_type = tz.time_types[last.type_index]
  "\{n} transitions, TZif v\{version} format\n" +
  "  First: \{first_type.abbr} (UTC\{format_offset(first_type.utoff)}) at \{first.utc_time}\n" +
  "  Last:  \{last_type.abbr} (UTC\{format_offset(last_type.utoff)}) at \{last.utc_time}\n" +
  metadata
}

///|
fn format_offset(utoff : Int) -> String {
  match @text.format_numeric_offset(utoff) {
    Ok(value) => value
    Err(_) =>
      if utoff >= 0 {
        "+" + utoff.to_string() + "s"
      } else {
        utoff.to_string() + "s"
      }
  }
}