// `moui verify` command — verify embedded-runtime app evidence.
// It generates the canonical `verify-manifest.json`.

///|
priv struct ParsedVerify {
  help : Bool
  json : Bool
  platform : String
  options : VerifyOptions?
}

///|
priv struct VerifyOptions {
  device : String?
  build_dir : String?
  output_path : String?
  require_passed : Bool
}

///|
/// Canonical producer of `verify-manifest.json`.
let verify_manifest_generated_by_canonical : String = "moui_cli"

///|
fn verify_usage() -> String {
  (
    #|Usage: moui verify   [options]
    #|
    #|Verify app runtime evidence for the embedded-runtime path.
    #|Build artifacts must exist under artifacts/// and the
    #|device must be connected. The command writes `verify-manifest.json`.
    #|
    #|Arguments:
    #|  platform     Target platform: android, ios, or harmonyos.
    #|  app          Application id (matches moui.mobile.json `id`).
    #|
    #|Options:
    #|  --device ID             Target device id. Defaults to the first device.
    #|  --build-dir PATH        Build artifact directory. Defaults to
    #|                          `artifacts/`.
    #|  --output PATH           Manifest output path. Defaults to
    #|                          `artifacts///verify-manifest.json`.
    #|  --require-passed        Exit non-zero when any observation fails.
    #|  --json                  Emit machine-readable output.
    #|  -h, --help              Show this help.
  )
}

///|
fn parse_verify_args(
  args : Array[String],
  cwd : String,
  host : HostPlatform,
) -> Result[ParsedVerify, String] {
  let _ = cwd
  let _ = host
  let help = Ref(false)
  let json = Ref(false)
  let positional : Array[String] = []
  let device : Ref[String?] = Ref(None)
  let build_dir : Ref[String?] = Ref(None)
  let output_path : Ref[String?] = Ref(None)
  let require_passed = Ref(false)
  let mut index = 0
  while index < args.length() {
    let argument = args[index]
    let (name, inline) = option_parts(argument)
    match name {
      "--json" =>
        match flag_without_value(inline, name) {
          Ok(_) => json.val = true
          Err(message) => return Err(message)
        }
      "-h" | "--help" =>
        match flag_without_value(inline, name) {
          Ok(_) => help.val = true
          Err(message) => return Err(message)
        }
      "--require-passed" =>
        match flag_without_value(inline, name) {
          Ok(_) => require_passed.val = true
          Err(message) => return Err(message)
        }
      "--device" | "--build-dir" | "--output" =>
        match option_value(args, index, inline, name) {
          Ok((value, next)) => {
            match name {
              "--device" => device.val = Some(value)
              "--build-dir" => build_dir.val = Some(value)
              "--output" => output_path.val = Some(value)
              _ => ()
            }
            index = next
            continue
          }
          Err(message) => return Err(message)
        }
      _ if argument.has_prefix("-") =>
        return Err("unknown option for moui verify: " + name)
      _ => positional.push(argument)
    }
    index += 1
  }
  if help.val {
    return Ok({ help: true, json: json.val, platform: "", options: None, })
  }
  if positional.length() != 2 {
    return Err("usage: moui verify   [options]")
  }
  let platform = positional[0]
  match platform {
    "android" | "ios" | "harmonyos" => ()
    _ => return Err("unsupported platform for moui verify: " + platform)
  }
  Ok({
    help: false,
    json: json.val,
    platform,
    options: Some({
      device: device.val,
      build_dir: build_dir.val,
      output_path: output_path.val,
      require_passed: require_passed.val,
    }),
  })
}

///|
/// Locate the build artifact for the given platform/app. Mirrors the
/// `find_build_artifact` layout used by `moui run`.
fn find_verify_artifact(
  platform : String,
  build_dir : String,
  app_id : String,
) -> String {
  match platform {
    "android" => join_path(build_dir, app_id + ".apk")
    "ios" => join_path(build_dir, app_id + ".app")
    "harmonyos" => join_path(build_dir, app_id + ".hap")
    _ => ""
  }
}

///|
/// Render the canonical window-hosted verify manifest as JSON.
fn verify_manifest_to_json(
  platform : String,
  app_id : String,
  artifact : String,
  status : String,
  probe : ProbeResult,
) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["schemaVersion"] = Json::number(1.0)
  fields["mode"] = Json::string("window-hosted-runtime-verify")
  fields["generatedBy"] = Json::string(verify_manifest_generated_by_canonical)
  fields["platform"] = Json::string(platform)
  fields["app"] = Json::string(app_id)
  fields["status"] = Json::string(status)
  let build_fields : Map[String, Json] = Map([])
  build_fields["fallbackSkia"] = Json::boolean(false)
  build_fields["artifact"] = Json::string(artifact)
  build_fields["command"] = Json::string(
    "moui_cli verify " + platform + " " + app_id,
  )
  fields["build"] = Json::object(build_fields)
  // Observations map directly from the probe result.
  let observations : Array[Json] = []
  for obs in probe.observations {
    let obs_fields : Map[String, Json] = Map([])
    obs_fields["kind"] = Json::string(obs.kind)
    obs_fields["name"] = Json::string(obs.name)
    obs_fields["value"] = Json::string(obs.value)
    obs_fields["passed"] = Json::boolean(obs.passed)
    observations.push(Json::object(obs_fields))
  }
  fields["observations"] = Json::array(observations)
  fields["evidenceBoundary"] = Json::string(
    "non-fallback matching-host smoke evidence; fallback builds are packaging only",
  )
  Json::object(fields)
}

///|
/// Check whether a `generatedBy` value is the canonical producer.
fn is_accepted_generated_by(value : String) -> Bool {
  value == verify_manifest_generated_by_canonical
}

///|
/// Compute the verify status from the probe result. `"passed"` when all
/// observations pass, `"failed"` otherwise.
fn verify_status_from_probe(probe : ProbeResult) -> String {
  if probe.ok {
    "passed"
  } else {
    "failed"
  }
}

///|
/// Write the verify manifest JSON to disk atomically.
fn write_verify_manifest(
  path : String,
  manifest : Json,
) -> Result[Unit, String] {
  let dir = parent_dir(path)
  if dir != "" && dir != "." {
    match mkdir_p(dir) {
      Ok(_) => ()
      Err(message) => return Err(message)
    }
  }
  let text = manifest.stringify(indent=2) + "\n"
  let bytes = @utf8.encode(text[:], bom=false)
  replace_file_atomic(path, bytes)
}

///|
/// Resolve the target device for verification. If `--device` was supplied,
/// use it; otherwise pick the first available device for the platform.
fn resolve_verify_device(
  options : VerifyOptions,
  platform : String,
  host : HostPlatform,
) -> Result[Device, String] {
  match options.device {
    Some(id) =>
      Ok({ platform, id, name: id, state: "selected", kind: "unknown", })
    None => {
      let devices = list_all_devices(host)
      let matching : Array[Device] = []
      for device in devices {
        if device.platform == platform {
          matching.push(device)
        }
      }
      if matching.is_empty() {
        return Err(
          "no " +
          platform +
          " device found; connect a device or start an emulator",
        )
      }
      Ok(matching[0])
    }
  }
}

///|
fn run_verify(parsed : ParsedVerify, host : HostPlatform) -> CliOutcome {
  if parsed.help {
    return stdout_outcome(verify_usage())
  }
  guard parsed.options is Some(options) else {
    return failed_outcome(
      "verify command options are unavailable",
      parsed.json,
      verify_usage(),
    )
  }
  // 1. Read mobile metadata for the app.
  let app = match read_mobile_app(".") {
    Ok(a) => a
    Err(message) =>
      return failed_outcome(
        "could not read moui.mobile.json: " + message,
        parsed.json,
        verify_usage(),
      )
  }
  // 2. Locate build artifacts.
  let build_dir = match options.build_dir {
    Some(d) => d
    None => join_path("artifacts", parsed.platform)
  }
  let artifact = find_verify_artifact(parsed.platform, build_dir, app.id)
  if !path_is_file(artifact) {
    return failed_outcome(
      "build artifact not found: " + artifact + "; run `moui run` first",
      parsed.json,
      verify_usage(),
    )
  }
  // 3. Resolve device.
  let device = match resolve_verify_device(options, parsed.platform, host) {
    Ok(d) => d
    Err(message) => return failed_outcome(message, parsed.json, verify_usage())
  }
  // 4. Run the probe.
  let probe = match parsed.platform {
    "android" => probe_android(device, app.id, app.id, host, 30)
    "ios" => probe_ios(device, app.id, host, 30)
    "harmonyos" => probe_harmonyos(device, app.id, app.id, host, 30)
    _ => { ok: false, observations: [], }
  }
  let status = verify_status_from_probe(probe)
  // 5. Write the manifest.
  let manifest = verify_manifest_to_json(
    parsed.platform,
    app.id,
    artifact,
    status,
    probe,
  )
  let manifest_path = match options.output_path {
    Some(p) => p
    None =>
      join_path(
        join_path(join_path("artifacts", parsed.platform), app.id),
        "verify-manifest.json",
      )
  }
  match write_verify_manifest(manifest_path, manifest) {
    Ok(_) => ()
    Err(message) => return failed_outcome(message, parsed.json, verify_usage())
  }
  // 6. Exit non-zero when --require-passed and probe failed.
  let exit_code = if options.require_passed && !probe.ok { 1 } else { 0 }
  // 7. Emit output.
  if parsed.json {
    let output : Json = {
      "schemaVersion": 1,
      "ok": probe.ok,
      "platform": Json::string(parsed.platform),
      "app": Json::string(app.id),
      "status": Json::string(status),
      "manifest": Json::string(manifest_path),
      "probe": probe_result_to_json(probe),
    }
    { exit_code, stdout: output.stringify(indent=2) + "\n", stderr: "", }
  } else {
    let mut text = "[verify] " + parsed.platform + " app=" + app.id + "\n"
    text += "  status: " + status + "\n"
    text += "  manifest: " + manifest_path + "\n"
    text += probe_result_to_text(probe)
    { exit_code, stdout: text, stderr: "", }
  }
}