// Copyright 2026 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include <inttypes.h>
#include <stdio.h>
#include <string.h>

#include "iree/async/frontier_tracker.h"
#include "iree/async/util/proactor_pool.h"
#include "iree/base/api.h"
#include "iree/base/tooling/flags.h"
#include "iree/hal/replay/execute.h"
#include "iree/io/file_contents.h"
#include "iree/tooling/device_util.h"

IREE_FLAG_LIST(
    string, replay_file_remap,
    "Remaps captured external file path prefixes before replay opens them. "
    "Repeat as --replay_file_remap=captured_prefix=replay_prefix.");
IREE_FLAG_LIST(
    string, replay_executable_substitution,
    "Substitutes a captured executable payload. Repeat as "
    "--replay_executable_substitution=EXECUTABLE_ID=PATH to infer the format, "
    "or --replay_executable_substitution=EXECUTABLE_ID@FORMAT=PATH when the "
    "format must be explicit. Use all=PATH or all@FORMAT=PATH to apply one "
    "replacement to every captured executable.");
IREE_FLAG(bool, agents_md, false,
          "Prints AGENTS.md guidance for HAL replay workflows and exits.");

static const char kIreeRunReplayUsage[] =
    "Executes an IREE HAL replay file against one or more HAL devices.\n"
    "\n"
    "The replay executor reconstructs the recorded HAL resource graph and\n"
    "reissues supported HAL operations against a freshly-created device "
    "group.\n"
    "Execution follows the recorded host-call sequence deterministically, "
    "while\n"
    "device work is ordered by the captured semaphore, event, command-buffer,\n"
    "and barrier dependencies. Select the target topology with --device= just\n"
    "like iree-run-module.\n"
    "\n"
    "Usage:\n"
    "  iree-run-replay [options] <capture.ireereplay>\n"
    "\n"
    "Important flags:\n"
    "  --device=URI\n"
    "      Target HAL device URI.\n"
    "  --replay_file_remap=captured_prefix=replay_prefix\n"
    "      Remaps referenced external file paths before replay opens them. "
    "The\n"
    "      replay still validates the captured file identity after remapping.\n"
    "      Repeat the flag for multiple roots.\n"
    "  --replay_executable_substitution=EXECUTABLE_ID=PATH\n"
    "  --replay_executable_substitution=EXECUTABLE_ID@FORMAT=PATH\n"
    "  --replay_executable_substitution=all=PATH\n"
    "  --replay_executable_substitution=all@FORMAT=PATH\n"
    "      Replaces captured executable payloads. Use iree-dump-replay to "
    "find\n"
    "      executable object ids. The all selector applies one replacement to\n"
    "      every captured executable. Replay validates export counts and "
    "reflected\n"
    "      ABI shape before using replacements.\n"
    "  --device_profiling_mode=...\n"
    "  --device_profiling_output=path.ireeprof\n"
    "      Captures HAL-native profiling for the replay execution.\n"
    "  --agents_md\n"
    "      Prints a Markdown playbook suitable for AGENTS.md.\n"
    "\n"
    "Examples:\n"
    "  iree-run-replay --device=local-sync /tmp/model.ireereplay\n"
    "  iree-run-replay --device=local-sync \\\n"
    "      --replay_file_remap=/mnt/train=/mnt/replay \\\n"
    "      /tmp/model.ireereplay\n";

static void iree_run_replay_print_agent_markdown(FILE* file) {
  fputs(
      "# IREE HAL Replay\n"
      "\n"
      "Use HAL replay when you need a reproducible HAL-level workload from an\n"
      "IREE application. A `.ireereplay` capture records the HAL resource "
      "graph,\n"
      "host-visible byte payloads, executable payloads, external file "
      "references,\n"
      "command buffers, queue operations, and synchronization edges.\n"
      "\n"
      "Replay is separate from profiling. `.ireereplay` says what HAL work to\n"
      "run; `.ireeprof` says what happened while that run executed. A normal\n"
      "workflow captures once, replays many times, and optionally captures\n"
      "`.ireeprof` bundles from `iree-benchmark-replay`.\n"
      "\n"
      "## Capture\n"
      "\n"
      "`iree-run-module` and `iree-benchmark-module` can record HAL work with\n"
      "`--device_replay_output=/tmp/model.ireereplay`. Keep the normal "
      "module,\n"
      "function, parameter, input, and device flags unchanged.\n"
      "\n"
      "```bash\n"
      "iree-run-module --device=local-sync --module=model.vmfb \\\n"
      "  --function=main --input=@inputs.txt \\\n"
      "  --device_replay_output=/tmp/model.ireereplay \\\n"
      "  --device_replay_file_policy=reference\n"
      "```\n"
      "\n"
      "Use `--device_replay_file_policy=reference` for large stable parameter\n"
      "archives. FD-backed HAL files are recorded by path and validation "
      "metadata\n"
      "instead of copied into every capture. Use `capture-ranges` for "
      "hermetic\n"
      "correctness replays that only need file bytes observed through HAL "
      "reads.\n"
      "Use `capture-all` only for small external files. Use `fail` when "
      "fd-backed\n"
      "external files must be rejected.\n"
      "\n"
      "The default validation mode is `identity`, which records length, "
      "device,\n"
      "inode, and modification time without scanning file contents. Use "
      "`digest`\n"
      "only when referenced files will be copied or staged to another "
      "filesystem\n"
      "and a full byte scan during capture and replay is acceptable.\n"
      "\n"
      "`iree-run-module` emits standard replay scopes named `init`, "
      "`execute`,\n"
      "and `deinit`. `iree-benchmark-module` emits `execute` scopes around "
      "the\n"
      "same timed region reported by the benchmark.\n"
      "\n"
      "## Run\n"
      "\n"
      "Run a replay once with the same target device shape you want to test:\n"
      "\n"
      "```bash\n"
      "iree-run-replay --device=local-sync /tmp/model.ireereplay\n"
      "```\n"
      "\n"
      "If the capture referenced external files from another mount root, "
      "remap\n"
      "the captured prefix before replay opens files:\n"
      "\n"
      "```bash\n"
      "iree-run-replay --device=local-sync \\\n"
      "  --replay_file_remap=/mnt/capture=/mnt/replay \\\n"
      "  /tmp/model.ireereplay\n"
      "```\n"
      "\n"
      "The remapped file must still match recorded identity or digest "
      "metadata.\n"
      "A mismatch is a hard failure because otherwise the replay could "
      "silently\n"
      "benchmark a different model or dataset.\n"
      "\n"
      "## Substitute Executables\n"
      "\n"
      "Use `iree-dump-replay` to find executable object ids, then substitute "
      "a\n"
      "replacement payload while preserving the captured HAL workload:\n"
      "\n"
      "```bash\n"
      "iree-dump-replay --format=text /tmp/model.ireereplay | grep executable\n"
      "iree-run-replay --device=amdgpu \\\n"
      "  --replay_executable_substitution=4=/tmp/new-kernel.hsaco \\\n"
      "  /tmp/model.ireereplay\n"
      "```\n"
      "\n"
      "For one-executable captures, or when one replacement intentionally "
      "applies\n"
      "to every captured executable, use the `all` selector. Add `@FORMAT` "
      "when\n"
      "the executable cache cannot infer the format from the file.\n"
      "\n"
      "Replay validates replacement ABI before dispatch, including export "
      "counts,\n"
      "constant counts, binding counts, workgroup sizes, and reflected "
      "parameter\n"
      "layouts when the capture has metadata.\n"
      "\n"
      "## Benchmark And Profile\n"
      "\n"
      "`iree-benchmark-replay` runs the same replay under Google Benchmark:\n"
      "\n"
      "```bash\n"
      "iree-benchmark-replay --device=local-sync --benchmark_min_time=20x \\\n"
      "  /tmp/model.ireereplay\n"
      "iree-benchmark-replay --device=local-sync --benchmark_min_time=20x \\\n"
      "  --replay_scope=execute /tmp/model.ireereplay\n"
      "```\n"
      "\n"
      "`--replay_scope=name` reports manual time accumulated inside matching\n"
      "scope markers while still executing the complete capture each "
      "iteration.\n"
      "See `iree-benchmark-replay --help` or `iree-benchmark-replay "
      "--agents_md`\n"
      "for benchmark-only behavior.\n"
      "\n"
      "Capture HAL profiling from replayed benchmark iterations with normal "
      "device\n"
      "profiling flags:\n"
      "\n"
      "```bash\n"
      "iree-benchmark-replay --device=amdgpu --benchmark_min_time=50x \\\n"
      "  --device_profiling_mode=queue-events,device-queue-events \\\n"
      "  --device_profiling_output=/tmp/model-replay.ireeprof \\\n"
      "  /tmp/model.ireereplay\n"
      "```\n"
      "\n"
      "## Inspect\n"
      "\n"
      "`iree-dump-replay --format=text` is for quick inspection. "
      "`--format=jsonl`\n"
      "is for jq and agent workflows:\n"
      "\n"
      "```bash\n"
      "iree-dump-replay --format=jsonl /tmp/model.ireereplay | \\\n"
      "  jq 'select(.kind==\"operation\" and "
      ".operation==\"device.queue_execute\")'\n"
      "iree-dump-replay --format=jsonl /tmp/model.ireereplay | \\\n"
      "  jq 'select(.payload_type==\"replay_scope\") | .payload.name'\n"
      "```\n"
      "\n"
      "JSONL rows carry object ids, operation ids, queue wait/signal lists,\n"
      "buffer binding tables, file references, and replay-file byte ranges.\n"
      "See `iree-dump-replay --help` or `iree-dump-replay --agents_md` for\n"
      "dump-only details.\n"
      "\n"
      "## Failure Contract\n"
      "\n"
      "Replay must fail loudly when it cannot reproduce captured HAL work. "
      "Missing\n"
      "or identity-mismatched referenced files, persistent host write maps "
      "without\n"
      "observable flush/unmap boundaries, host calls, collectives, channels,\n"
      "allocator import/export, opaque external handles, and replacement "
      "executable\n"
      "ABI mismatches should remain visible in dumps and fail in strict "
      "execution\n"
      "instead of being skipped.\n"
      "\n"
      "When debugging a capture, keep the `.ireereplay` and referenced files\n"
      "together, dump JSONL first, run once with `iree-run-replay`, then "
      "benchmark\n"
      "with profiling only after the single-run path is clean.\n",
      file);
}

typedef struct iree_tooling_replay_executable_substitution_t {
  // True when this replacement applies to every captured executable.
  bool match_all;
  // Captured executable object id to replace.
  iree_hal_replay_object_id_t executable_id;
  // Optional replacement executable format.
  iree_string_view_t executable_format;
  // Replacement executable file path.
  iree_string_view_t source_path;
  // Mapped replacement executable file contents.
  iree_io_file_contents_t* file_contents;
} iree_tooling_replay_executable_substitution_t;

typedef struct iree_tooling_replay_executable_substitution_state_t {
  // Replacement entries parsed from --replay_executable_substitution.
  iree_tooling_replay_executable_substitution_t* entries;
  // Number of entries in |entries|.
  iree_host_size_t entry_count;
} iree_tooling_replay_executable_substitution_state_t;

static void iree_tooling_release_replay_executable_substitutions(
    iree_allocator_t host_allocator,
    iree_tooling_replay_executable_substitution_state_t* state) {
  for (iree_host_size_t i = 0; i < state->entry_count; ++i) {
    iree_io_file_contents_free(state->entries[i].file_contents);
  }
  iree_allocator_free(host_allocator, state->entries);
  memset(state, 0, sizeof(*state));
}

static iree_status_t iree_tooling_parse_replay_executable_substitutions(
    iree_allocator_t host_allocator,
    iree_tooling_replay_executable_substitution_state_t* out_state) {
  IREE_ASSERT_ARGUMENT(out_state);
  memset(out_state, 0, sizeof(*out_state));

  iree_flag_string_list_t flag_list =
      FLAG_replay_executable_substitution_list();
  if (flag_list.count == 0) return iree_ok_status();

  iree_host_size_t entry_size = 0;
  if (IREE_UNLIKELY(!iree_host_size_checked_mul(
          flag_list.count, sizeof(*out_state->entries), &entry_size))) {
    return iree_make_status(IREE_STATUS_OUT_OF_RANGE,
                            "replay executable substitution list is too large");
  }
  IREE_RETURN_IF_ERROR(iree_allocator_malloc(host_allocator, entry_size,
                                             (void**)&out_state->entries));
  memset(out_state->entries, 0, entry_size);
  out_state->entry_count = flag_list.count;

  iree_status_t status = iree_ok_status();
  for (iree_host_size_t i = 0; i < flag_list.count && iree_status_is_ok(status);
       ++i) {
    iree_string_view_t selector;
    iree_string_view_t path;
    if (iree_string_view_split(flag_list.values[i], '=', &selector, &path) <
            0 ||
        iree_string_view_is_empty(selector) ||
        iree_string_view_is_empty(path)) {
      status =
          iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
                           "--replay_executable_substitution values must be "
                           "EXECUTABLE_ID=PATH, EXECUTABLE_ID@FORMAT=PATH, "
                           "all=PATH, or all@FORMAT=PATH");
      break;
    }

    iree_string_view_t id_string = selector;
    iree_string_view_t executable_format = iree_string_view_empty();
    iree_string_view_t maybe_format;
    if (iree_string_view_split(selector, '@', &id_string, &maybe_format) >= 0) {
      if (iree_string_view_is_empty(id_string) ||
          iree_string_view_is_empty(maybe_format)) {
        status = iree_make_status(
            IREE_STATUS_INVALID_ARGUMENT,
            "--replay_executable_substitution selector must be "
            "EXECUTABLE_ID, EXECUTABLE_ID@FORMAT, all, or all@FORMAT");
        break;
      }
      executable_format = maybe_format;
    }

    uint64_t executable_id = 0;
    const bool match_all = iree_string_view_equal(id_string, IREE_SV("all"));
    if (match_all) {
      if (flag_list.count != 1) {
        status = iree_make_status(
            IREE_STATUS_INVALID_ARGUMENT,
            "--replay_executable_substitution all selector cannot be combined "
            "with other executable substitutions");
        break;
      }
    } else {
      if (!iree_string_view_atoi_uint64(id_string, &executable_id) ||
          executable_id == IREE_HAL_REPLAY_OBJECT_ID_NONE) {
        status = iree_make_status(
            IREE_STATUS_INVALID_ARGUMENT,
            "--replay_executable_substitution executable selector must be "
            "a non-zero integer id or all");
        break;
      }
      for (iree_host_size_t j = 0; j < i; ++j) {
        if (out_state->entries[j].executable_id == executable_id) {
          status = iree_make_status(
              IREE_STATUS_INVALID_ARGUMENT,
              "--replay_executable_substitution repeats executable id %" PRIu64,
              executable_id);
          break;
        }
      }
    }
    if (!iree_status_is_ok(status)) break;

    out_state->entries[i].match_all = match_all;
    out_state->entries[i].executable_id =
        (iree_hal_replay_object_id_t)executable_id;
    out_state->entries[i].executable_format = executable_format;
    out_state->entries[i].source_path = path;
    status = iree_io_file_contents_map(path, IREE_IO_FILE_ACCESS_READ,
                                       host_allocator,
                                       &out_state->entries[i].file_contents);
  }
  if (!iree_status_is_ok(status)) {
    iree_tooling_release_replay_executable_substitutions(host_allocator,
                                                         out_state);
  }
  return status;
}

static iree_status_t iree_tooling_replay_executable_substitution_callback(
    void* user_data,
    const iree_hal_replay_executable_substitution_request_t* request,
    iree_hal_replay_executable_substitution_t* out_substitution) {
  iree_tooling_replay_executable_substitution_state_t* state =
      (iree_tooling_replay_executable_substitution_state_t*)user_data;
  memset(out_substitution, 0, sizeof(*out_substitution));
  for (iree_host_size_t i = 0; i < state->entry_count; ++i) {
    const iree_tooling_replay_executable_substitution_t* entry =
        &state->entries[i];
    if (!entry->match_all && entry->executable_id != request->executable_id) {
      continue;
    }
    out_substitution->substitute = true;
    out_substitution->source = entry->source_path;
    out_substitution->executable_format = entry->executable_format;
    out_substitution->executable_data = entry->file_contents->const_buffer;
    return iree_ok_status();
  }
  return iree_ok_status();
}

static iree_status_t iree_tooling_parse_replay_file_remaps(
    iree_allocator_t host_allocator,
    iree_hal_replay_file_path_remap_t** out_file_path_remaps,
    iree_host_size_t* out_file_path_remap_count) {
  IREE_ASSERT_ARGUMENT(out_file_path_remaps);
  IREE_ASSERT_ARGUMENT(out_file_path_remap_count);
  *out_file_path_remaps = NULL;
  *out_file_path_remap_count = 0;

  iree_flag_string_list_t flag_list = FLAG_replay_file_remap_list();
  if (flag_list.count == 0) return iree_ok_status();

  iree_host_size_t remap_size = 0;
  if (IREE_UNLIKELY(!iree_host_size_checked_mul(
          flag_list.count, sizeof(**out_file_path_remaps), &remap_size))) {
    return iree_make_status(IREE_STATUS_OUT_OF_RANGE,
                            "replay file remap list is too large");
  }
  iree_hal_replay_file_path_remap_t* file_path_remaps = NULL;
  IREE_RETURN_IF_ERROR(iree_allocator_malloc(host_allocator, remap_size,
                                             (void**)&file_path_remaps));
  memset(file_path_remaps, 0, remap_size);
  for (iree_host_size_t i = 0; i < flag_list.count; ++i) {
    iree_string_view_t captured_prefix;
    iree_string_view_t replay_prefix;
    if (iree_string_view_split(flag_list.values[i], '=', &captured_prefix,
                               &replay_prefix) < 0 ||
        iree_string_view_is_empty(captured_prefix)) {
      iree_allocator_free(host_allocator, file_path_remaps);
      return iree_make_status(
          IREE_STATUS_INVALID_ARGUMENT,
          "--replay_file_remap values must be captured_prefix=replay_prefix");
    }
    file_path_remaps[i].captured_prefix = captured_prefix;
    file_path_remaps[i].replay_prefix = replay_prefix;
  }
  *out_file_path_remaps = file_path_remaps;
  *out_file_path_remap_count = flag_list.count;
  return iree_ok_status();
}

static iree_status_t iree_tooling_create_device_group_from_list(
    iree_hal_device_list_t* device_list, iree_allocator_t host_allocator,
    iree_hal_device_group_t** out_device_group) {
  IREE_ASSERT_ARGUMENT(device_list);
  IREE_ASSERT_ARGUMENT(out_device_group);
  *out_device_group = NULL;

  iree_async_frontier_tracker_t* frontier_tracker = NULL;
  IREE_RETURN_IF_ERROR(iree_async_frontier_tracker_create(
      iree_async_frontier_tracker_options_default(), host_allocator,
      &frontier_tracker));

  iree_hal_device_group_builder_t builder;
  iree_hal_device_group_builder_initialize(&builder, frontier_tracker);
  iree_async_frontier_tracker_release(frontier_tracker);

  iree_status_t status = iree_ok_status();
  for (iree_host_size_t i = 0;
       i < device_list->count && iree_status_is_ok(status); ++i) {
    status = iree_hal_device_group_builder_add_device(&builder,
                                                      device_list->devices[i]);
  }
  if (iree_status_is_ok(status)) {
    status = iree_hal_device_group_builder_finalize(&builder, host_allocator,
                                                    out_device_group);
  } else {
    iree_hal_device_group_builder_deinitialize(&builder);
  }
  return status;
}

int main(int argc, char** argv) {
  IREE_TRACE_APP_ENTER();
  IREE_TRACE_ZONE_BEGIN(z0);

  iree_flags_set_usage("iree-run-replay", kIreeRunReplayUsage);
  iree_flags_parse_checked(IREE_FLAGS_PARSE_MODE_DEFAULT, &argc, &argv);
  if (FLAG_agents_md) {
    iree_run_replay_print_agent_markdown(stdout);
    fflush(stdout);
    IREE_TRACE_ZONE_END(z0);
    IREE_TRACE_APP_EXIT(EXIT_SUCCESS);
    return EXIT_SUCCESS;
  }

  iree_allocator_t host_allocator = iree_allocator_system();
  iree_status_t status = iree_ok_status();
  if (argc != 2) {
    status = iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
                              "expected one replay file path argument");
  }

  iree_io_file_contents_t* file_contents = NULL;
  if (iree_status_is_ok(status)) {
    status = iree_io_file_contents_map(iree_make_cstring_view(argv[1]),
                                       IREE_IO_FILE_ACCESS_READ, host_allocator,
                                       &file_contents);
  }

  iree_async_proactor_pool_t* proactor_pool = NULL;
  if (iree_status_is_ok(status)) {
    status = iree_async_proactor_pool_create(
        iree_numa_node_count(), /*node_ids=*/NULL,
        iree_async_proactor_pool_options_default(), host_allocator,
        &proactor_pool);
  }

  iree_hal_device_create_params_t create_params =
      iree_hal_device_create_params_default();
  create_params.proactor_pool = proactor_pool;
  iree_hal_device_list_t* device_list = NULL;
  if (iree_status_is_ok(status)) {
    status = iree_hal_create_devices_from_flags(
        iree_hal_available_driver_registry(), iree_hal_default_device_uri(),
        &create_params, host_allocator, &device_list);
  }

  iree_hal_device_group_t* device_group = NULL;
  if (iree_status_is_ok(status)) {
    status = iree_tooling_create_device_group_from_list(
        device_list, host_allocator, &device_group);
  }

  iree_hal_profiling_from_flags_t* profiling = NULL;
  if (iree_status_is_ok(status)) {
    status = iree_hal_begin_device_group_profiling_from_flags(
        device_group, host_allocator, &profiling);
  }

  iree_hal_replay_file_path_remap_t* file_path_remaps = NULL;
  iree_host_size_t file_path_remap_count = 0;
  if (iree_status_is_ok(status)) {
    status = iree_tooling_parse_replay_file_remaps(
        host_allocator, &file_path_remaps, &file_path_remap_count);
  }

  iree_tooling_replay_executable_substitution_state_t executable_substitutions;
  memset(&executable_substitutions, 0, sizeof(executable_substitutions));
  if (iree_status_is_ok(status)) {
    status = iree_tooling_parse_replay_executable_substitutions(
        host_allocator, &executable_substitutions);
  }

  if (iree_status_is_ok(status)) {
    iree_hal_replay_execute_options_t options =
        iree_hal_replay_execute_options_default();
    options.file_path_remap_count = file_path_remap_count;
    options.file_path_remaps = file_path_remaps;
    if (executable_substitutions.entry_count != 0) {
      options.executable_substitution_callback.fn =
          iree_tooling_replay_executable_substitution_callback;
      options.executable_substitution_callback.user_data =
          &executable_substitutions;
    }
    status = iree_hal_replay_execute_file(
        file_contents->const_buffer, device_group, &options, host_allocator);
  }

  status =
      iree_status_join(status, iree_hal_end_profiling_from_flags(profiling));
  iree_tooling_release_replay_executable_substitutions(
      host_allocator, &executable_substitutions);
  iree_allocator_free(host_allocator, file_path_remaps);
  iree_hal_device_group_release(device_group);
  iree_hal_device_list_free(device_list);
  iree_async_proactor_pool_release(proactor_pool);
  iree_io_file_contents_free(file_contents);

  int exit_code = EXIT_SUCCESS;
  if (!iree_status_is_ok(status)) {
    iree_status_fprint(stderr, status);
    iree_status_free(status);
    exit_code = EXIT_FAILURE;
  }
  fflush(stderr);

  IREE_TRACE_ZONE_END(z0);
  IREE_TRACE_APP_EXIT(exit_code);
  return exit_code;
}
