///|
/// Errors reported by the command-line layer.
pub(all) suberror CliError {
  Failure(String)
}

///|
fn message_of(error : CliError) -> String {
  match error {
    Failure(value) => value
  }
}

///|
fn usage() -> Unit {
  println("moonsign - PDF digital signatures (PAdES B-B)")
  println("")
  println("Usage:")
  println(
    "  moonsign sign  --key  --cert  [options]",
  )
  println("  moonsign verify  --cert  [--json]")
  println("  moonsign info  [--json]")
  println("")
  println("Sign options:")
  println("  --out       output PDF (default: .signed.pdf)")
  println("  --chain     extra certificates to embed")
  println("  --field     signature field name (default: Signature1)")
  println("  --json            machine-readable summary")
  println("")
  println("Exit codes: 0 ok/valid, 2 invalid signature, 1 usage or I/O error")
}

///|
fn is_command(value : String) -> Bool {
  value == "sign" ||
  value == "verify" ||
  value == "info" ||
  value == "help" ||
  value == "--help" ||
  value == "-h"
}

///|
pub(all) struct CliOptions {
  mut input : String?
  mut out : String?
  mut key : String?
  mut cert : String?
  mut chain : String?
  mut field : String
  mut json : Bool
}

///|
fn is_flag(value : String) -> Bool {
  match value.get_char(0) {
    Some(c) => c == '-'
    None => false
  }
}

///|
fn parse_options(args : Array[String]) -> CliOptions raise CliError {
  let options : CliOptions = {
    input: None,
    out: None,
    key: None,
    cert: None,
    chain: None,
    field: "Signature1",
    json: false,
  }
  let mut index = 0
  while index < args.length() {
    let arg = args[index]
    if arg == "--json" {
      options.json = true
      index = index + 1
    } else if arg == "--out" ||
      arg == "--key" ||
      arg == "--cert" ||
      arg == "--chain" ||
      arg == "--field" {
      if index + 1 >= args.length() {
        raise CliError::Failure("missing value for " + arg)
      }
      let value = args[index + 1]
      if arg == "--out" {
        options.out = Some(value)
      } else if arg == "--key" {
        options.key = Some(value)
      } else if arg == "--cert" {
        options.cert = Some(value)
      } else if arg == "--chain" {
        options.chain = Some(value)
      } else {
        options.field = value
      }
      index = index + 2
    } else if is_flag(arg) {
      raise CliError::Failure("unknown option: " + arg)
    } else {
      let has_input = match options.input {
        Some(_) => true
        None => false
      }
      if has_input {
        raise CliError::Failure("unexpected extra argument: " + arg)
      }
      options.input = Some(arg)
      index = index + 1
    }
  }
  options
}

///|
fn require(value : String?, name : String) -> String raise CliError {
  match value {
    Some(path) => path
    None => raise CliError::Failure("missing required " + name)
  }
}

///|
fn read_bytes(path : String) -> Bytes raise CliError {
  @fs.read_file_to_bytes(path) catch {
    _ => raise CliError::Failure("cannot read file: " + path)
  }
}

///|
fn read_text(path : String) -> String raise CliError {
  @fs.read_file_to_string(path) catch {
    _ => raise CliError::Failure("cannot read file: " + path)
  }
}

///|
fn write_bytes(path : String, bytes : Bytes) -> Unit raise CliError {
  @fs.write_bytes_to_file(path, bytes) catch {
    _ => raise CliError::Failure("cannot write file: " + path)
  }
}

///|
fn json_escape(value : String) -> String {
  let buf = StringBuilder()
  for c in value.iter() {
    if c == '"' {
      buf.write_string("\\\"")
    } else if c == '\\' {
      buf.write_string("\\\\")
    } else {
      buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
fn default_output(input : String) -> String {
  input + ".signed.pdf"
}

///|
fn sign_impl(args : Array[String]) -> Unit raise CliError {
  let options = parse_options(args)
  let input = require(options.input, "input PDF")
  let key_path = require(options.key, "private key (--key)")
  let cert_path = require(options.cert, "certificate (--cert)")
  let out_path = match options.out {
    Some(path) => path
    None => default_output(input)
  }
  let pdf_bytes = read_bytes(input)
  let key_text = read_text(key_path)
  let cert_text = read_text(cert_path)
  let key_blocks = @pem.parse_pem(key_text) catch {
    e => raise CliError::Failure("cannot parse key PEM: " + e.message())
  }
  let mut private_key : @pem.PrivateKey? = None
  for block in key_blocks {
    if block.label.contains("PRIVATE KEY") {
      let parsed = @pem.parse_private_key(block.der) catch {
        e => raise CliError::Failure("cannot parse private key: " + e.message())
      }
      private_key = Some(parsed)
      break
    }
  }
  let key = match private_key {
    Some(value) => value
    None => raise CliError::Failure("no private key block found in " + key_path)
  }
  let certs = @pem.parse_certificates(cert_text) catch {
    e => raise CliError::Failure("cannot parse certificate: " + e.message())
  }
  if certs.length() == 0 {
    raise CliError::Failure("no CERTIFICATE block found in " + cert_path)
  }
  let leaf = certs[0]
  let extra : Array[Bytes] = []
  for i in 1.. {
      let chain_text = read_text(path)
      let chain_certs = @pem.parse_certificates(chain_text) catch {
        e => raise CliError::Failure("cannot parse chain: " + e.message())
      }
      for cert in chain_certs {
        extra.push(cert.der)
      }
    }
    None => ()
  }
  let signing_key = match key {
    @pem.PrivateKey::Rsa(modulus, private_exponent) =>
      @pades.SigningKey::Rsa(modulus, private_exponent)
    @pem.PrivateKey::EcdsaP256(scalar) => @pades.SigningKey::EcdsaP256(scalar)
  }
  let sign_options : @pades.SignOptions = {
    field_name: options.field,
    contents_chars: 16384,
    signing_time: None,
    hash: @crypto.Hash::Sha256,
  }
  let signed = @pades.sign_pdf(
    pdf_bytes, signing_key, leaf, extra, sign_options,
  ) catch {
    e => raise CliError::Failure(e.message())
  }
  write_bytes(out_path, signed)
  if options.json {
    println(
      "{\"command\":\"sign\",\"ok\":true,\"out\":\"" +
      json_escape(out_path) +
      "\",\"field\":\"" +
      json_escape(options.field) +
      "\"}",
    )
  } else {
    println("Signed: " + out_path)
    println("Field: " + options.field)
    println("Digest: SHA-256")
  }
}

///|
fn verify_impl(args : Array[String]) -> Unit raise CliError {
  let options = parse_options(args)
  let input = require(options.input, "input PDF")
  let cert_path = require(options.cert, "certificate (--cert)")
  let pdf_bytes = read_bytes(input)
  let cert_text = read_text(cert_path)
  let certs = @pem.parse_certificates(cert_text) catch {
    e => raise CliError::Failure("cannot parse certificate: " + e.message())
  }
  if certs.length() == 0 {
    raise CliError::Failure("no CERTIFICATE block found in " + cert_path)
  }
  let signatures = @pdf.find_signatures(pdf_bytes)
  if signatures.length() == 0 {
    raise CliError::Failure("no signature found in " + input)
  }
  let valid = @pades.verify_pdf(pdf_bytes, certs[0]) catch {
    e => raise CliError::Failure(e.message())
  }
  if options.json {
    println(
      "{\"command\":\"verify\",\"valid\":" +
      (if valid { "true" } else { "false" }) +
      ",\"signatures\":" +
      signatures.length().to_string() +
      ",\"field\":\"" +
      json_escape(signatures[0].field_name) +
      "\"}",
    )
  } else {
    println("Signature 1: " + signatures[0].field_name)
    println("  subfilter: " + signatures[0].subfilter)
    if valid {
      println("Result: VALID")
      println("  The document has not changed since signing.")
      println(
        "  (Trust in the certificate is your decision: pass the right one.)",
      )
    } else {
      println("Result: INVALID")
      println("  The document changed, or the certificate does not match.")
    }
  }
  if !valid {
    @env.exit(2)
  }
}

///|
fn info_impl(args : Array[String]) -> Unit raise CliError {
  let options = parse_options(args)
  let input = require(options.input, "input PDF")
  let pdf_bytes = read_bytes(input)
  let signatures = @pdf.find_signatures(pdf_bytes)
  if options.json {
    let buf = StringBuilder()
    buf.write_string("{\"command\":\"info\",\"signatures\":[")
    for i in 0.. 0 {
        buf.write_string(",")
      }
      buf.write_string("{\"field\":\"")
      buf.write_string(json_escape(signatures[i].field_name))
      buf.write_string("\",\"subfilter\":\"")
      buf.write_string(json_escape(signatures[i].subfilter))
      buf.write_string("\",\"contents_bytes\":")
      buf.write_string(signatures[i].contents.length().to_string())
      buf.write_string("}")
    }
    buf.write_string("]}")
    println(buf.to_string())
  } else if signatures.length() == 0 {
    println("No signature found.")
  } else {
    println("Signatures: " + signatures.length().to_string())
    for i in 0.. Unit {
  let result : Result[Unit, String] = try {
    if command == "sign" {
      sign_impl(args)
    } else if command == "verify" {
      verify_impl(args)
    } else {
      info_impl(args)
    }
    Ok(())
  } catch {
    e => Err(message_of(e))
  }
  match result {
    Ok(_) => ()
    Err(message) => {
      println("moonsign: " + message)
      @env.exit(1)
    }
  }
}

///|
fn main {
  let all = @env.args()
  let mut index = 0
  while index < all.length() && !is_command(all[index]) {
    index = index + 1
  }
  if index >= all.length() {
    usage()
    return
  }
  let command = all[index]
  if command == "help" || command == "--help" || command == "-h" {
    usage()
    return
  }
  let rest : Array[String] = []
  for i in (index + 1)..