///|
/// whenwords - A relative time formatting library
/// MoonBit implementation following the whenwords specification v0.1.0

// Time constants in seconds
let seconds_per_minute : Int64 = 60

///|
let seconds_per_hour : Int64 = 3600

///|
let seconds_per_day : Int64 = 86400

///|
let seconds_per_month : Int64 = 2592000 // 30 days

///|
let seconds_per_year : Int64 = 31536000 // 365 days

///|
/// Converts a timestamp to a relative time string like "3 hours ago" or "in 2 days"
///
/// Parameters:
/// - timestamp: Unix timestamp in seconds
/// - reference: Reference timestamp (defaults to current context, must be provided)
///
/// Returns: Human-readable relative time string
pub fn timeago(timestamp : Int64, reference~ : Int64) -> String {
  let diff = reference - timestamp
  let abs_diff = if diff < 0L { -diff } else { diff }
  let is_future = diff < 0L
  let result = format_time_diff(abs_diff)
  if is_future {
    if result == "just now" {
      "just now"
    } else {
      "in " + strip_ago_suffix(result)
    }
  } else {
    result
  }
}

///|
fn strip_ago_suffix(s : String) -> String {
  if s.has_suffix(" ago") {
    s.unsafe_substring(start=0, end=s.length() - 4)
  } else {
    s
  }
}

///|
fn format_time_diff(seconds : Int64) -> String {
  // 0-44 seconds: "just now"
  if seconds < 45L {
    return "just now"
  }
  // 45-89 seconds: "1 minute ago"
  if seconds < 90L {
    return "1 minute ago"
  }
  // 90 sec - 44 min: "{n} minutes ago"
  if seconds < 45L * 60L {
    let minutes = round_half_up(seconds, seconds_per_minute)
    return pluralize(minutes, "minute") + " ago"
  }
  // 45-89 minutes: "1 hour ago"
  if seconds < 90L * 60L {
    return "1 hour ago"
  }
  // 90 min - 21 hours: "{n} hours ago"
  if seconds < 22L * 3600L {
    let hours = round_half_up(seconds, seconds_per_hour)
    return pluralize(hours, "hour") + " ago"
  }
  // 22-35 hours: "1 day ago"
  if seconds < 36L * 3600L {
    return "1 day ago"
  }
  // 36 hours - 25 days: "{n} days ago"
  if seconds < 26L * 86400L {
    let days = round_half_up(seconds, seconds_per_day)
    return pluralize(days, "day") + " ago"
  }
  // 26-45 days: "1 month ago"
  if seconds < 46L * 86400L {
    return "1 month ago"
  }
  // 46-319 days: "{n} months ago"
  if seconds < 320L * 86400L {
    let months = round_half_up(seconds, seconds_per_month)
    return pluralize(months, "month") + " ago"
  }
  // 320-547 days: "1 year ago"
  if seconds < 548L * 86400L {
    return "1 year ago"
  }
  // 548+ days: "{n} years ago"
  let years = round_half_up(seconds, seconds_per_year)
  pluralize(years, "year") + " ago"
}

///|
fn round_half_up(value : Int64, divisor : Int64) -> Int64 {
  (value + divisor / 2L) / divisor
}

///|
fn pluralize(count : Int64, unit : String) -> String {
  if count == 1L {
    "1 " + unit
  } else {
    count.to_string() + " " + unit + "s"
  }
}

///|
/// Options for duration formatting
pub(all) struct DurationOptions {
  compact : Bool // Use compact format like "2h 30m"
  max_units : Int // Maximum number of units to display (default: 2)
}

///|
pub fn DurationOptions::default() -> DurationOptions {
  { compact: false, max_units: 2 }
}

///|
/// Formats a number of seconds as a human-readable duration
///
/// Parameters:
/// - seconds: Non-negative number of seconds
/// - options: Formatting options (compact mode, max units)
///
/// Returns: Human-readable duration string
pub fn duration(
  seconds : Int64,
  options? : DurationOptions = DurationOptions::default(),
) -> String {
  if seconds == 0L {
    return if options.compact { "0s" } else { "0 seconds" }
  }
  let mut remaining = seconds
  let parts : Array[String] = []
  // Years
  if remaining >= seconds_per_year {
    let years = remaining / seconds_per_year
    remaining = remaining % seconds_per_year
    parts.push(format_duration_unit(years, "year", options.compact))
  }
  // Months
  if remaining >= seconds_per_month && parts.length() < options.max_units {
    let months = remaining / seconds_per_month
    remaining = remaining % seconds_per_month
    parts.push(format_duration_unit(months, "month", options.compact))
  }
  // Days
  if remaining >= seconds_per_day && parts.length() < options.max_units {
    let days = remaining / seconds_per_day
    remaining = remaining % seconds_per_day
    parts.push(format_duration_unit(days, "day", options.compact))
  }
  // Hours
  if remaining >= seconds_per_hour && parts.length() < options.max_units {
    let hours = remaining / seconds_per_hour
    remaining = remaining % seconds_per_hour
    parts.push(format_duration_unit(hours, "hour", options.compact))
  }
  // Minutes
  if remaining >= seconds_per_minute && parts.length() < options.max_units {
    let minutes = remaining / seconds_per_minute
    remaining = remaining % seconds_per_minute
    parts.push(format_duration_unit(minutes, "minute", options.compact))
  }
  // Seconds (only if we have room and there are remaining seconds)
  if remaining > 0L && parts.length() < options.max_units {
    parts.push(format_duration_unit(remaining, "second", options.compact))
  }
  // Handle case where all units are zero but we have remaining seconds
  if parts.is_empty() {
    parts.push(format_duration_unit(remaining, "second", options.compact))
  }
  if options.compact {
    parts.join(" ")
  } else {
    parts.join(", ")
  }
}

///|
fn format_duration_unit(count : Int64, unit : String, compact : Bool) -> String {
  if compact {
    let abbrev = match unit {
      "year" => "y"
      "month" => "mo"
      "day" => "d"
      "hour" => "h"
      "minute" => "m"
      "second" => "s"
      _ => unit
    }
    count.to_string() + abbrev
  } else {
    pluralize(count, unit)
  }
}

///|
/// Error type for parse_duration
pub suberror ParseDurationError {
  ParseDurationError(String)
}

///|
/// Parses a duration string into seconds
///
/// Supported formats:
/// - Compact: "2h30m", "2h 30m"
/// - Verbose: "2 hours 30 minutes", "2 hours and 30 minutes"
/// - Decimal: "2.5 hours", "1.5h"
/// - Colon: "2:30" (h:mm), "2:30:00" (h:mm:ss)
///
/// Returns: Number of seconds, or error if parsing fails
pub fn parse_duration(input : String) -> Int64 raise ParseDurationError {
  let s = input.trim().to_string()
  if s.is_empty() {
    raise ParseDurationError("Empty input")
  }
  // Try colon notation first: "2:30" or "2:30:00"
  if s.contains(":") {
    return parse_colon_duration(s)
  }
  // Parse compact/verbose format
  parse_unit_duration(s)
}

///|
fn parse_colon_duration(s : String) -> Int64 raise ParseDurationError {
  let parts : Array[String] = s
    .split(":")
    .map(fn(sv) { sv.to_string() })
    .collect()
  match parts.length() {
    2 => {
      let hours = parse_int64_safe(parts[0])
      let minutes = parse_int64_safe(parts[1])
      hours * seconds_per_hour + minutes * seconds_per_minute
    }
    3 => {
      let hours = parse_int64_safe(parts[0])
      let minutes = parse_int64_safe(parts[1])
      let secs = parse_int64_safe(parts[2])
      hours * seconds_per_hour + minutes * seconds_per_minute + secs
    }
    _ => raise ParseDurationError("Invalid colon notation")
  }
}

///|
fn parse_int64_safe(s : String) -> Int64 raise ParseDurationError {
  let trimmed = s.trim()
  @strconv.parse_int64(trimmed) catch {
    _ => raise ParseDurationError("Invalid number: " + trimmed.to_string())
  }
}

///|
fn parse_float64_safe(s : String) -> Double raise ParseDurationError {
  let trimmed = s.trim()
  @strconv.parse_double(trimmed) catch {
    _ => raise ParseDurationError("Invalid number: " + trimmed.to_string())
  }
}

///|
fn parse_unit_duration(s : String) -> Int64 raise ParseDurationError {
  // Remove "and", ",", extra spaces
  let normalized = s
    .to_lower()
    .replace_all(old=",", new=" ")
    .replace_all(old=" and ", new=" ")
  let mut total_seconds = 0.0
  let mut current_num = ""
  let mut i = 0
  let chars = normalized.to_array()
  while i < chars.length() {
    let c = chars[i]
    // Accumulate digits and decimal points
    if (c >= '0' && c <= '9') || c == '.' {
      current_num = current_num + c.to_string()
      i = i + 1
      continue
    }
    // Skip whitespace between number and unit
    if c == ' ' && current_num.is_empty() {
      i = i + 1
      continue
    }
    // Try to parse a unit
    if not(current_num.is_empty()) || c != ' ' {
      // Find the unit
      let unit_start = i
      while i < chars.length() &&
            chars[i] != ' ' &&
            not(chars[i] >= '0' && chars[i] <= '9') {
        i = i + 1
      }
      if unit_start < i {
        let unit = normalized.unsafe_substring(start=unit_start, end=i)
        if not(current_num.is_empty()) {
          let num = parse_float64_safe(current_num)
          let multiplier = unit_to_seconds(unit)
          if multiplier == 0L {
            raise ParseDurationError("Unknown unit: " + unit)
          }
          total_seconds = total_seconds + num * multiplier.to_double()
          current_num = ""
        }
      }
    }
    if i < chars.length() && chars[i] == ' ' {
      i = i + 1
    }
  }
  // Handle trailing number without unit (assume seconds)
  if not(current_num.is_empty()) {
    let num = parse_float64_safe(current_num)
    total_seconds = total_seconds + num
  }
  if total_seconds < 0.0 {
    raise ParseDurationError("Negative duration")
  }
  total_seconds.to_int64()
}

///|
fn unit_to_seconds(unit : String) -> Int64 {
  match unit {
    "s" | "sec" | "second" | "seconds" => 1L
    "m" | "min" | "minute" | "minutes" => seconds_per_minute
    "h" | "hr" | "hour" | "hours" => seconds_per_hour
    "d" | "day" | "days" => seconds_per_day
    "w" | "wk" | "week" | "weeks" => 7L * seconds_per_day
    "mo" | "month" | "months" => seconds_per_month
    "y" | "yr" | "year" | "years" => seconds_per_year
    _ => 0L
  }
}

///|
/// Date information extracted from a Unix timestamp
priv struct DateInfo {
  year : Int
  month : Int // 1-12
  day : Int // 1-31
  weekday : Int // 0=Sunday, 1=Monday, ..., 6=Saturday
}

///|
fn timestamp_to_date(timestamp : Int64) -> DateInfo {
  // Convert Unix timestamp to date components
  // Days since Unix epoch (1970-01-01)
  let days = timestamp / seconds_per_day
  // Calculate weekday (1970-01-01 was Thursday = 4)
  let weekday = ((days % 7L).to_int() + 4) % 7
  // Calculate year, month, day using a simplified algorithm
  let mut remaining_days = days.to_int()
  let mut year = 1970
  // Advance by years
  while true {
    let days_in_year = if is_leap_year(year) { 366 } else { 365 }
    if remaining_days < days_in_year {
      break
    }
    remaining_days = remaining_days - days_in_year
    year = year + 1
  }
  // Advance by months
  let mut month = 1
  while month <= 12 {
    let days_in_month = get_days_in_month(year, month)
    if remaining_days < days_in_month {
      break
    }
    remaining_days = remaining_days - days_in_month
    month = month + 1
  }
  let day = remaining_days + 1
  { year, month, day, weekday }
}

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

///|
fn get_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 }
    _ => 30
  }
}

///|
fn weekday_name(weekday : Int) -> String {
  match weekday {
    0 => "Sunday"
    1 => "Monday"
    2 => "Tuesday"
    3 => "Wednesday"
    4 => "Thursday"
    5 => "Friday"
    6 => "Saturday"
    _ => "Unknown"
  }
}

///|
fn month_name(month : Int) -> String {
  match month {
    1 => "January"
    2 => "February"
    3 => "March"
    4 => "April"
    5 => "May"
    6 => "June"
    7 => "July"
    8 => "August"
    9 => "September"
    10 => "October"
    11 => "November"
    12 => "December"
    _ => "Unknown"
  }
}

///|
/// Returns a contextual date string like "Today", "Yesterday", "Last Tuesday", or "March 5"
///
/// Parameters:
/// - timestamp: Unix timestamp in seconds
/// - reference: Reference timestamp for comparison
///
/// Returns: Human-readable date string
pub fn human_date(timestamp : Int64, reference~ : Int64) -> String {
  let target = timestamp_to_date(timestamp)
  let ref_date = timestamp_to_date(reference)
  // Calculate day difference
  let target_days = timestamp / seconds_per_day
  let ref_days = reference / seconds_per_day
  let day_diff = (ref_days - target_days).to_int()
  // Same day
  if day_diff == 0 {
    return "Today"
  }
  // Yesterday
  if day_diff == 1 {
    return "Yesterday"
  }
  // Tomorrow
  if day_diff == -1 {
    return "Tomorrow"
  }
  // Within past 7 days: "Last {weekday}"
  if day_diff >= 2 && day_diff <= 7 {
    return "Last " + weekday_name(target.weekday)
  }
  // Within next 7 days: "This {weekday}"
  if day_diff >= -7 && day_diff <= -2 {
    return "This " + weekday_name(target.weekday)
  }
  // Same year: "Month day"
  if target.year == ref_date.year {
    return month_name(target.month) + " " + target.day.to_string()
  }
  // Different year: "Month day, year"
  month_name(target.month) +
  " " +
  target.day.to_string() +
  ", " +
  target.year.to_string()
}

///|
/// Formats a start and end timestamp as a smart date range
///
/// Examples:
/// - Same day: "March 5, 2024"
/// - Same month: "March 5–7, 2024"
/// - Same year: "March 5 – April 7, 2024"
/// - Cross-year: "December 28, 2024 – January 3, 2025"
///
/// Note: If start > end, they are silently swapped
pub fn date_range(start : Int64, end : Int64) -> String {
  // Swap if needed
  let (start, end) = if start > end { (end, start) } else { (start, end) }
  let start_date = timestamp_to_date(start)
  let end_date = timestamp_to_date(end)
  // Same day
  if start_date.year == end_date.year &&
    start_date.month == end_date.month &&
    start_date.day == end_date.day {
    return month_name(start_date.month) +
      " " +
      start_date.day.to_string() +
      ", " +
      start_date.year.to_string()
  }
  // Same month and year: "March 5–7, 2024"
  if start_date.year == end_date.year && start_date.month == end_date.month {
    return month_name(start_date.month) +
      " " +
      start_date.day.to_string() +
      "–" +
      end_date.day.to_string() +
      ", " +
      start_date.year.to_string()
  }
  // Same year: "March 5 – April 7, 2024"
  if start_date.year == end_date.year {
    return month_name(start_date.month) +
      " " +
      start_date.day.to_string() +
      " – " +
      month_name(end_date.month) +
      " " +
      end_date.day.to_string() +
      ", " +
      start_date.year.to_string()
  }
  // Cross-year: "December 28, 2024 – January 3, 2025"
  month_name(start_date.month) +
  " " +
  start_date.day.to_string() +
  ", " +
  start_date.year.to_string() +
  " – " +
  month_name(end_date.month) +
  " " +
  end_date.day.to_string() +
  ", " +
  end_date.year.to_string()
}