///|
fn cache_store_root(workspace_root : String) -> String {
  let configured = @xsys.get_env_var("ACTRUN_CACHE_ROOT").unwrap_or(
    "_build/actrun/cache",
  )
  ignore(ensure_exec_dir_recursive(configured))
  let workspace_key = sanitize_task_id(
    absolute_exec_path(resolve_task_cwd(workspace_root, "")),
  )
  let root = absolute_exec_path(configured + "/" + workspace_key)
  ignore(ensure_exec_dir_recursive(root))
  root
}

///|
fn cache_sequence_path(workspace_root : String) -> String {
  cache_store_root(workspace_root) + "/__actrun_sequence.txt"
}

///|
fn cache_key_metadata_path(cache_root : String) -> String {
  cache_root + "/__actrun_cache_key.txt"
}

///|
fn cache_sequence_metadata_path(cache_root : String) -> String {
  cache_root + "/__actrun_cache_sequence.txt"
}

///|
fn cache_store_path(workspace_root : String, key : String) -> String {
  cache_store_root(workspace_root) + "/" + sanitize_task_id(key)
}

///|
#warnings("-deprecated")
fn action_cache_parse_int64(value : StringView) -> Int64 raise {
  @strconv.parse_int64(value)
}

///|
fn read_exec_int(path : String) -> Int? {
  guard read_exec_text(path) is Some(content) else { return None }
  let trimmed = content.trim(chars=" \t\r\n").to_owned()
  if trimmed.length() == 0 {
    return None
  }
  let parsed = action_cache_parse_int64(trimmed) catch { _ => return None }
  Some(parsed.to_int())
}

///|
fn write_cache_metadata(
  workspace_root : String,
  key : String,
  cache_root : String,
) -> Bool {
  let next_sequence = read_exec_int(cache_sequence_path(workspace_root)).unwrap_or(
      0,
    ) +
    1
  if !write_exec_text(
      cache_sequence_path(workspace_root),
      next_sequence.to_string(),
    ) {
    return false
  }
  if !write_exec_text(cache_key_metadata_path(cache_root), key) {
    return false
  }
  write_exec_text(
    cache_sequence_metadata_path(cache_root),
    next_sequence.to_string(),
  )
}

///|
fn read_cache_entry_key(cache_root : String) -> String? {
  let key_path = cache_key_metadata_path(cache_root)
  if @xfs.path_exists(key_path) {
    return read_exec_text(key_path).map(text => {
      text.trim(chars=" \t\r\n").to_owned()
    })
  }
  None
}

///|
fn read_cache_entry_sequence(cache_root : String) -> Int {
  read_exec_int(cache_sequence_metadata_path(cache_root)).unwrap_or(0)
}

///|
fn restore_key_candidates(
  key : String,
  restore_keys_input : String,
) -> Array[String] {
  let candidates : Array[String] = []
  candidates.push(key)
  for restore_key in split_nonempty_lines(restore_keys_input) {
    candidates.push(restore_key)
  }
  candidates
}

///|
priv enum CacheRestoreLookup {
  Exact(String)
  Partial(String)
  Miss
  Err(String)
}

///|
fn resolve_cache_restore_lookup(
  workspace_root : String,
  key : String,
  restore_keys_input : String,
) -> CacheRestoreLookup {
  let exact_root = cache_store_path(workspace_root, key)
  if @xfs.path_exists(exact_root) {
    return Exact(key)
  }
  let store_root = cache_store_root(workspace_root)
  let entries = try @xfs.read_dir(store_root) catch {
    _ => return Err("failed to inspect cache store")
  } noraise {
    value => value
  }
  let candidates = restore_key_candidates(key, restore_keys_input)
  for candidate in candidates {
    let mut selected_key = ""
    let mut selected_sequence = -1
    for entry in entries {
      if entry == "__actrun_sequence.txt" {
        continue
      }
      let entry_root = store_root + "/" + entry
      guard read_cache_entry_key(entry_root) is Some(entry_key) else {
        continue
      }
      let sequence = read_cache_entry_sequence(entry_root)
      if entry_key.has_prefix(candidate) && sequence >= selected_sequence {
        selected_key = entry_key
        selected_sequence = sequence
      }
    }
    if selected_key.length() > 0 {
      return Partial(selected_key)
    }
  }
  Miss
}

///|
fn workspace_relative_store_path(
  workspace_root : String,
  resolved_path : String,
) -> String? {
  let workspace_abs = absolute_exec_path(resolve_task_cwd(workspace_root, ""))
  let resolved_abs = absolute_exec_path(resolved_path)
  if resolved_abs == workspace_abs {
    return Some("__root__")
  }
  let prefix = workspace_abs + "/"
  if resolved_abs.has_prefix(prefix) {
    return Some(
      exec_text_slice(resolved_abs, prefix.length(), resolved_abs.length()),
    )
  }
  None
}

///|
fn normalize_cache_path(path : String) -> String {
  let trimmed = path.trim(chars=" \t\r\n").to_owned()
  if trimmed.has_prefix("~/") {
    let home = @xsys.get_env_var("HOME").unwrap_or("")
    if home.length() > 0 {
      return home + exec_text_slice(trimmed, 1, trimmed.length())
    }
  }
  if trimmed == "~" {
    return @xsys.get_env_var("HOME").unwrap_or(trimmed)
  }
  trimmed
}

///|
fn normalize_cache_paths(paths : Array[String]) -> Array[String] {
  let result : Array[String] = []
  for path in paths {
    result.push(normalize_cache_path(path))
  }
  result
}

///|
fn execute_save_cache_native(
  plan : TaskPlan,
  workspace_root : String,
  resolved_plan_env : Map[String, String],
) -> TaskRunReport {
  let key = resolved_plan_env.get("INPUT_KEY").unwrap_or("")
  let path_input = resolved_plan_env.get("INPUT_PATH").unwrap_or("")
  if key.length() == 0 {
    return task_report_failure(
      plan, workspace_root, "cache-save requires with.key",
    )
  }
  let cache_paths = normalize_cache_paths(split_nonempty_lines(path_input))
  if cache_paths.length() == 0 {
    return task_report_failure(
      plan, workspace_root, "cache-save requires with.path",
    )
  }
  let cache_root = cache_store_path(workspace_root, key)
  if !exec_remove_tree(cache_root) {
    return task_report_failure(
      plan,
      workspace_root,
      "failed to clear cache store for '\{key}'",
    )
  }
  if !ensure_exec_dir_recursive(cache_root) {
    return task_report_failure(
      plan,
      workspace_root,
      "failed to prepare cache store for '\{key}'",
    )
  }
  let mut copied = false
  for input_path in cache_paths {
    let resolved_path = resolve_task_cwd(workspace_root, input_path)
    if !@xfs.path_exists(resolved_path) {
      continue
    }
    guard workspace_relative_store_path(workspace_root, resolved_path)
      is Some(relative_path) else {
      return task_report_failure(
        plan,
        workspace_root,
        "cache-save path '\{input_path}' must stay within the workspace",
      )
    }
    let target = cache_root + "/" + relative_path
    if !exec_copy_tree(resolved_path, target) {
      return task_report_failure(
        plan,
        workspace_root,
        "failed to save cache path '\{input_path}'",
      )
    }
    copied = true
  }
  if !copied {
    return task_report_failure(
      plan,
      workspace_root,
      "cache-save found no files for '\{path_input}'",
    )
  }
  if !write_cache_metadata(workspace_root, key, cache_root) {
    return task_report_failure(
      plan,
      workspace_root,
      "failed to write cache metadata for '\{key}'",
    )
  }
  task_report_success(plan, workspace_root)
}

///|
fn cache_input_result_failure(
  plan : TaskPlan,
  workspace_root : String,
  action_name : String,
  missing_key : Bool,
) -> TaskExecutionResult {
  let message = if missing_key {
    action_name + " requires with.key"
  } else {
    action_name + " requires with.path"
  }
  failure_execution_result(plan, workspace_root, message)
}

///|
fn restore_cache_store_native(
  workspace_root : String,
  key : String,
  restore_keys_input : String,
  lookup_only : Bool,
) -> CacheRestoreLookup {
  let lookup = resolve_cache_restore_lookup(
    workspace_root, key, restore_keys_input,
  )
  let matched_key = match lookup {
    Exact(found_key) => found_key
    Partial(found_key) => found_key
    Miss => return Miss
    Err(message) => return Err(message)
  }
  if lookup_only {
    return match lookup {
      Exact(_) => Exact(matched_key)
      Partial(_) => Partial(matched_key)
      Miss => Miss
      Err(message) => Err(message)
    }
  }
  let cache_root = cache_store_path(workspace_root, matched_key)
  let entries = try @xfs.read_dir(cache_root) catch {
    _ => return Err("failed to inspect cache '\{matched_key}'")
  } noraise {
    value => value
  }
  for entry in entries {
    if entry == "__actrun_cache_key.txt" ||
      entry == "__actrun_cache_sequence.txt" {
      continue
    }
    let source = cache_root + "/" + entry
    if entry == "__root__" {
      let root_entries = try @xfs.read_dir(source) catch {
        _ => return Err("failed to inspect cache '\{matched_key}' root")
      } noraise {
        value => value
      }
      for root_entry in root_entries {
        // Reject entries that could escape the workspace via path traversal
        if root_entry.contains("..") {
          continue
        }
        if !exec_copy_tree(
            source + "/" + root_entry,
            workspace_root + "/" + root_entry,
          ) {
          return Err("failed to restore cache '\{matched_key}'")
        }
      }
    } else if entry.contains("..") {
      return Err("cache entry '\{entry}' contains invalid path component")
    } else if !exec_copy_tree(source, workspace_root + "/" + entry) {
      return Err("failed to restore cache '\{matched_key}'")
    }
  }
  match lookup {
    Exact(_) => Exact(matched_key)
    Partial(_) => Partial(matched_key)
    Miss => Miss
    Err(message) => Err(message)
  }
}

///|
fn execute_restore_cache_native(
  plan : TaskPlan,
  workspace_root : String,
  resolved_plan_env : Map[String, String],
) -> TaskExecutionResult {
  let key = resolved_plan_env.get("INPUT_KEY").unwrap_or("")
  let restore_keys_input = resolved_plan_env
    .get("INPUT_RESTORE_KEYS")
    .unwrap_or("")
  let lookup_only = parse_bool_false_default(
    resolved_plan_env.get("INPUT_LOOKUP_ONLY").unwrap_or(""),
  )
  let fail_on_cache_miss = parse_bool_false_default(
    resolved_plan_env.get("INPUT_FAIL_ON_CACHE_MISS").unwrap_or(""),
  )
  let path_input = resolved_plan_env.get("INPUT_PATH").unwrap_or("")
  if key.length() == 0 {
    return cache_input_result_failure(
      plan, workspace_root, "cache-restore", true,
    )
  }
  if split_nonempty_lines(path_input).length() == 0 {
    return cache_input_result_failure(
      plan, workspace_root, "cache-restore", false,
    )
  }
  let cache_hit = match
    restore_cache_store_native(
      workspace_root, key, restore_keys_input, lookup_only,
    ) {
    Exact(_) => "true"
    Partial(_) => "false"
    Miss =>
      if fail_on_cache_miss {
        return failure_execution_result(
          plan,
          workspace_root,
          "cache miss for key '\{key}' and fail-on-cache-miss is true",
        )
      } else {
        "false"
      }
    Err(message) =>
      return failure_execution_result(plan, workspace_root, message)
  }
  {
    report: task_report_success(plan, workspace_root),
    env_updates: {},
    path_entries: [],
    output_values: { "cache-hit": cache_hit },
    state_updates: {},
  }
}

///|
fn execute_cache_action_native(
  plan : TaskPlan,
  workspace_root : String,
  resolved_plan_env : Map[String, String],
) -> TaskExecutionResult {
  let key = resolved_plan_env.get("INPUT_KEY").unwrap_or("")
  let restore_keys_input = resolved_plan_env
    .get("INPUT_RESTORE_KEYS")
    .unwrap_or("")
  let lookup_only = parse_bool_false_default(
    resolved_plan_env.get("INPUT_LOOKUP_ONLY").unwrap_or(""),
  )
  let fail_on_cache_miss = parse_bool_false_default(
    resolved_plan_env.get("INPUT_FAIL_ON_CACHE_MISS").unwrap_or(""),
  )
  let save_always = parse_bool_false_default(
    resolved_plan_env.get("INPUT_SAVE_ALWAYS").unwrap_or("false"),
  )
  let path_input = resolved_plan_env.get("INPUT_PATH").unwrap_or("")
  if key.length() == 0 {
    return cache_input_result_failure(plan, workspace_root, "cache", true)
  }
  if split_nonempty_lines(path_input).length() == 0 {
    return cache_input_result_failure(plan, workspace_root, "cache", false)
  }
  let cache_hit = match
    restore_cache_store_native(
      workspace_root, key, restore_keys_input, lookup_only,
    ) {
    Exact(_) => "true"
    Partial(_) => "false"
    Miss =>
      if fail_on_cache_miss {
        return {
          report: task_report_failure(
            plan,
            workspace_root,
            "cache miss for key '\{key}' and fail-on-cache-miss is true",
          ),
          env_updates: {},
          path_entries: [],
          output_values: {},
          state_updates: {
            "CACHE_KEY": key,
            "CACHE_PATH": path_input,
            "CACHE_HIT": "",
            "CACHE_SKIP_POST": "true",
          },
        }
      } else {
        ""
      }
    Err(message) =>
      return failure_execution_result(plan, workspace_root, message)
  }
  {
    report: task_report_success(plan, workspace_root),
    env_updates: {},
    path_entries: [],
    output_values: { "cache-hit": cache_hit },
    state_updates: {
      "CACHE_KEY": key,
      "CACHE_PATH": path_input,
      "CACHE_HIT": cache_hit,
      "CACHE_SAVE_ALWAYS": if save_always {
        "true"
      } else {
        ""
      },
    },
  }
}

///|
fn execute_cache_post_save_native(
  plan : TaskPlan,
  workspace_root : String,
  resolved_plan_env : Map[String, String],
  state : JobRuntimeState,
) -> TaskRunReport {
  let saved_state = action_scope_state(state, plan.action_scope)
  let save_always = saved_state.get("CACHE_SAVE_ALWAYS").unwrap_or("") == "true"
  if !save_always {
    if saved_state.get("CACHE_SKIP_POST").unwrap_or("") == "true" {
      return task_report_success(plan, workspace_root)
    }
    if saved_state.get("CACHE_HIT").unwrap_or("") == "true" {
      return task_report_success(plan, workspace_root)
    }
  }
  let key = saved_state
    .get("CACHE_KEY")
    .unwrap_or(resolved_plan_env.get("INPUT_KEY").unwrap_or(""))
  let path_input = saved_state
    .get("CACHE_PATH")
    .unwrap_or(resolved_plan_env.get("INPUT_PATH").unwrap_or(""))
  return execute_save_cache_native(
    plan,
    workspace_root,
    merge_runner_env(resolved_plan_env, {
      "INPUT_KEY": key,
      "INPUT_PATH": path_input,
    }),
  )
}

///|
fn execute_cache_builtin_native(
  plan : TaskPlan,
  workspace_root : String,
  resolved_plan_env : Map[String, String],
  state : JobRuntimeState,
  action_kind : String,
) -> TaskExecutionResult {
  if action_kind == "cache" {
    return execute_cache_action_native(plan, workspace_root, resolved_plan_env)
  }
  if action_kind == "cache-restore" {
    return execute_restore_cache_native(plan, workspace_root, resolved_plan_env)
  }
  if action_kind == "cache-save" {
    return builtin_task_result(
      execute_save_cache_native(plan, workspace_root, resolved_plan_env),
    )
  }
  if action_kind == "cache-save-post" {
    return builtin_task_result(
      execute_cache_post_save_native(
        plan, workspace_root, resolved_plan_env, state,
      ),
    )
  }
  cache_input_result_failure(plan, workspace_root, action_kind, true)
}