///|
/// What a profile-aware command selects: a node root and profile, or a versioned bundle.
priv enum CodecSelection {
  NodeSelection(@smithy.ShapeId, @codec_contract.ProfileId?)
  BundleSelection(@codec_contract.BundleId, @codec_contract.ProfileId?)
}

///|
priv struct CodecOptions {
  selection : CodecSelection
  /// Canonicalize instead of check.
  canonicalize : Bool
  /// The bundle profile that writes the output; default the reading profile.
  output_profile : @codec_contract.ProfileId?
  file : String
  output : String?
  diagnostics : ValueDiagnostics
}

///|
/// Parse the shared selection of `validate value` and `codec canonicalize`. The selection names declared
/// contracts only; it cannot change a profile's rules.
fn codec_selection(
  shape : String?,
  bundle : String?,
  profile : String?,
) -> CodecSelection raise CliError {
  let profile = match profile {
    None => None
    Some(text) =>
      Some(
        @codec_contract.ProfileId::parse(text) catch {
          _ =>
            raise CliError("expected a profile identifier after --profile", 2)
        },
      )
  }
  match (shape, bundle) {
    (Some(_), Some(_)) =>
      raise CliError("use either --shape or --bundle, not both", 2)
    (None, None) =>
      raise CliError(
        "select a root with --shape or a document bundle with --bundle", 2,
      )
    (Some(text), None) => {
      guard @smithy.ShapeId::parse(text) is Some(id) && id.member_name() is None else {
        raise CliError("expected an absolute Smithy shape ID after --shape", 2)
      }
      NodeSelection(id, profile)
    }
    (None, Some(text)) => {
      let id = @codec_contract.BundleId::parse(text) catch {
        _ => raise CliError("expected a bundle identifier after --bundle", 2)
      }
      BundleSelection(id, profile)
    }
  }
}

///|
/// The adapters that this host trusts. Model content names them; it cannot add one.
fn host_adapters() -> @codec.AdapterRegistry {
  @morphir.registry()
}

///|
fn codec_problem(
  opts : CodecOptions,
  code : String,
  message : String,
  status : Int32,
  source~ : String? = None,
) -> Int32 {
  let source = source.unwrap_or(opts.file)
  if opts.diagnostics == JsonFindings {
    println(
      Json::object({
        "valid": Json::boolean(false),
        "complete": Json::boolean(false),
        "findings": Json::array([
          Json::object({
            "code": Json::string(code),
            "severity": Json::string("error"),
            "message": Json::string(message),
            "source": Json::string(source),
            "path": Json::array([]),
            "pointer": Json::string(""),
          }),
        ]),
      }).stringify(),
    )
  } else {
    eprintln("\{source}: error[\{code}]: \{message}")
  }
  status
}

///|
fn report_codec_result(
  opts : CodecOptions,
  result : @codec.CodecResult,
) -> Int32 {
  if opts.diagnostics == JsonFindings {
    println(result.to_json().stringify())
  } else {
    for finding in result.findings {
      eprintln(
        "\{opts.file}\{finding.path.pointer()}: error[\{finding.code}] \{finding.shape}: \{finding.message}",
      )
    }
  }
  if result.is_valid() {
    0
  } else {
    1
  }
}

///|
/// A codec preparation or selection error is a configuration error: exit 2.
fn codec_preparation_problem(
  opts : CodecOptions,
  error : @codec_contract.ContractError,
) -> Int32 {
  codec_problem(
    opts,
    error.code(),
    error.message(),
    2,
    source=Some("--profile"),
  )
}

///|
priv enum PreparedSelection {
  PreparedNode(@codec.PreparedCodec)
  PreparedDocuments(@codec.PreparedBundle, @codec_contract.ProfileId?)
}

///|
fn run_codec(opts : CodecOptions, assembled : @smithy.LoadResult) -> Int32 {
  let model_opts : ValueOptions = {
    target: match opts.selection {
      NodeSelection(id, _) => id
      BundleSelection(_, _) =>
        @smithy.ShapeId::parse("smithy.api#Document").unwrap()
    },
    file: opts.file,
    diagnostics: opts.diagnostics,
    config: @validation.Config::new(),
    input_format: JsonInput,
    ion_index: None,
  }
  if !validation_model_ok(model_opts, assembled) {
    return 1
  }
  let model = assembled.model.unwrap()
  let limits = @codec.CodecLimits::default()
  let adapters = host_adapters()
  // Prepare before reading the value, so a configuration error leaves every input unread.
  let prepared = match opts.selection {
    NodeSelection(root, profile) => {
      if opts.output_profile is Some(_) {
        return codec_problem(
          opts,
          "InvalidSelection",
          "--output-profile needs --bundle; select the profile of a root with --profile.",
          2,
          source=Some("--output-profile"),
        )
      }
      PreparedNode(
        @codec.PreparedCodec::prepare(model, root, profile~, adapters~, limits~) catch {
          error => return codec_preparation_problem(opts, error)
        },
      )
    }
    BundleSelection(id, profile) =>
      PreparedDocuments(
        @codec.PreparedBundle::prepare(model, id, adapters~, limits~) catch {
          error => return codec_preparation_problem(opts, error)
        },
        profile,
      )
  }
  let text = try {
    if opts.file == "-" {
      read_stdin()
    } else {
      @fs.read_file_to_string(opts.file)
    }
  } catch {
    _ =>
      return codec_problem(
        opts, "InputRead", "I could not read the JSON value input.", 1,
      )
  }
  if !opts.canonicalize {
    let result = match prepared {
      PreparedNode(codec) => codec.check(text)
      PreparedDocuments(bundle, profile) => bundle.check(text, profile~)
    }
    return report_codec_result(opts, result)
  }
  let canonical = try {
    match prepared {
      PreparedNode(codec) => codec.canonicalize(text)
      PreparedDocuments(bundle, profile) =>
        bundle.canonicalize(text, profile~, output=opts.output_profile)
    }
  } catch {
    @codec.CodecFailure(result) => return report_codec_result(opts, result)
  }
  // The output exists only after the whole value is checked and written.
  match opts.output {
    Some(path) =>
      @fs.write_string_to_file(path, canonical + "\n") catch {
        _ =>
          return codec_problem(
            opts,
            "OutputWrite",
            "I could not write the canonical output.",
            1,
            source=Some(path),
          )
      }
    None => println(canonical)
  }
  0
}