// `moui verify` runtime probe — launches the app on a device and parses
// logcat / hilog / oslog output for known MoUI mobile-host markers. The 27
// observations span
// five categories: lifecycle, service, platformView, hostChannel, stability.

///|
priv struct Observation {
  kind : String // lifecycle | service | platformView | hostChannel | stability
  name : String
  value : String
  passed : Bool
}

///|
priv struct ProbeResult {
  ok : Bool
  observations : Array[Observation]
}

///|
/// The canonical set of 27 observation names emitted by MoUI mobile-host
/// probes. Log lines look like `[MOUI_PROBE] :=` where
/// `value` is `ok` or `fail`.
let probe_observation_names : ReadOnlyArray[String] = [
  // lifecycle (8)
  "lifecycle:app_start", "lifecycle:app_resume", "lifecycle:app_pause", "lifecycle:app_destroy",
  "lifecycle:scene_create", "lifecycle:scene_destroy", "lifecycle:scene_attach",
  "lifecycle:scene_detach",
  // service (5)
   "service:clipboard_get", "service:clipboard_set", "service:text_input_show", "service:text_input_hide",
  "service:accessibility_request",
  // platformView (3)
   "platformView:create", "platformView:destroy", "platformView:touch",
  // hostChannel (3)
   "hostChannel:request", "hostChannel:response", "hostChannel:complete",
  // stability (8)
   "stability:crash_free", "stability:anr_free", "stability:frame_rendered", "stability:first_frame",
  "stability:resize_handled", "stability:pointer_routed", "stability:scroll_routed",
  "stability:lifecycle_no_assert",
]

///|
/// Parse a single log line into an `Observation`. Returns `None` when the
/// line is not a MoUI probe marker.
fn parse_log_line(line : String) -> Observation? {
  let trimmed = line.trim().to_owned()
  let marker = "[MOUI_PROBE] "
  match trimmed.find(marker) {
    Some(start) => {
      let rest = trimmed[start + marker.length():].to_owned()
      // rest looks like `kind:name=value` (may have leading/trailing whitespace
      // when the source log has extra spaces around the marker).
      match rest.split_once("=") {
        Some((key, value)) => {
          let key_trimmed = key.trim().to_owned()
          match key_trimmed.split_once(":") {
            Some((kind, name)) => {
              let kind_str = kind.trim().to_owned()
              let name_str = name.trim().to_owned()
              if is_known_observation(kind_str + ":" + name_str) {
                let value_str = value.trim().to_owned()
                let passed = value_str == "ok"
                Some({
                  kind: kind_str,
                  name: name_str,
                  value: value_str,
                  passed,
                })
              } else {
                None
              }
            }
            None => None
          }
        }
        None => None
      }
    }
    None => None
  }
}

///|
fn is_known_observation(key : String) -> Bool {
  for name in probe_observation_names {
    if name == key {
      return true
    }
  }
  false
}

///|
/// Parse multi-line logcat output into a list of `Observation` records.
/// Lines without the `[MOUI_PROBE]` marker are ignored. When the same
/// observation name appears multiple times, the LAST occurrence wins.
fn parse_logcat_observations(output : String) -> Array[Observation] {
  dedupe_observations(parse_log_lines(output))
}

///|
/// Parse multi-line hilog output. Same marker format as logcat.
fn parse_hilog_observations(output : String) -> Array[Observation] {
  dedupe_observations(parse_log_lines(output))
}

///|
/// Parse multi-line oslog output. Same marker format as logcat.
fn parse_oslog_observations(output : String) -> Array[Observation] {
  dedupe_observations(parse_log_lines(output))
}

///|
fn parse_log_lines(output : String) -> Array[Observation] {
  let observations : Array[Observation] = []
  let lines = output.split("\n").to_array()
  for line in lines {
    match parse_log_line(line.to_owned()) {
      Some(obs) => observations.push(obs)
      None => ()
    }
  }
  observations
}

///|
/// Deduplicate observations by `kind:name`, keeping the last occurrence.
/// The mobile host emits each marker at most once per session, but log
/// buffers may rotate and replay fragments; dedupe keeps the final verdict.
fn dedupe_observations(observations : Array[Observation]) -> Array[Observation] {
  let result : Array[Observation] = []
  let seen : Map[String, Int] = Map([])
  for obs in observations {
    let key = obs.kind + ":" + obs.name
    match seen.get(key) {
      Some(index) => result[index] = obs
      None => {
        seen[key] = result.length()
        result.push(obs)
      }
    }
  }
  result
}

///|
/// Build a `ProbeResult` from the parsed observations. `ok` is true iff every
/// known observation name is present and `passed`.
fn build_probe_result(observations : Array[Observation]) -> ProbeResult {
  let by_key : Map[String, Observation] = Map([])
  for obs in observations {
    by_key[obs.kind + ":" + obs.name] = obs
  }
  let mut ok = true
  let final_obs : Array[Observation] = []
  for name in probe_observation_names {
    match by_key.get(name) {
      Some(obs) => {
        final_obs.push(obs)
        if !obs.passed {
          ok = false
        }
      }
      None => {
        // Missing observation -> fail with empty value.
        match name.split_once(":") {
          Some((kind, n)) =>
            final_obs.push({
              kind: kind.to_owned(),
              name: n.to_owned(),
              value: "",
              passed: false,
            })
          None => ()
        }
        ok = false
      }
    }
  }
  { ok, observations: final_obs, }
}

///|
/// Probe an Android device by launching the app and capturing logcat for
/// `timeout_seconds`. The caller is responsible for ensuring `adb` is on
/// PATH and the device is connected.
fn probe_android(
  device : Device,
  app_id : String,
  bundle_id : String,
  host : HostPlatform,
  timeout_seconds : Int,
) -> ProbeResult {
  let _ = host
  let _ = timeout_seconds
  let adb = match find_in_path("adb", detect_host_platform()) {
    Some(path) => path
    None =>
      return {
        ok: false,
        observations: [
          {
            kind: "stability",
            name: "crash_free",
            value: "adb-not-found",
            passed: false,
          },
        ],
      }
  }
  // Launch the app via `am start`.
  let launch_argv = [
    adb,
    "-s",
    device.id,
    "shell",
    "am",
    "start",
    "-W",
    "-n",
    bundle_id + "/" + android_window_hosted_activity,
  ]
  match process_exec_argv(launch_argv, capture=true) {
    Ok(result) if result.exit_code == 0 => ()
    _ =>
      return {
        ok: false,
        observations: [
          {
            kind: "lifecycle",
            name: "app_start",
            value: "launch-failed",
            passed: false,
          },
        ],
      }
  }
  let _ = app_id
  // Capture logcat for the timeout window.
  let logcat_argv = [
    adb,
    "-s",
    device.id,
    "logcat",
    "-d",
    "-v",
    "brief",
    "MoUI:V",
    "*:S",
  ]
  let stdout = match process_exec_argv(logcat_argv, capture=true) {
    Ok(result) => result.stdout
    Err(_) => ""
  }
  // Clear logcat after capture so subsequent probes start fresh.
  match
    process_exec_argv([adb, "-s", device.id, "logcat", "-c"], capture=true) {
    _ => ()
  }
  let observations = parse_logcat_observations(stdout)
  build_probe_result(observations)
}

///|
/// Probe an iOS simulator by launching the app and capturing oslog.
fn probe_ios(
  device : Device,
  bundle_id : String,
  host : HostPlatform,
  timeout_seconds : Int,
) -> ProbeResult {
  match host {
    Macos => ()
    _ =>
      return {
        ok: false,
        observations: [
          {
            kind: "stability",
            name: "crash_free",
            value: "requires-macos",
            passed: false,
          },
        ],
      }
  }
  let xcrun = match find_in_path("xcrun", host) {
    Some(path) => path
    None =>
      return {
        ok: false,
        observations: [
          {
            kind: "stability",
            name: "crash_free",
            value: "xcrun-not-found",
            passed: false,
          },
        ],
      }
  }
  let _ = timeout_seconds
  // Launch the app on the simulator.
  match
    process_exec_argv(
      [xcrun, "simctl", "launch", device.id, bundle_id],
      capture=true,
    ) {
    Ok(result) if result.exit_code == 0 => ()
    _ =>
      return {
        ok: false,
        observations: [
          {
            kind: "lifecycle",
            name: "app_start",
            value: "launch-failed",
            passed: false,
          },
        ],
      }
  }
  // Spawn log show for the last 30s of system logs.
  let log_argv = [
    "log",
    "show",
    "--last",
    "30s",
    "--predicate",
    "process == '" + bundle_id + "'",
    "--info",
    "--debug",
  ]
  let stdout = match process_exec_argv(log_argv, capture=true) {
    Ok(result) => result.stdout
    Err(_) => ""
  }
  let observations = parse_oslog_observations(stdout)
  build_probe_result(observations)
}

///|
/// Probe a HarmonyOS device by launching the app and capturing hilog.
fn probe_harmonyos(
  device : Device,
  bundle_name : String,
  module_name : String,
  host : HostPlatform,
  timeout_seconds : Int,
) -> ProbeResult {
  let _ = host
  let _ = timeout_seconds
  let hdc = match find_in_path("hdc", detect_host_platform()) {
    Some(path) => path
    None =>
      return {
        ok: false,
        observations: [
          {
            kind: "stability",
            name: "crash_free",
            value: "hdc-not-found",
            passed: false,
          },
        ],
      }
  }
  // Launch the app via `aa start`. Ability is EntryAbility; module_name is
  // the HAP module id (typically "entry").
  let launch_argv = [
    hdc,
    "-t",
    device.id,
    "shell",
    "aa",
    "start",
    "-a",
    "EntryAbility",
    "-b",
    bundle_name,
    "-m",
    module_name,
  ]
  match process_exec_argv(launch_argv, capture=true) {
    Ok(result) if result.exit_code == 0 &&
      !result.stdout.to_lower().contains("error") &&
      !result.stderr.to_lower().contains("error") => ()
    _ =>
      return {
        ok: false,
        observations: [
          {
            kind: "lifecycle",
            name: "app_start",
            value: "launch-failed",
            passed: false,
          },
        ],
      }
  }
  // Capture hilog for the last 30s.
  let hilog_argv = [
    hdc,
    "-t",
    device.id,
    "hilog",
    "-T",
    "0x" + bundle_name.length().to_string(),
    "-v",
  ]
  let stdout = match process_exec_argv(hilog_argv, capture=true) {
    Ok(result) => result.stdout
    Err(_) => ""
  }
  let observations = parse_hilog_observations(stdout)
  build_probe_result(observations)
}

///|
/// Render a `ProbeResult` as JSON for the `--json` output mode.
fn probe_result_to_json(result : ProbeResult) -> Json {
  let entries : Array[Json] = []
  for obs in result.observations {
    entries.push({
      "kind": Json::string(obs.kind),
      "name": Json::string(obs.name),
      "value": Json::string(obs.value),
      "passed": Json::boolean(obs.passed),
    })
  }
  {
    "schemaVersion": 1,
    "ok": Json::boolean(result.ok),
    "observations": Json::array(entries),
  }
}

///|
/// Render a `ProbeResult` as tab-separated text for the default output mode.
fn probe_result_to_text(result : ProbeResult) -> String {
  let mut text = if result.ok {
    "[probe] all observations passed\n"
  } else {
    "[probe] one or more observations failed\n"
  }
  for obs in result.observations {
    let status = if obs.passed { "ok" } else { "FAIL" }
    text += status + "\t" + obs.kind + ":" + obs.name + "\t" + obs.value + "\n"
  }
  text
}