///|
/// Delivery entry points: render one message for one target and push it over
/// the network. None means the endpoint accepted it; Some carries a
/// credential-free reason.

///|
/// Deliver one message to many targets, one request each, sequentially.
/// Returns `(target name, failure)` pairs in input order.
pub async fn deliver_all(
  targets : Array[Target],
  msg : Message,
  timeout_ms : Int,
  ctx : Context,
) -> Array[(String, String?)] {
  let results = []
  for target in targets {
    results.push((target.name, deliver(target, msg, timeout_ms, ctx)))
  }
  results
}

///|
/// Deliver one message to one target.
pub async fn deliver(
  target : Target,
  msg : Message,
  timeout_ms : Int,
  ctx : Context,
) -> String? {
  let delivery = try render_delivery(target, msg, ctx) catch {
    e => return Some(e.to_string())
  } noraise {
    rendered => rendered
  }
  transport(target, delivery, timeout_ms, ctx)
}

///|
/// 飞书、钉钉、企业微信都是"HTTP 200 + 响应体错误码":只看状态码会把 token
/// 错误当成投递成功。应答形状对不上(空 body、纯文本)一律视为成功,避免把
/// 不认识的结构误报为失败。
fn body_failure(preset : String, body : String) -> String? {
  let code_key = match preset {
    "feishu" => "code"
    "dingtalk" | "wecom" => "errcode"
    _ => return None
  }
  let parsed = try @json.parse(body) catch {
    _ => return None
  } noraise {
    value => value
  }
  guard parsed is Object(fields) else { return None }
  let code = match fields.get(code_key) {
    Some(Number(value, ..)) => value.to_int()
    _ => return None
  }
  if code == 0 {
    return None
  }
  let mut reason = preset + " code " + code.to_string()
  match fields.get("msg") {
    Some(String(text)) if text != "" => reason = reason + ": " + text
    _ => ()
  }
  match fields.get("errmsg") {
    Some(String(text)) if text != "" => reason = reason + ": " + text
    _ => ()
  }
  Some(reason)
}

///|
/// Route one rendered request: JSON presets over HTTP, email over SMTP.
async fn transport(
  target : Target,
  delivery : Delivery,
  timeout_ms : Int,
  ctx : Context,
) -> String? {
  match delivery.mail {
    Some(mail) => smtp_deliver(target, delivery, mail, ctx.host, timeout_ms)
    None => http_deliver(target, delivery, timeout_ms)
  }
}

///|
/// HTTP presets: one request, whole-call timeout. Non-2xx is a failure; for
/// feishu/dingtalk/wecom the response body is inspected too, because they
/// report credential errors as "200 + errcode".
async fn http_deliver(
  target : Target,
  delivery : Delivery,
  timeout_ms : Int,
) -> String? {
  let headers : Map[@http.CaseInsensitiveString, String] = Map([])
  match delivery.content_type {
    Some(content_type) =>
      headers[@http.CaseInsensitiveString("Content-Type")] = content_type
    None => ()
  }
  for pair in delivery.headers {
    let (key, value) = pair
    headers[@http.CaseInsensitiveString(key)] = value
  }
  let exchange = async fn() {
    match delivery.http_method {
      "GET" => @http.get(delivery.url, headers~)
      "PUT" => @http.put(delivery.url, delivery.body, headers~)
      _ => @http.post(delivery.url, delivery.body, headers~)
    }
  }
  let answer = try @async.with_timeout_opt(timeout_ms, exchange) catch {
    e => return Some(e.to_string())
  } noraise {
    answer => answer
  }
  match answer {
    None => Some("did not finish within " + timeout_ms.to_string() + "ms")
    Some((resp, body)) => {
      if resp.code < 200 || resp.code >= 300 {
        return Some("HTTP " + resp.code.to_string() + " " + resp.reason)
      }
      body_failure(target.preset, @utf8.decode_lossy(body.binary()))
    }
  }
}