///|
struct CliResult {
  exit_code : Int
  output : String
}

///|
fn usage() -> String {
  "moon-avro - schema, Avro binary and OCF inspector\n\nUsage:\n  moon run cmd/moon_avro -- help\n  moon run cmd/moon_avro -- schema inspect ''\n  moon run cmd/moon_avro -- encode '' ''\n  moon run cmd/moon_avro -- decode '' ''\n  moon run cmd/moon_avro -- ocf-info ''\n  moon run cmd/moon_avro -- roundtrip\n\nCommands:\n  schema inspect  Parse an inline Avro schema and print canonical form.\n  encode          Convert schema-defined JSON datum to Avro binary hex.\n  decode          Convert Avro binary hex to schema-defined JSON datum.\n  ocf-info        Inspect a complete OCF byte sequence supplied as hex.\n  roundtrip       Encode then decode a built-in record datum.\n\nThe CLI intentionally accepts inline data only; it never reads or writes user files."
}

///|
fn command_index(args : Array[String]) -> Int? {
  for index in 0.. return Some(index)
      _ => ()
    }
  }
  None
}

///|
fn hex(bytes : Bytes) -> String {
  let output = StringBuilder()
  for byte in bytes {
    output.write_string(byte.to_hex())
  }
  output.to_string().to_lower()
}

///|
fn bytes_from_hex(input : String) -> Bytes? {
  let source = if input.has_prefix("0x") || input.has_prefix("0X") {
    input[2:]
  } else {
    input
  }
  if source.length() % 2 != 0 {
    return None
  }
  let result : Array[Byte] = []
  for index = 0; index < source.length(); index = index + 2 {
    let value = @string.parse_int(source[index:index + 2], base=16) catch {
      _ => return None
    }
    if value < 0 || value > 255 {
      return None
    }
    result.push(value.to_byte())
  }
  Some(Bytes::from_array(result))
}

///|
fn schema_error_message(error : @schema.SchemaError) -> String {
  match error {
    DuplicateName(name) => "duplicate name: \{name}"
    EmptySchema => "empty schema"
    InvalidDefault(field~, reason~) => "invalid default for \{field}: \{reason}"
    InvalidJson(message) => "invalid JSON: \{message}"
    InvalidName(name) => "invalid name: \{name}"
    InvalidSchema(message) => "invalid schema: \{message}"
    UnknownName(name) => "unknown name: \{name}"
    UnsupportedSchema(message) => "unsupported schema: \{message}"
  }
}

///|
fn codec_error_message(error : @codec.CodecError) -> String {
  match error {
    TypeMismatch(expected~, actual~) =>
      "type mismatch: expected \{expected}, got \{actual}"
    UnexpectedEof(offset~) => "unexpected end of input at byte \{offset}"
    InvalidEncoding(offset~, message~) =>
      "invalid encoding at byte \{offset}: \{message}"
    InvalidLength(offset~, length~) =>
      "invalid length \{length} at byte \{offset}"
    LimitExceeded(offset~, limit~) =>
      "limit exceeded at byte \{offset}: \{limit}"
    MissingField(field~) => "missing record field: \{field}"
    TrailingData(offset~) => "trailing data at byte \{offset}"
  }
}

///|
fn encode_json(schema_source : String, datum_source : String) -> CliResult {
  let schema = @schema.parse(schema_source) catch {
    error =>
      return {
        exit_code: 2,
        output: "encode schema failed: \{schema_error_message(error)}\n",
      }
  }
  let datum = @avro_json.parse(schema, datum_source) catch {
    _ =>
      return {
        exit_code: 2,
        output: "encode datum failed: invalid schema-defined JSON\n",
      }
  }
  let encoded = @codec.encode(schema, datum) catch {
    error =>
      return {
        exit_code: 1,
        output: "binary encode failed: \{codec_error_message(error)}\n",
      }
  }
  { exit_code: 0, output: hex(encoded) }
}

///|
fn decode_json(schema_source : String, encoded_hex : String) -> CliResult {
  let schema = @schema.parse(schema_source) catch {
    error =>
      return {
        exit_code: 2,
        output: "decode schema failed: \{schema_error_message(error)}\n",
      }
  }
  let encoded = match bytes_from_hex(encoded_hex) {
    Some(bytes) => bytes
    None =>
      return {
        exit_code: 2,
        output: "decode input must be even-length hexadecimal\n",
      }
  }
  let datum = @codec.decode(schema, encoded) catch {
    error =>
      return {
        exit_code: 2,
        output: "binary decode failed: \{codec_error_message(error)}\n",
      }
  }
  let rendered = @avro_json.stringify(schema, datum) catch {
    _ => return { exit_code: 1, output: "JSON rendering failed\n" }
  }
  { exit_code: 0, output: rendered }
}

///|
fn inspect_ocf(encoded_hex : String) -> CliResult {
  let input = match bytes_from_hex(encoded_hex) {
    Some(bytes) => bytes
    None =>
      return {
        exit_code: 2,
        output: "ocf-info input must be even-length hexadecimal\n",
      }
  }
  let (header, records) = @ocf.decode_container(input) catch {
    _ =>
      return {
        exit_code: 2,
        output: "ocf-info failed: invalid or unsupported OCF data\n",
      }
  }
  {
    exit_code: 0,
    output: "schema_kind: \{header.schema().kind_name()}\ncodec: \{header.codec().name()}\nrecords: \{records.length()}",
  }
}

///|
fn inspect_schema(source : String) -> CliResult {
  let parsed = @schema.parse(source) catch {
    error =>
      return {
        exit_code: 2,
        output: "schema inspect failed: \{schema_error_message(error)}\n",
      }
  }
  let names = parsed.definition_names()
  let definitions = if names.is_empty() { "(none)" } else { names.join(", ") }
  {
    exit_code: 0,
    output: "kind: \{parsed.kind_name()}\ncanonical: \{parsed.canonical_form()}\nfingerprint64: 0x\{parsed.fingerprint64().to_string(radix=16)}\ndefinitions: \{definitions}",
  }
}

///|
fn roundtrip_demo() -> CliResult {
  let source = "{\"type\":\"record\",\"name\":\"Greeting\",\"fields\":[{\"name\":\"message\",\"type\":\"string\"},{\"name\":\"count\",\"type\":\"long\"}]}"
  let parsed = @schema.parse(source) catch {
    error =>
      return {
        exit_code: 1,
        output: "built-in demo schema is invalid: \{schema_error_message(error)}\n",
      }
  }
  let datum = @codec.Record([("message", String("hello")), ("count", Long(42L))])
  let encoded = @codec.encode(parsed, datum) catch {
    error =>
      return {
        exit_code: 1,
        output: "binary encode failed: \{codec_error_message(error)}\n",
      }
  }
  let decoded = @codec.decode(parsed, encoded) catch {
    error =>
      return {
        exit_code: 1,
        output: "binary decode failed: \{codec_error_message(error)}\n",
      }
  }
  if decoded != datum {
    return { exit_code: 1, output: "binary roundtrip changed the datum\n" }
  }
  {
    exit_code: 0,
    output: "schema: \{parsed.kind_name()}\nencoded_hex: \{hex(encoded)}\ndecoded: \{repr(decoded)}\nroundtrip: ok",
  }
}

///|
fn run_cli(args : Array[String]) -> CliResult {
  match command_index(args) {
    Some(index) =>
      match args[index] {
        "help" | "--help" | "-h" => { exit_code: 0, output: usage() }
        "inspect" =>
          match args.get(index + 1) {
            Some(source) => inspect_schema(source)
            None =>
              {
                exit_code: 2,
                output: "inspect requires an inline schema JSON\n\n\{usage()}",
              }
          }
        "schema" =>
          match (args.get(index + 1), args.get(index + 2)) {
            (Some("inspect"), Some(source)) => inspect_schema(source)
            _ =>
              {
                exit_code: 2,
                output: "schema supports only the inspect subcommand\n\n\{usage()}",
              }
          }
        "encode" =>
          match (args.get(index + 1), args.get(index + 2)) {
            (Some(schema), Some(datum)) => encode_json(schema, datum)
            _ =>
              {
                exit_code: 2,
                output: "encode requires schema JSON and datum JSON\n\n\{usage()}",
              }
          }
        "decode" =>
          match (args.get(index + 1), args.get(index + 2)) {
            (Some(schema), Some(encoded)) => decode_json(schema, encoded)
            _ =>
              {
                exit_code: 2,
                output: "decode requires schema JSON and binary hex\n\n\{usage()}",
              }
          }
        "ocf-info" =>
          match args.get(index + 1) {
            Some(encoded) => inspect_ocf(encoded)
            None =>
              {
                exit_code: 2,
                output: "ocf-info requires OCF hex\n\n\{usage()}",
              }
          }
        "roundtrip" => roundtrip_demo()
        _ => { exit_code: 2, output: usage() }
      }
    None => { exit_code: 2, output: usage() }
  }
}

///|
fn main {
  let result = run_cli(@env.args())
  println(result.output)
  cli_process_exit(result.exit_code)
}