///|
/// Turning one container description into the argument list `podman` sees.
/// This is the heart of the tool: every Compose field that maps onto a podman
/// flag is translated here.

///|
/// A parsed volume/mount description.
struct Mount {
  kind : String
  source : String?
  target : String
  options : Array[String]
}

///|
/// The argument list for `podman create`/`run`, following the reference
/// implementation's ordering closely enough that the output is comparable.
pub fn container_to_args(
  compose : Compose,
  cnt : @yaml.Yaml,
  detached : Bool,
  no_deps : Bool,
) -> Array[String] raise Error {
  let args : Array[String] = []
  let name = y_str_or(y_get(cnt, "name").unwrap_or(y_null()), "")
  args.push("--name=" + name)
  if detached {
    args.push("-d")
  }
  match y_get(cnt, "pod") {
    Some(pod) =>
      match y_str(pod) {
        Some(p) if p.length() > 0 => args.push("--pod=" + p)
        _ => ()
      }
    None => ()
  }
  if !no_deps {
    let dep_names = y_list_str(y_get(cnt, "_deps").unwrap_or(y_null()))
    if dep_names.length() > 0 {
      let csv : Array[String] = []
      for d in dep_names {
        match compose.container_names_by_service.get(d) {
          Some(names) =>
            for n in names {
              csv.push(n)
            }
          None => ()
        }
      }
      if csv.length() > 0 {
        args.push("--requires=" + csv.join(","))
      }
    }
  }
  for item in y_list_str(y_get(cnt, "security_opt").unwrap_or(y_null())) {
    args.push("--security-opt")
    args.push(item)
  }
  for item in y_list_str(y_get(cnt, "annotations").unwrap_or(y_null())) {
    args.push("--annotation")
    args.push(item)
  }
  if y_bool(y_get(cnt, "read_only").unwrap_or(y_null())).unwrap_or(false) {
    args.push("--read-only")
  }
  match y_get(cnt, "http_proxy") {
    Some(v) => if y_bool(v) is Some(false) { args.push("--http-proxy=false") }
    None => ()
  }
  for label in y_list_str(y_get(cnt, "labels").unwrap_or(y_null())) {
    args.push("--label")
    args.push(label)
  }
  push_pairs(
    args,
    "--cap-add",
    y_list_str(y_get(cnt, "cap_add").unwrap_or(y_null())),
  )
  push_pairs(
    args,
    "--cap-drop",
    y_list_str(y_get(cnt, "cap_drop").unwrap_or(y_null())),
  )
  push_pairs(
    args,
    "--group-add",
    y_list_str(y_get(cnt, "group_add").unwrap_or(y_null())),
  )
  push_pairs(
    args,
    "--device",
    y_list_str(y_get(cnt, "devices").unwrap_or(y_null())),
  )
  push_pairs(
    args,
    "--device-cgroup-rule",
    y_list_str(y_get(cnt, "device_cgroup_rules").unwrap_or(y_null())),
  )
  push_pairs(args, "--dns", y_list_str(y_get(cnt, "dns").unwrap_or(y_null())))
  push_pairs(
    args,
    "--dns-opt",
    y_list_str(y_get(cnt, "dns_opt").unwrap_or(y_null())),
  )
  push_pairs(
    args,
    "--dns-search",
    y_list_str(y_get(cnt, "dns_search").unwrap_or(y_null())),
  )
  // env_file entries are read and expanded inline.
  for env_file in y_list(y_get(cnt, "env_file").unwrap_or(y_null())) {
    let (path, required) = env_file_path(env_file)
    if path.length() == 0 {
      continue
    }
    let full = path_abspath(compose.dirname, path)
    if !@fs.path_exists(full) {
      if required {
        raise fail("env file at \{full} does not exist")
      }
      continue
    }
    for entry in compose_env_file(compose, full) {
      args.push("-e")
      args.push(entry.0 + "=" + entry.1)
    }
  }
  for item in y_env_list(y_get(cnt, "environment").unwrap_or(y_null())) {
    if item.contains_char('=') {
      args.push("-e")
      args.push(item)
    } else {
      match compose.environ.get(item) {
        Some(value) => {
          args.push("-e")
          args.push(item + "=" + value)
        }
        None => ()
      }
    }
  }
  for item in y_list_str(y_get(cnt, "tmpfs").unwrap_or(y_null())) {
    args.push("--tmpfs")
    args.push(item)
  }
  for mount in container_mounts(compose, cnt) {
    for arg in mount_arg(compose, cnt, mount) {
      args.push(arg)
    }
  }
  for arg in network_args(compose, cnt) {
    args.push(arg)
  }
  match y_get(cnt, "logging") {
    Some(logging) =>
      match y_map(logging) {
        Some(lm) => {
          let driver = match lm.get("driver") {
            Some(d) => y_str_or(d, "k8s-file")
            None => "k8s-file"
          }
          args.push("--log-driver=" + driver)
          match lm.get("options") {
            Some(options) =>
              match y_map(options) {
                Some(om) =>
                  for entry in om {
                    args.push(
                      "--log-opt=" + entry.0 + "=" + y_str_or(entry.1, ""),
                    )
                  }
                None => ()
              }
            None => ()
          }
        }
        None => ()
      }
    None => ()
  }
  for secret in y_list(y_get(cnt, "secrets").unwrap_or(y_null())) {
    for arg in secret_args(secret) {
      args.push(arg)
    }
  }
  push_pairs(
    args,
    "--add-host",
    y_list_str(y_get(cnt, "extra_hosts").unwrap_or(y_null())),
  )
  push_pairs(
    args,
    "--expose",
    y_list_str(y_get(cnt, "expose").unwrap_or(y_null())),
  )
  if y_bool(y_get(cnt, "publishall").unwrap_or(y_null())).unwrap_or(false) {
    args.push("-P")
  }
  for port in y_list(y_get(cnt, "ports").unwrap_or(y_null())) {
    match port_to_str(port) {
      Some(spec) => {
        args.push("-p")
        args.push(spec)
      }
      None => ()
    }
  }
  match y_get(cnt, "userns_mode") {
    Some(v) =>
      match y_str(v) {
        Some(s) => {
          args.push("--userns")
          args.push(s)
        }
        None => ()
      }
    None => ()
  }
  match y_get(cnt, "user") {
    Some(v) =>
      match y_str(v) {
        Some(s) => {
          args.push("-u")
          args.push(s)
        }
        None => ()
      }
    None => ()
  }
  match y_get(cnt, "working_dir") {
    Some(v) =>
      match y_str(v) {
        Some(s) => {
          args.push("-w")
          args.push(s)
        }
        None => ()
      }
    None => ()
  }
  push_opt(args, "--hostname", y_get(cnt, "hostname"))
  push_opt(args, "--shm-size", y_get(cnt, "shm_size"))
  if y_bool(y_get(cnt, "stdin_open").unwrap_or(y_null())).unwrap_or(false) {
    args.push("-i")
  }
  push_opt(args, "--stop-signal", y_get(cnt, "stop_signal"))
  match y_get(cnt, "stop_grace_period") {
    Some(v) =>
      match y_str(v) {
        Some(s) =>
          match str_to_seconds(s) {
            Some(seconds) => {
              args.push("--stop-timeout")
              args.push(seconds.to_string())
            }
            None => ()
          }
        None => ()
      }
    None => ()
  }
  match y_get(cnt, "sysctls") {
    Some(v) =>
      match v {
        @yaml.Yaml::Map(m) =>
          for entry in m {
            args.push("--sysctl")
            args.push(entry.0 + "=" + y_str_or(entry.1, ""))
          }
        @yaml.Yaml::Array(_) => push_pairs(args, "--sysctl", y_list_str(v))
        _ => ()
      }
    None => ()
  }
  if y_bool(y_get(cnt, "tty").unwrap_or(y_null())).unwrap_or(false) {
    args.push("--tty")
  }
  if y_bool(y_get(cnt, "privileged").unwrap_or(y_null())).unwrap_or(false) {
    args.push("--privileged")
  }
  push_opt(args, "--pid", y_get(cnt, "pid"))
  match y_get(cnt, "pull_policy") {
    Some(v) =>
      match y_str(v) {
        Some(policy) => if policy != "build" { args.push("--pull=" + policy) }
        None => ()
      }
    None => ()
  }
  push_opt(args, "--restart", y_get(cnt, "restart"))
  for ulimit in ulimit_args(y_get(cnt, "ulimits").unwrap_or(y_null())) {
    args.push("--ulimit")
    args.push(ulimit)
  }
  for arg in resource_args(cnt) {
    args.push(arg)
  }
  if y_bool(y_get(cnt, "init").unwrap_or(y_null())).unwrap_or(false) {
    args.push("--init")
  }
  push_opt(args, "--init-path", y_get(cnt, "init-path"))
  push_opt(args, "--ipc", y_get(cnt, "ipc"))
  match y_get(cnt, "entrypoint") {
    Some(v) =>
      match v {
        @yaml.Yaml::String(s) => {
          args.push("--entrypoint")
          args.push(json_string_array(shell_split(s)))
        }
        @yaml.Yaml::Array(_) => {
          args.push("--entrypoint")
          args.push(json_string_array(y_list_str(v)))
        }
        _ => ()
      }
    None => ()
  }
  push_opt(args, "--platform", y_get(cnt, "platform"))
  push_opt(args, "--runtime", y_get(cnt, "runtime"))
  push_opt(args, "--cpuset-cpus", y_get(cnt, "cpuset"))
  for arg in healthcheck_args(y_get(cnt, "healthcheck")) {
    args.push(arg)
  }
  for uidmap in y_list_str(y_get(cnt, "x-podman.uidmaps").unwrap_or(y_null())) {
    args.push("--uidmap")
    args.push(uidmap)
  }
  for gidmap in y_list_str(y_get(cnt, "x-podman.gidmaps").unwrap_or(y_null())) {
    args.push("--gidmap")
    args.push(gidmap)
  }
  if y_bool(y_get(cnt, "x-podman.no_hosts").unwrap_or(y_null())).unwrap_or(
      false,
    ) {
    args.push("--no-hosts")
  }
  // The image, then the command.
  match y_get(cnt, "x-podman.rootfs") {
    Some(rootfs) =>
      match y_str(rootfs) {
        Some(s) => {
          args.push("--rootfs")
          args.push(s)
        }
        None => ()
      }
    None =>
      match y_get(cnt, "image") {
        Some(image) => args.push(y_str_or(image, ""))
        None => ()
      }
  }
  match y_get(cnt, "command") {
    Some(v) =>
      match v {
        @yaml.Yaml::String(s) =>
          for part in shell_split(s) {
            args.push(part)
          }
        _ =>
          for part in y_list_str(v) {
            args.push(part)
          }
      }
    None => ()
  }
  args
}

///|
/// Append `flag value` for every value in `values`.
fn push_pairs(
  args : Array[String],
  flag : String,
  values : Array[String],
) -> Unit {
  for v in values {
    args.push(flag)
    args.push(v)
  }
}

///|
/// Append `flag value` when a scalar is present.
fn push_opt(args : Array[String], flag : String, value : @yaml.Yaml?) -> Unit {
  match value {
    Some(v) =>
      match y_str(v) {
        Some(s) => {
          args.push(flag)
          args.push(s)
        }
        None => ()
      }
    None => ()
  }
}

///|
/// Split an `env_file` entry into its path and whether it is required.
fn env_file_path(entry : @yaml.Yaml) -> (String, Bool) {
  match entry {
    @yaml.Yaml::String(s) => (s, true)
    @yaml.Yaml::Map(m) => {
      let path = match m.get("path") {
        Some(p) => y_str_or(p, "")
        None => ""
      }
      let required = match m.get("required") {
        Some(r) => y_bool(r).unwrap_or(true)
        None => true
      }
      (path, required)
    }
    _ => ("", true)
  }
}

///|
/// Render a `ports` entry as `[[ip:]published:]target[/proto]`.
fn port_to_str(port : @yaml.Yaml) -> String? {
  match port {
    @yaml.Yaml::String(s) => Some(s)
    @yaml.Yaml::Integer(i) => Some(i.to_string())
    @yaml.Yaml::Map(m) => {
      let target = match m.get("target") {
        Some(t) => y_str_or(t, "")
        None => ""
      }
      if target.length() == 0 {
        return None
      }
      let protocol = match m.get("protocol") {
        Some(p) => y_str_or(p, "tcp")
        None => "tcp"
      }
      let out = StringBuilder()
      match m.get("host_ip") {
        Some(ip) =>
          match y_str(ip) {
            Some(s) if s.length() > 0 => {
              out.write_string(s)
              out.write_char(':')
            }
            _ => ()
          }
        None => ()
      }
      match m.get("published") {
        Some(published) =>
          match y_str(published) {
            Some(s) if s.length() > 0 => {
              out.write_string(s)
              out.write_char(':')
            }
            _ => ()
          }
        None => ()
      }
      out.write_string(target)
      out.write_char('/')
      out.write_string(protocol)
      Some(out.to_string())
    }
    _ => None
  }
}

///|
/// Secret references become `--secret` flags.
fn secret_args(secret : @yaml.Yaml) -> Array[String] {
  let args : Array[String] = []
  match secret {
    @yaml.Yaml::String(s) => {
      args.push("--secret")
      args.push(s)
    }
    @yaml.Yaml::Map(m) => {
      let source = match m.get("source") {
        Some(s) => y_str_or(s, "")
        None => ""
      }
      if source.length() == 0 {
        return args
      }
      let spec = StringBuilder()
      spec.write_string(source)
      match m.get("target") {
        Some(t) =>
          match y_str(t) {
            Some(target) if target.length() > 0 =>
              spec.write_string(",target=" + target)
            _ => ()
          }
        None => ()
      }
      args.push("--secret")
      args.push(spec.to_string())
    }
    _ => ()
  }
  args
}

///|
/// `ulimits` in either the scalar, list or mapping form.
fn ulimit_args(v : @yaml.Yaml) -> Array[String] {
  let out : Array[String] = []
  match v {
    @yaml.Yaml::Null | @yaml.Yaml::BadValue => ()
    @yaml.Yaml::String(s) => out.push(s)
    @yaml.Yaml::Map(m) =>
      for entry in m {
        out.push(entry.0 + "=" + norm_ulimit(entry.1))
      }
    @yaml.Yaml::Array(_) =>
      for entry in y_dict(v) {
        out.push(entry.0 + "=" + norm_ulimit(entry.1))
      }
    _ => ()
  }
  out
}

///|
fn norm_ulimit(v : @yaml.Yaml) -> String {
  match v {
    @yaml.Yaml::Map(m) => {
      let soft = match m.get("soft") {
        Some(s) => y_str_or(s, "")
        None => ""
      }
      let hard = match m.get("hard") {
        Some(h) => y_str_or(h, "")
        None => soft
      }
      let soft = if soft.length() == 0 { hard } else { soft }
      soft + ":" + hard
    }
    _ => y_str_or(v, "")
  }
}

///|
/// CPU and memory limits from `deploy.resources.limits`.
fn resource_args(cnt : @yaml.Yaml) -> Array[String] {
  let out : Array[String] = []
  match y_get(cnt, "deploy") {
    Some(deploy) =>
      match y_get(deploy, "resources") {
        Some(resources) =>
          match y_get(resources, "limits") {
            Some(limits) => {
              match y_get(limits, "cpus") {
                Some(cpus) =>
                  match y_str(cpus) {
                    Some(s) => {
                      out.push("--cpus")
                      out.push(s)
                    }
                    None => ()
                  }
                None => ()
              }
              match y_get(limits, "memory") {
                Some(memory) =>
                  match y_str(memory) {
                    Some(s) => {
                      out.push("--memory")
                      out.push(s)
                    }
                    None => ()
                  }
                None => ()
              }
            }
            None => ()
          }
        None => ()
      }
    None => ()
  }
  out
}

///|
/// The `healthcheck` block.
fn healthcheck_args(healthcheck : @yaml.Yaml?) -> Array[String] {
  let out : Array[String] = []
  let hm = match healthcheck.bind(y_map) {
    Some(m) => m
    None => return out
  }
  let disable = match hm.get("disable") {
    Some(d) => y_bool(d).unwrap_or(false)
    None => false
  }
  if disable {
    out.push("--no-healthcheck")
  } else {
    match hm.get("test") {
      Some(hc) => apply_healthcheck_test(out, hc)
      None => ()
    }
  }
  push_opt(out, "--health-interval", hm.get("interval"))
  push_opt(out, "--health-timeout", hm.get("timeout"))
  push_opt(out, "--health-start-period", hm.get("start_period"))
  push_opt(out, "--health-startup-interval", hm.get("start_interval"))
  match hm.get("retries") {
    Some(r) =>
      match y_str(r) {
        Some(s) => {
          out.push("--health-retries")
          out.push(s)
        }
        None => ()
      }
    None => ()
  }
  out
}

///|
/// The `test:` of a healthcheck: a shell string, or a `CMD`/`CMD-SHELL` list.
fn apply_healthcheck_test(out : Array[String], hc : @yaml.Yaml) -> Unit {
  match hc {
    @yaml.Yaml::String(s) => {
      out.push("--health-cmd")
      out.push(json_string_array(["CMD-SHELL", s]))
    }
    @yaml.Yaml::Array(_) => {
      let parts = y_list_str(hc)
      if parts.length() > 0 {
        let kind = parts[0]
        let rest : Array[String] = []
        for i in 1.. ()
  }
}

///|
/// Convert `"90s"`, `"1m30s"`, `"500ms"` or a bare number to seconds.
pub fn str_to_seconds(text : String) -> Int32? {
  let s = text.trim().to_owned()
  if s.length() == 0 {
    return None
  }
  let mut total = 0
  let number = StringBuilder()
  let mut i = 0
  let chars = s.to_array()
  let mut saw_unit = false
  while i < chars.length() {
    let c = chars[i]
    if (c >= '0' && c <= '9') || c == '.' {
      number.write_char(c)
      i += 1
      continue
    }
    let value = @string.parse_double(number.to_string()) catch {
      _ => return None
    }
    number.reset()
    saw_unit = true
    match c {
      'h' => total = total + (value * 3600.0).to_int()
      'm' =>
        // `ms` is milliseconds; a lone `m` is minutes.
        if i + 1 < chars.length() && chars[i + 1] == 's' {
          i += 1
        } else {
          total = total + (value * 60.0).to_int()
        }
      's' => total = total + value.to_int()
      _ => return None
    }
    i += 1
  }
  if !number.is_empty() {
    let value = @string.parse_double(number.to_string()) catch {
      _ => return None
    }
    total = total + value.to_int()
  } else if !saw_unit {
    return None
  }
  Some(total)
}

///|
/// A JSON array of strings, for `--entrypoint` and `--health-cmd`, which podman
/// expects in that form.
fn json_string_array(items : Array[String]) -> String {
  let out = StringBuilder()
  out.write_char('[')
  let mut first = true
  for item in items {
    if !first {
      out.write_char(',')
    }
    first = false
    out.write_char('"')
    for c in item {
      match c {
        '"' => out.write_string("\\\"")
        '\\' => out.write_string("\\\\")
        '\n' => out.write_string("\\n")
        '\t' => out.write_string("\\t")
        '\r' => out.write_string("\\r")
        _ => out.write_char(c)
      }
    }
    out.write_char('"')
  }
  out.write_char(']')
  out.to_string()
}