///|
/// example/mailreader โ€” fetch mail over IMAP and decrypt+verify it
/// through @mime.decrypt_mime. Plan: docs/mail-fetch-plan.md (v1.1).
/// Plaintext policy mirrors the mailer: non-local hosts require the
/// explicit --allow-insecure opt-in; the recommended real-world path
/// is a local stunnel tunnel to the provider's IMAPS port.

///|
/// Callback recorder for decrypt_mime delivery (same shape as the
/// mailer's roundtrip recorder).
struct FetchRecorder {
  bodies : Array[(String, String)]
  attachments : Array[(String, Bytes)]
  verified : Array[@api.VerifyStatus]
  errors : Array[String]
}

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

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

///|
impl @mime.MimeCallbacks for FetchRecorder with fn on_attachment(
  self,
  headers,
  data,
) {
  let mut name = "attachment-\{self.attachments.length() + 1}"
  for pair in headers {
    if pair.0.to_lower() == "content-disposition" &&
      pair.1.contains("filename=") {
      let v = pair.1
      let cut = v.split("filename=").collect()
      if cut.length() == 2 {
        // stop at the next parameter separator, then strip quotes
        let seg = cut[1].split(";").collect()
        let candidate = seg[0].trim().trim(chars="\"").to_owned()
        // C1 (review): attachments are delivered regardless of the
        // signature outcome, and filenames come from the wire โ€” a
        // "../" name would escape --out. Basename-only whitelist.
        let sanitized = sanitize_filename(candidate)
        if sanitized != "" {
          name = sanitized
        }
      }
    }
  }
  self.attachments.push((name, data))
}

///|
/// Wire-provided filename โ†’ safe local basename. Empty/`.`/`..`/path
/// separators/control characters are rejected (empty result = caller
/// keeps its numbered fallback).
fn sanitize_filename(name : String) -> String {
  let mut last = -1
  for i, c in name {
    if c == '/' || c == '\\' {
      last = i
    }
  }
  let base = name[last + 1:name.length()].to_owned()
  guard base != "" && base != "." && base != ".." else { return "" }
  for c in base {
    let v = c.to_int()
    guard v >= 0x20 && v <= 0x7E else { return "" }
  }
  base
}

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

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

///|
struct Config {
  mut host : String
  mut port : Int32
  mut user : String?
  mut pass_env : String?
  mut folder : String
  mut last_n : Int32
  mut fetch_uid : Int32?
  mut out_dir : String
  keys : Array[String]
  certs : Array[String]
  mut allow_insecure : Bool
}

///|
fn Config::new() -> Config {
  {
    host: "127.0.0.1",
    port: 1143,
    user: None,
    pass_env: None,
    folder: "INBOX",
    last_n: 5,
    fetch_uid: None,
    out_dir: ".",
    keys: [],
    certs: [],
    allow_insecure: false,
  }
}

///|
fn usage() -> Unit {
  let lines = [
    "usage: moon run example/mailreader -- [flags]", "  --imap-host H          default 127.0.0.1 (put stunnel in front for IMAPS)",
    "  --imap-port N          default 1143", "  --user ADDRESS         mailbox login (required)",
    "  --pass-env NAME        env var holding the auth code (required)", "  --folder NAME          default INBOX (the v1 subset)",
    "  --last N               list the newest N messages (default 5)", "  --fetch UID            fetch one message by UID, then decrypt+verify",
    "  --keys PATH            armored private key, repeatable (for --fetch)", "  --certs PATH           armored public key, repeatable (verifiers)",
    "  --out DIR              where .eml and attachments are written", "  --allow-insecure       opt-in for plaintext to a non-local host",
  ]
  for l in lines {
    println(l)
  }
}

///|
fn parse_args(argv : Array[String]) -> Config raise {
  let cfg = Config::new()
  let mut i = 0
  while i < argv.length() {
    let flag = argv[i]
    let val = if flag == "--allow-insecure" {
      ""
    } else {
      guard i + 1 < argv.length() else {
        @error.fail("mailreader: \{flag} needs a value")
      }
      argv[i + 1]
    }
    match flag {
      "--imap-host" => cfg.host = val
      "--imap-port" => cfg.port = parse_port(val)
      "--user" => cfg.user = Some(val)
      "--pass-env" => cfg.pass_env = Some(val)
      "--folder" => cfg.folder = val
      "--last" => cfg.last_n = parse_count(val)
      "--fetch" => {
        // an unparseable --fetch must fail loudly, not silently
        // degrade to list mode (review finding m5)
        let v = match @netmail.parse_int(val) {
          Some(v) => v
          None =>
            @error.fail(
              "mailreader: --fetch expects a numeric UID, got '\{val}'",
            )
        }
        guard v > 0 else {
          @error.fail(
            "mailreader: --fetch expects a positive UID, got '\{val}'",
          )
        }
        cfg.fetch_uid = Some(v)
      }
      "--out" => cfg.out_dir = val
      "--keys" => cfg.keys.push(val)
      "--certs" => cfg.certs.push(val)
      "--allow-insecure" => cfg.allow_insecure = true
      _ => @error.fail("mailreader: unknown flag '\{flag}'")
    }
    i = i + (if flag == "--allow-insecure" { 1 } else { 2 })
  }
  cfg
}

///|
fn parse_port(s : String) -> Int32 raise {
  let v = match @netmail.parse_int(s) {
    Some(v) => v
    None => @error.fail("mailreader: bad number '\{s}'")
  }
  guard v > 0 && v <= 65535 else { @error.fail("mailreader: bad port '\{s}'") }
  v
}

///|
/// Row limit for --last (not a port: separate bound, separate message).
fn parse_count(s : String) -> Int32 raise {
  let v = match @netmail.parse_int(s) {
    Some(v) => v
    None => @error.fail("mailreader: bad number '\{s}'")
  }
  guard v > 0 && v <= 1_000_000 else {
    @error.fail("mailreader: --last out of range (1..1000000)")
  }
  v
}

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

///|
/// Extract a header value from the fetched header-block bytes, joining
/// folded continuation lines. Byte-faithful read, lossy text decode
/// (values may be RFC 2047 words; that decode comes next).
fn header_value(headers : Bytes, name : String) -> String? {
  let text = @netmail.bytes_lossy(headers)
  let mut current_name = ""
  let mut current_value = ""
  let mut found : String? = None
  for raw_line in text.split("\r\n") {
    let line = raw_line.to_owned()
    if line == "" {
      continue
    }
    if (line.has_prefix(" ") || line.has_prefix("\t")) && current_name != "" {
      // RFC 5322 unfolding: the CRLF is removed, the folding whitespace
      // is kept as a single space
      current_value = current_value + " " + line.trim().to_owned()
      continue
    }
    // flush the previous header before starting a new one
    if current_name != "" {
      if current_name == name {
        found = Some(current_value)
      }
      current_value = ""
    }
    let colon = line.split(":").collect()
    if colon.length() >= 2 {
      current_name = colon[0].to_lower().to_owned()
      current_value = line[colon[0].length() + 1:line.length()]
        .trim()
        .to_owned()
    }
  }
  if current_name != "" && current_name == name {
    found = Some(current_value)
  }
  found
}

///|
fn load_private_keys(paths : Array[String]) -> Array[@api.PrivateKey] raise {
  let out : Array[@api.PrivateKey] = []
  for p in paths {
    out.push(@api.PrivateKey::from_armored(@fs.read_file_to_string(p)))
  }
  out
}

///|
fn load_verifiers(paths : Array[String]) -> Array[@api.PublicKey] raise {
  let out : Array[@api.PublicKey] = []
  for p in paths {
    let text = @fs.read_file_to_string(p)
    out.push(
      @api.PublicKey::from_armored(text) catch {
        _ => @api.PrivateKey::from_armored(text).to_public()
      },
    )
  }
  out
}

///|
fn join_path(dir : String, name : String) -> String {
  if dir.has_suffix("/") {
    "\{dir}\{name}"
  } else {
    "\{dir}/\{name}"
  }
}

///|
fn list_mailbox(cfg : Config, user : String, password : String) -> Unit raise {
  @netmail.with_imap_session(
    cfg.host,
    cfg.port,
    cfg.allow_insecure,
    user,
    password,
    cfg.folder,
    fn(client) -> Unit raise {
      let summaries = client.imap_fetch_recent(cfg.last_n)
      println(
        "folder \{cfg.folder}: \{client.state.exists} message(s), uidvalidity \{client.state.uidvalidity}, showing newest \{summaries.length()}",
      )
      for s in summaries {
        let from = match header_value(s.headers, "from") {
          Some(v) => @mail.decode_rfc2047(v)
          None => "?"
        }
        let subject = match header_value(s.headers, "subject") {
          Some(v) => @mail.decode_rfc2047(v)
          None => "?"
        }
        let date = match header_value(s.headers, "date") {
          Some(v) => v
          None => s.internaldate
        }
        println(
          "seq=\{s.seq} uid=\{s.uid} size=\{s.size} \{s.flags} | \{date} | \{from} | \{subject}",
        )
      }
    },
  )
}

///|
fn fetch_and_decrypt(
  cfg : Config,
  user : String,
  password : String,
  uid : Int32,
) -> Unit raise {
  guard !cfg.keys.is_empty() else {
    @error.fail("mailreader: --fetch needs at least one --keys PATH")
  }
  let keys = load_private_keys(cfg.keys)
  let verifiers = load_verifiers(cfg.certs)
  @netmail.with_imap_session(
    cfg.host,
    cfg.port,
    cfg.allow_insecure,
    user,
    password,
    cfg.folder,
    fn(client) -> Unit raise {
      let raw = client.imap_fetch_message(uid)
      let eml_path = join_path(cfg.out_dir, "msg-\{uid}.eml")
      @fs.write_bytes_to_file(eml_path, raw)
      println("saved \{eml_path} (\{raw.length()} bytes)")
      // The library's decrypt pipeline is String-based; fetched PGP/MIME
      // is 7-bit (RFC 3156 ยง3), so the lossy decode is loss-free there.
      let cb = FetchRecorder::new()
      @mime.decrypt_mime(
        @netmail.bytes_lossy(raw),
        keys,
        verifiers~,
        callbacks=cb,
      )
      for e in cb.errors {
        eprintln("mime error: \{e}")
      }
      println("verify status: \{@netmail.status_names(cb.verified)}")
      for b in cb.bodies {
        println("--- body[\{b.1}] ---")
        println(b.0)
      }
      for a in cb.attachments {
        let path = join_path(cfg.out_dir, a.0)
        @fs.write_bytes_to_file(path, a.1)
        println("attachment: \{path} (\{a.1.length()} bytes)")
      }
      if cb.verified is [Ok] {
        println("decrypt+verify OK")
      } else if cb.errors.length() > 0 || cb.verified is [Failed] {
        abort("mailreader: decrypt/verify FAILED")
      }
    },
  )
}

///|
fn run(cfg : Config) -> Unit raise {
  let user = match cfg.user {
    Some(u) => u
    None => @error.fail("mailreader: --user is required")
  }
  let pass_env = match cfg.pass_env {
    Some(name) => name
    None => @error.fail("mailreader: --pass-env is required")
  }
  let password = require_env(pass_env)
  match cfg.fetch_uid {
    Some(uid) => fetch_and_decrypt(cfg, user, password, uid)
    None => list_mailbox(cfg, user, password)
  }
}

///|
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("mailreader: no arguments")
  }
  let cfg = parse_args(rest) catch {
    e => {
      eprintln("\{e}")
      usage()
      abort("mailreader: bad arguments")
    }
  }
  run(cfg) catch {
    e => {
      eprintln("\{e}")
      abort("mailreader: failed")
    }
  }
}