///|
/// Domain aggregate.
pub(all) struct DomainStat {
  domain : String
  count : Int
  bytes : Int
  failures : Int
} derive(Eq, Debug)

///|
/// Status-code aggregate.
pub(all) struct StatusStat {
  group : String
  count : Int
} derive(Eq, Debug)

///|
/// Resource-type aggregate.
pub(all) struct ResourceStat {
  kind : String
  count : Int
  bytes : Int
} derive(Eq, Debug)

///|
/// Slow request projection.
pub(all) struct SlowEntry {
  url : String
  http_method : String
  status : Int
  time : Int
  wait : Int
  receive : Int
} derive(Eq, Debug)

///|
/// Main summary returned by the analyzer.
pub(all) struct HarSummary {
  entry_count : Int
  page_count : Int
  failed_count : Int
  redirected_count : Int
  cached_count : Int
  total_bytes : Int
  total_time : Int
  longest_time : Int
  average_time : Int
  domains : Array[DomainStat]
  statuses : Array[StatusStat]
  resources : Array[ResourceStat]
  slow_entries : Array[SlowEntry]
} derive(Eq, Debug)

///|
pub fn analyze_archive(archive : HarArchive) -> HarSummary {
  let domains : Array[DomainStat] = []
  let statuses : Array[StatusStat] = []
  let resources : Array[ResourceStat] = []
  let slow : Array[SlowEntry] = []
  let mut failed_count = 0
  let mut redirected_count = 0
  let mut cached_count = 0
  let mut total_bytes = 0
  let mut total_time = 0
  let mut longest_time = 0
  for i = 0; i < archive.log.entries.length(); i = i + 1 {
    let entry = archive.log.entries[i]
    let bytes = entry_bytes(entry)
    total_bytes = total_bytes + bytes
    total_time = total_time + entry.time
    if entry.time > longest_time {
      longest_time = entry.time
    }
    if entry.response.status >= 400 {
      failed_count = failed_count + 1
    }
    if entry.response.status >= 300 && entry.response.status < 400 {
      redirected_count = redirected_count + 1
    }
    if entry.response.status == 304 ||
      (entry.response.body_size == 0 && entry.response.content.size == 0) {
      cached_count = cached_count + 1
    }
    bump_domain(
      domains,
      extract_domain(entry.request.url),
      bytes,
      entry.response.status >= 400,
    )
    bump_status(statuses, status_group(entry.response.status))
    bump_resource(
      resources,
      resource_kind(entry.response.content.mime_type, entry.request.url),
      bytes,
    )
    insert_slow(
      slow,
      SlowEntry::{
        url: entry.request.url,
        http_method: entry.request.http_method,
        status: entry.response.status,
        time: entry.time,
        wait: entry.timings.wait,
        receive: entry.timings.receive,
      },
      10,
    )
  }
  HarSummary::{
    entry_count: archive.log.entries.length(),
    page_count: archive.log.pages.length(),
    failed_count,
    redirected_count,
    cached_count,
    total_bytes,
    total_time,
    longest_time,
    average_time: if archive.log.entries.length() == 0 {
      0
    } else {
      total_time / archive.log.entries.length()
    },
    domains,
    statuses,
    resources,
    slow_entries: slow,
  }
}

///|
pub fn entry_bytes(entry : HarEntry) -> Int {
  known_size(entry.request.headers_size) +
  known_size(entry.request.body_size) +
  known_size(entry.response.headers_size) +
  known_size(entry.response.body_size) +
  entry.response.content.size
}

///|
fn known_size(value : Int) -> Int {
  if value > 0 {
    value
  } else {
    0
  }
}

///|
pub fn extract_domain(url : String) -> String {
  let no_scheme = match url.split_once("://") {
    Some((_, rest)) => rest.to_owned()
    None => url
  }
  let host = match no_scheme.split_once("/") {
    Some((left, _)) => left.to_owned()
    None => no_scheme
  }
  match host.split_once(":") {
    Some((left, _)) => left.to_owned()
    None => host
  }
}

///|
pub fn status_group(status : Int) -> String {
  if status < 100 {
    "other"
  } else if status < 200 {
    "1xx"
  } else if status < 300 {
    "2xx"
  } else if status < 400 {
    "3xx"
  } else if status < 500 {
    "4xx"
  } else if status < 600 {
    "5xx"
  } else {
    "other"
  }
}

///|
pub fn resource_kind(mime : String, url : String) -> String {
  let lower = mime.to_lower()
  if lower.contains("html") {
    "document"
  } else if lower.contains("javascript") ||
    lower.contains("ecmascript") ||
    url.has_suffix(".js") {
    "script"
  } else if lower.contains("css") || url.has_suffix(".css") {
    "style"
  } else if lower.contains("image") ||
    url.has_suffix(".png") ||
    url.has_suffix(".jpg") ||
    url.has_suffix(".jpeg") ||
    url.has_suffix(".webp") ||
    url.has_suffix(".gif") {
    "image"
  } else if lower.contains("json") {
    "json"
  } else if lower.contains("font") ||
    url.has_suffix(".woff") ||
    url.has_suffix(".woff2") {
    "font"
  } else if lower.contains("video") {
    "video"
  } else if lower.contains("audio") {
    "audio"
  } else {
    "other"
  }
}

///|
fn bump_domain(
  stats : Array[DomainStat],
  domain : String,
  bytes : Int,
  failed : Bool,
) -> Unit {
  for i = 0; i < stats.length(); i = i + 1 {
    if stats[i].domain == domain {
      let fail_add = if failed { 1 } else { 0 }
      stats[i] = DomainStat::{
        domain,
        count: stats[i].count + 1,
        bytes: stats[i].bytes + bytes,
        failures: stats[i].failures + fail_add,
      }
      return
    }
  }
  let fail_add = if failed { 1 } else { 0 }
  stats.push(DomainStat::{ domain, count: 1, bytes, failures: fail_add })
}

///|
fn bump_status(stats : Array[StatusStat], group : String) -> Unit {
  for i = 0; i < stats.length(); i = i + 1 {
    if stats[i].group == group {
      stats[i] = StatusStat::{ group, count: stats[i].count + 1 }
      return
    }
  }
  stats.push(StatusStat::{ group, count: 1 })
}

///|
fn bump_resource(
  stats : Array[ResourceStat],
  kind : String,
  bytes : Int,
) -> Unit {
  for i = 0; i < stats.length(); i = i + 1 {
    if stats[i].kind == kind {
      stats[i] = ResourceStat::{
        kind,
        count: stats[i].count + 1,
        bytes: stats[i].bytes + bytes,
      }
      return
    }
  }
  stats.push(ResourceStat::{ kind, count: 1, bytes })
}

///|
fn insert_slow(items : Array[SlowEntry], item : SlowEntry, limit : Int) -> Unit {
  let mut inserted = false
  for i = 0; i < items.length(); i = i + 1 {
    if item.time > items[i].time {
      items.insert(i, item)
      inserted = true
      break
    }
  }
  if !inserted {
    items.push(item)
  }
  while items.length() > limit {
    ignore(items.pop())
  }
}

///|
pub fn summary_line(summary : HarSummary) -> String {
  "entries=" +
  summary.entry_count.to_string() +
  " pages=" +
  summary.page_count.to_string() +
  " failed=" +
  summary.failed_count.to_string() +
  " bytes=" +
  summary.total_bytes.to_string() +
  " avg_ms=" +
  summary.average_time.to_string()
}