///|
/// example/mailer — compose a PGP signed+encrypted mail (body +
/// attachments) and hand it to a transport. See plan v2
/// (docs/plan-example-mailer.html) and HANDOFF §8.

///|
/// Callback recorder for --roundtrip (compose → self-decrypt).
struct RoundtripRecorder {
  bodies : Array[(String, String)]
  attachments : Array[(String, Bytes)]
  verified : Array[@api.VerifyStatus]
  errors : Array[String]
}

///|
fn new_recorder() -> RoundtripRecorder {
  RoundtripRecorder::{ bodies: [], attachments: [], verified: [], errors: [], }
}

///|
impl @mime.MimeCallbacks for RoundtripRecorder with fn on_body(
  self,
  body,
  mimetype,
) {
  self.bodies.push((body, mimetype))
}

///|
impl @mime.MimeCallbacks for RoundtripRecorder with fn on_attachment(
  self,
  headers,
  data,
) {
  let mut name = "?"
  for pair in headers {
    if pair.0.to_lower() == "content-disposition" &&
      pair.1.contains("filename=") {
      name = pair.1
    }
  }
  self.attachments.push((name, data))
}

///|
impl @mime.MimeCallbacks for RoundtripRecorder with fn on_verified(self, status) {
  self.verified.push(status)
}

///|
impl @mime.MimeCallbacks for RoundtripRecorder with fn on_error(self, message) {
  self.errors.push(message)
}

///|
struct Config {
  mut from : @mail.MailAddress?
  to : Array[@mail.MailAddress]
  mut subject : String
  mut body_file : String?
  attachments : Array[@mail.MailAttachment]
  mut sender_key : String?
  mut sender_key_pass_env : String?
  mut recipient_cert : String?
  mut domain : String?
  mut mode : String
  mut smtp_host : String
  mut smtp_port : Int32
  mut smtp_user : String?
  mut smtp_pass_env : String?
  mut smtp_tls : String
  mut smtp_trust : String
  mut out : String
  mut roundtrip : Bool
}

///|
fn new_config() -> Config {
  Config::{
    from: None,
    to: [],
    subject: "",
    body_file: None,
    attachments: [],
    sender_key: None,
    sender_key_pass_env: None,
    recipient_cert: None,
    domain: None,
    mode: "file",
    smtp_host: "127.0.0.1",
    smtp_port: 2525,
    smtp_user: None,
    smtp_pass_env: None,
    smtp_tls: "auto",
    smtp_trust: "system",
    out: "mail.eml",
    roundtrip: false,
  }
}

///|
fn usage() -> Unit {
  let lines = [
    "usage: moon run example/mailer -- [flags]", "  --from \"Name \"              sender mailbox (required)",
    "  --to \"Name \"                recipient, repeatable (required)", "  --subject TEXT                   ASCII subject (required)",
    "  --body-file PATH                 text body file (required)", "  --attach NAME:PATH               attachment, repeatable",
    "  --sender-key PATH                armored private key (required)", "  --sender-key-pass-env NAME       env var holding the key passphrase",
    "  --recipient-cert PATH            armored public key (required)", "  --domain DOM                     Message-ID / EHLO domain (required)",
    "  --mode smtp|file                 transport (default: file)", "  --smtp-host H --smtp-port N      default 127.0.0.1:2525",
    "  --smtp-user U --smtp-pass-env N  AUTH PLAIN creds (env var name)", "  --smtp-tls auto|implicit|starttls|none",
    "                                 TLS mode (default auto: 465 -> implicit,",
    "  587 -> starttls, otherwise none)", "  --smtp-trust system|pem:FILE|none  peer cert verification (default system)",
    "  --out PATH                       file-mode output (default mail.eml)", "  --roundtrip                      compose then self-decrypt (offline check)",
  ]
  for l in lines {
    println(l)
  }
}

///|
fn parse_address(s : String) -> @mail.MailAddress raise {
  let t = s.trim().to_owned()
  guard t != "" else { @error.fail("mailer: empty --from/--to value") }
  match t.split_once("<") {
    Some((name, rest)) => {
      guard rest.has_suffix(">") else {
        @error.fail("mailer: bad address '\{s}'")
      }
      let addr = rest[0:rest.length() - 1].trim().to_owned()
      @mail.MailAddress::{ display_name: name.trim().to_owned(), addr, }
    }
    None => @mail.MailAddress::{ display_name: "", addr: t, }
  }
}

///|
fn parse_args(argv : Array[String]) -> Config raise {
  let cfg = new_config()
  let mut i = 0
  while i < argv.length() {
    let flag = argv[i]
    let val = if flag == "--roundtrip" {
      ""
    } else {
      guard i + 1 < argv.length() else {
        @error.fail("mailer: \{flag} needs a value")
      }
      argv[i + 1]
    }
    match flag {
      "--from" => cfg.from = Some(parse_address(val))
      "--to" => cfg.to.push(parse_address(val))
      "--subject" => cfg.subject = val
      "--body-file" => cfg.body_file = Some(val)
      "--attach" => {
        let spec = val
        // split at the LAST colon so paths may contain colons
        let mut cut = -1
        for k, c in spec {
          if c == ':' {
            cut = k
          }
        }
        guard cut > 0 else {
          @error.fail("mailer: --attach expects NAME:PATH, got '\{spec}'")
        }
        let name = spec[0:cut].to_owned()
        let path = spec[cut + 1:spec.length()].to_owned()
        cfg.attachments.push(@mail.MailAttachment::{
          filename: name,
          content_type: "application/octet-stream",
          data: @fs.read_file_to_bytes(path),
        })
      }
      "--sender-key" => cfg.sender_key = Some(val)
      "--sender-key-pass-env" => cfg.sender_key_pass_env = Some(val)
      "--recipient-cert" => cfg.recipient_cert = Some(val)
      "--domain" => cfg.domain = Some(val)
      "--mode" => {
        let m = val
        guard m == "smtp" || m == "file" else {
          @error.fail("mailer: --mode must be smtp|file")
        }
        cfg.mode = m
      }
      "--smtp-host" => cfg.smtp_host = val
      "--smtp-port" => {
        let p = val
        let mut v = 0
        for c in p {
          let d = c.to_int() - 48
          guard d >= 0 && d <= 9 else {
            @error.fail("mailer: bad --smtp-port '\{p}'")
          }
          v = v * 10 + d
        }
        cfg.smtp_port = v
      }
      "--smtp-user" => cfg.smtp_user = Some(val)
      "--smtp-pass-env" => cfg.smtp_pass_env = Some(val)
      "--smtp-tls" => cfg.smtp_tls = val
      "--smtp-trust" => cfg.smtp_trust = val
      "--out" => cfg.out = val
      "--roundtrip" => cfg.roundtrip = true
      _ => @error.fail("mailer: unknown flag '\{flag}'")
    }
    i = i + (if flag == "--roundtrip" { 1 } else { 2 })
  }
  cfg
}

///|
fn require_env(name : String) -> String raise {
  match @env.get_env_var(name) {
    Some(v) => v
    None => @error.fail("mailer: environment variable \{name} is not set")
  }
}

///|
fn load_recipient_public(path : String) -> @api.PublicKey raise {
  let text = @fs.read_file_to_string(path)
  @api.PublicKey::from_armored(text) catch {
    _ => @api.PrivateKey::from_armored(text).to_public()
  }
}

///|
/// Resolve --smtp-tls: explicit mode, or infer from the port (465 ->
/// implicit, 587 -> starttls, otherwise plaintext) per plan
/// tls-transport §3.5.
fn resolve_tls_mode(flag : String, port : Int32) -> @netmail.SmtpTlsMode? raise {
  match flag {
    "implicit" => Some(@netmail.Implicit)
    "starttls" => Some(@netmail.Starttls)
    "none" => None
    "auto" =>
      match port {
        465 => Some(@netmail.Implicit)
        587 => Some(@netmail.Starttls)
        _ => None
      }
    other =>
      @error.fail(
        "mailer: --smtp-tls must be auto|implicit|starttls|none, got '\{other}'",
      )
  }
}

///|
/// Resolve --smtp-trust: system roots (default), a private PEM anchor
/// (e2e: self-signed sink), or explicit no verification.
fn parse_trust(flag : String) -> @netmail.SmtpTrust raise {
  if flag == "system" {
    @netmail.System
  } else if flag == "none" {
    @netmail.Insecure
  } else if flag.has_prefix("pem:") {
    @netmail.PemFile(flag[4:].to_owned())
  } else {
    @error.fail(
      "mailer: --smtp-trust must be system|pem:FILE|none, got '\{flag}'",
    )
  }
}

///|
async fn run(cfg : Config) -> Unit {
  let from = match cfg.from {
    Some(f) => f
    None => @error.fail("mailer: --from is required")
  }
  guard cfg.subject != "" else { @error.fail("mailer: --subject is required") }
  guard !cfg.to.is_empty() else { @error.fail("mailer: --to is required") }
  let body_file = match cfg.body_file {
    Some(f) => f
    None => @error.fail("mailer: --body-file is required")
  }
  let sender_key_path = match cfg.sender_key {
    Some(f) => f
    None => @error.fail("mailer: --sender-key is required")
  }
  let cert_path = match cfg.recipient_cert {
    Some(f) => f
    None => @error.fail("mailer: --recipient-cert is required")
  }
  let domain = match cfg.domain {
    Some(d) => d
    None => @error.fail("mailer: --domain is required")
  }
  let body = @fs.read_file_to_string(body_file)
  let key = @api.PrivateKey::from_armored(
    @fs.read_file_to_string(sender_key_path),
  )
  let sender = match cfg.sender_key_pass_env {
    Some(env_name) => key.unlock(require_env(env_name))
    None => key
  }
  let recipient = load_recipient_public(cert_path)
  let message = @mail.build_encrypted_mail(
    from,
    cfg.to,
    subject=cfg.subject,
    text=body,
    attachments=cfg.attachments,
    signer=sender,
    recipients=[recipient],
    domain~,
  )
  if cfg.roundtrip {
    let cb = new_recorder()
    @mime.decrypt_mime(
      message,
      [sender],
      verifiers=[sender.to_public()],
      callbacks=cb,
    )
    if cb.errors.length() != 0 {
      for e in cb.errors {
        eprintln("roundtrip error: \{e}")
      }
      abort("roundtrip FAILED")
    }
    match cb.verified {
      [@api.Ok] => {
        println(
          "roundtrip: signature Ok, \{cb.bodies.length()} body, \{cb.attachments.length()} attachment(s)",
        )
        for b in cb.bodies {
          println("body[\{b.1}] = \{b.0}")
        }
        for a in cb.attachments {
          println("attachment: \{a.0} (\{a.1.length()} bytes)")
        }
        return
      }
      other => {
        println("roundtrip status: \{@netmail.status_names(other)}")
        abort("roundtrip FAILED")
      }
    }
  }
  let envelope_from = from.addr
  let envelope_rcpts : Array[String] = []
  for a in cfg.to {
    envelope_rcpts.push(a.addr)
  }
  let status : String = match cfg.mode {
    "file" => {
      guard cfg.smtp_tls == "auto" && cfg.smtp_trust == "system" else {
        @error.fail(
          "mailer: --smtp-tls/--smtp-trust apply only to --mode smtp (silently ignored before; review 2026-10-06 P3)",
        )
      }
      let t = @netmail.FileTransport::{ out_path: cfg.out, }
      t.send(message, envelope_from, envelope_rcpts)
      "delivered via file transport (\{cfg.out})"
    }
    "smtp" => {
      let user = cfg.smtp_user
      let password = match cfg.smtp_pass_env {
        Some(name) => Some(require_env(name))
        None => None
      }
      match resolve_tls_mode(cfg.smtp_tls, cfg.smtp_port) {
        Some(mode) => {
          let t = @netmail.TlsTransport::{
            host: cfg.smtp_host,
            port: cfg.smtp_port,
            mode,
            trust: parse_trust(cfg.smtp_trust),
            ehlo_domain: domain,
            user,
            password,
          }
          t.send(message, envelope_from, envelope_rcpts)
          let mode_name = if mode is @netmail.Implicit {
            "implicit"
          } else {
            "starttls"
          }
          "delivered via smtp+tls transport (tls=\{mode_name}, trust=\{cfg.smtp_trust})"
        }
        None => {
          guard cfg.smtp_trust == "system" else {
            @error.fail("mailer: --smtp-trust applies only to TLS submission")
          }
          let t = @netmail.SmtpTransport::{
            host: cfg.smtp_host,
            port: cfg.smtp_port,
            ehlo_domain: domain,
            user,
            password,
          }
          t.send(message, envelope_from, envelope_rcpts)
          "delivered via smtp transport (no TLS)"
        }
      }
    }
    other => abort("mailer: unknown mode '\{other}'")
  }
  println(status)
}

///|
async fn main {
  let args = @env.args()
  // Locate the first flag: moon run prefixes argv with its own tokens.
  let mut start = args.length()
  for i, a in args {
    if a.has_prefix("--") {
      start = i
      break
    }
  }
  let rest = args[start:args.length()].to_owned()
  if rest.is_empty() {
    usage()
    abort("mailer: no arguments")
  }
  // Argument parsing is synchronous (no suspension), so a catch here
  // is safe. The transport call is NOT wrapped: the async coroutine
  // lowering mishandles resumption when a catch handler frame is live
  // across a suspension (toolchain bug, see HANDOFF tls entry), so
  // transport errors propagate out of `async fn main raise` instead.
  let cfg = parse_args(rest) catch {
    e => {
      eprintln("\{e}")
      usage()
      abort("mailer: bad arguments")
    }
  }
  run(cfg)
}