///|
pub struct Config {
  service : String
  level : String
  status : Int
  since : String
  until : String
}

///|
pub fn Config::new(
  service? : String = "",
  level? : String = "",
  status? : Int = 0,
  since? : String = "",
  until? : String = "",
) -> Config {
  { service, level, status, since, until, }
}

///|
pub fn Config::validate(self : Config) -> String? {
  if self.level != "" &&
    self.level != "DEBUG" &&
    self.level != "INFO" &&
    self.level != "WARN" &&
    self.level != "ERROR" {
    return Some("--level must be DEBUG, INFO, WARN, or ERROR")
  }
  if self.status != 0 && (self.status < 100 || self.status > 599) {
    return Some("--status must be an HTTP status from 100 through 599")
  }
  if self.since != "" && !valid_timestamp(self.since) {
    return Some(
      "--since must be a valid UTC timestamp such as 2026-09-24T03:00:00Z",
    )
  }
  if self.until != "" && !valid_timestamp(self.until) {
    return Some(
      "--until must be a valid UTC timestamp such as 2026-09-24T03:00:00Z",
    )
  }
  if self.since != "" &&
    self.until != "" &&
    timestamp_sort_key(self.since) > timestamp_sort_key(self.until) {
    return Some("--since must be at or before --until")
  }
  None
}

///|
pub struct ServiceStats {
  name : String
  count : Int
  errors : Int
  error_rate_pct : Double
  p50_ms : Double
  p95_ms : Double
}

///|
pub struct StatusStats {
  service : String
  status : Int
  count : Int
  p50_ms : Double
  p95_ms : Double
}

///|
pub struct Report {
  accepted : Int
  filtered : Int
  invalid : Int
  invalid_lines : Array[Int]
  errors : Int
  error_rate_pct : Double
  p50_ms : Double
  p95_ms : Double
  services : Array[ServiceStats]
  statuses : Array[StatusStats]
}

///|
priv struct Entry {
  timestamp : String
  service : String
  level : String
  status : Int
  latency_ms : Double
}

///|
fn digit_at(s : String, i : Int) -> Int {
  let value = s.unsafe_get(i).to_int()
  if value >= 48 && value <= 57 {
    value - 48
  } else {
    -1
  }
}

///|
fn digits(s : String, start : Int, end : Int) -> Int {
  let mut value = 0
  for i in start.. Bool {
  if (s.length() != 20 && s.length() != 24) ||
    s.unsafe_get(4).to_int() != 45 ||
    s.unsafe_get(7).to_int() != 45 ||
    s.unsafe_get(10).to_int() != 84 ||
    s.unsafe_get(13).to_int() != 58 ||
    s.unsafe_get(16).to_int() != 58 {
    return false
  }
  if s.length() == 20 {
    if s.unsafe_get(19).to_int() != 90 {
      return false
    }
  } else if s.unsafe_get(19).to_int() != 46 ||
    s.unsafe_get(23).to_int() != 90 ||
    digits(s, 20, 23) < 0 {
    return false
  }
  let year = digits(s, 0, 4)
  let month = digits(s, 5, 7)
  let day = digits(s, 8, 10)
  let hour = digits(s, 11, 13)
  let minute = digits(s, 14, 16)
  let second = digits(s, 17, 19)
  if year < 1 ||
    month < 1 ||
    month > 12 ||
    hour < 0 ||
    hour > 23 ||
    minute < 0 ||
    minute > 59 ||
    second < 0 ||
    second > 59 {
    return false
  }
  let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
  let days = match month {
    2 => if leap { 29 } else { 28 }
    4 | 6 | 9 | 11 => 30
    _ => 31
  }
  day >= 1 && day <= days
}

///|
/// Normalizing whole seconds to .000 makes lexical time bounds exact.
fn timestamp_sort_key(s : String) -> String {
  if s.length() == 20 {
    s[:19].to_owned() + ".000Z"
  } else {
    s
  }
}

///|
fn parse_entry(line : String) -> Entry? {
  let parsed = Some(@json.parse(line)) catch { _ => None }
  guard parsed is Some(Json::Object(fields)) else { return None }
  match
    (
      fields.get("timestamp"),
      fields.get("service"),
      fields.get("level"),
      fields.get("status"),
      fields.get("latency_ms"),
    ) {
    (
      Some(Json::String(timestamp)),
      Some(Json::String(service)),
      Some(Json::String(level)),
      Some(Json::Number(status, ..)),
      Some(Json::Number(latency_ms, ..)),
    ) => {
      if !valid_timestamp(timestamp) ||
        service.trim().is_empty() ||
        (
          level != "DEBUG" &&
          level != "INFO" &&
          level != "WARN" &&
          level != "ERROR"
        ) ||
        status < 100.0 ||
        status > 599.0 ||
        status != status.to_int().to_double() ||
        latency_ms < 0.0 ||
        latency_ms > 86400000.0 ||
        latency_ms.is_nan() {
        return None
      }
      Some({
        timestamp: timestamp_sort_key(timestamp),
        service,
        level,
        status: status.to_int(),
        latency_ms,
      })
    }
    _ => None
  }
}

///|
fn percentile(values : Array[Double], percentile : Int) -> Double {
  if values.is_empty() {
    return 0.0
  }
  values.sort_by(fn(a, b) { if a < b { -1 } else if a > b { 1 } else { 0 } })
  let index = ((values.length() * percentile + 99) / 100 - 1).max(0)
  values[index]
}

///|
fn rate(errors : Int, count : Int) -> Double {
  if count == 0 {
    0.0
  } else {
    errors.to_double() * 100.0 / count.to_double()
  }
}

///|
/// Invalid nonblank lines are reported by physical line number without
/// including raw content, so a report does not expose log payloads.
pub fn analyze(content : String, config? : Config = Config::new()) -> Report {
  let entries : Array[Entry] = []
  let invalid_lines : Array[Int] = []
  let mut filtered = 0
  let since = if config.since == "" {
    ""
  } else {
    timestamp_sort_key(config.since)
  }
  let until = if config.until == "" {
    ""
  } else {
    timestamp_sort_key(config.until)
  }
  for index, raw_line in content.split("\n") {
    let line = raw_line.trim().to_owned()
    if line.is_empty() {
      continue
    }
    match parse_entry(line) {
      None => invalid_lines.push(index + 1)
      Some(entry) =>
        if (config.service != "" && entry.service != config.service) ||
          (config.level != "" && entry.level != config.level) ||
          (config.status != 0 && entry.status != config.status) ||
          (since != "" && entry.timestamp < since) ||
          (until != "" && entry.timestamp > until) {
          filtered += 1
        } else {
          entries.push(entry)
        }
    }
  }
  let service_names : Array[String] = []
  let service_indices : @hashmap.HashMap[String, Int] = HashMap([])
  let service_latencies : Array[Array[Double]] = []
  let service_errors : Array[Int] = []
  let group_services : Array[String] = []
  let group_statuses : Array[Int] = []
  let group_indices : @hashmap.HashMap[String, Int] = HashMap([])
  let group_latencies : Array[Array[Double]] = []
  let all_latencies : Array[Double] = []
  let mut errors = 0
  for entry in entries {
    all_latencies.push(entry.latency_ms)
    let service_index = match service_indices.get(entry.service) {
      Some(index) => index
      None => {
        let index = service_names.length()
        service_indices.set(entry.service, index)
        service_names.push(entry.service)
        service_latencies.push([])
        service_errors.push(0)
        index
      }
    }
    service_latencies[service_index].push(entry.latency_ms)
    if entry.status >= 500 {
      service_errors[service_index] += 1
      errors += 1
    }
    // The length prefix makes the composite key unambiguous even if a
    // service name contains punctuation or digits used by the status code.
    let group_key = "\{entry.service.length()}:\{entry.service}:\{entry.status}"
    let group_index = match group_indices.get(group_key) {
      Some(index) => index
      None => {
        let index = group_services.length()
        group_indices.set(group_key, index)
        group_services.push(entry.service)
        group_statuses.push(entry.status)
        group_latencies.push([])
        index
      }
    }
    group_latencies[group_index].push(entry.latency_ms)
  }
  let services : Array[ServiceStats] = []
  for i, name in service_names {
    let count = service_latencies[i].length()
    let service_error_count = service_errors[i]
    services.push({
      name,
      count,
      errors: service_error_count,
      error_rate_pct: rate(service_error_count, count),
      p50_ms: percentile(service_latencies[i].copy(), 50),
      p95_ms: percentile(service_latencies[i].copy(), 95),
    })
  }
  services.sort_by(fn(a, b) { a.name[:].lexical_compare(b.name[:]) })
  let statuses : Array[StatusStats] = []
  for i, service in group_services {
    let latencies = group_latencies[i]
    statuses.push({
      service,
      status: group_statuses[i],
      count: latencies.length(),
      p50_ms: percentile(latencies.copy(), 50),
      p95_ms: percentile(latencies.copy(), 95),
    })
  }
  statuses.sort_by(fn(a, b) {
    let order = a.service[:].lexical_compare(b.service[:])
    if order != 0 {
      order
    } else {
      a.status.compare(b.status)
    }
  })
  {
    accepted: entries.length(),
    filtered,
    invalid: invalid_lines.length(),
    invalid_lines,
    errors,
    error_rate_pct: rate(errors, entries.length()),
    p50_ms: percentile(all_latencies.copy(), 50),
    p95_ms: percentile(all_latencies, 95),
    services,
    statuses,
  }
}

///|
pub fn Report::to_text(self : Report) -> String {
  let mut output = "accepted=\{self.accepted} filtered=\{self.filtered} invalid=\{self.invalid} errors=\{self.errors} error_rate_pct=\{self.error_rate_pct} p50_ms=\{self.p50_ms} p95_ms=\{self.p95_ms}\n"
  if !self.invalid_lines.is_empty() {
    output += "invalid_lines="
    for i, line in self.invalid_lines {
      if i > 0 {
        output += ","
      }
      output += line.to_string()
    }
    output += "\n"
  }
  for service in self.services {
    let name = Json::string(service.name).stringify()
    output += "service=\{name} requests=\{service.count} errors=\{service.errors} error_rate_pct=\{service.error_rate_pct} p50_ms=\{service.p50_ms} p95_ms=\{service.p95_ms}\n"
  }
  for group in self.statuses {
    let name = Json::string(group.service).stringify()
    output += "service=\{name} status=\{group.status} requests=\{group.count} p50_ms=\{group.p50_ms} p95_ms=\{group.p95_ms}\n"
  }
  output
}

///|
pub fn Report::to_json(self : Report) -> Json {
  let service_values : Array[Json] = []
  for service in self.services {
    service_values.push({
      "name": Json::string(service.name),
      "count": Json::number(service.count.to_double()),
      "errors": Json::number(service.errors.to_double()),
      "error_rate_pct": Json::number(service.error_rate_pct),
      "p50_ms": Json::number(service.p50_ms),
      "p95_ms": Json::number(service.p95_ms),
    })
  }
  let status_values : Array[Json] = []
  for group in self.statuses {
    status_values.push({
      "service": Json::string(group.service),
      "status": Json::number(group.status.to_double()),
      "count": Json::number(group.count.to_double()),
      "p50_ms": Json::number(group.p50_ms),
      "p95_ms": Json::number(group.p95_ms),
    })
  }
  let bad_lines : Array[Json] = []
  for line in self.invalid_lines {
    bad_lines.push(Json::number(line.to_double()))
  }
  {
    "accepted": Json::number(self.accepted.to_double()),
    "filtered": Json::number(self.filtered.to_double()),
    "invalid": Json::number(self.invalid.to_double()),
    "invalid_lines": Json::array(bad_lines),
    "errors": Json::number(self.errors.to_double()),
    "error_rate_pct": Json::number(self.error_rate_pct),
    "p50_ms": Json::number(self.p50_ms),
    "p95_ms": Json::number(self.p95_ms),
    "services": Json::array(service_values),
    "statuses": Json::array(status_values),
  }
}