///|
async fn validate_executable(path : String) -> String {
  guard @fs.exists(path) else { raise ExecutableNotFound(path) }
  guard @fs.kind(path) is Regular else { raise ExecutableNotAFile(path) }
  guard @fs.can_execute(path) else {
    raise @launch.LaunchError("file is not executable: \{path}")
  }
  @fs.realpath(path)
}

///|
async fn adapter_command() -> String {
  match @env.get_env_var("MOONDBG_LLDB_DAP") {
    Some(command) if command != "" => return command
    _ => ()
  }
  let discovered = Some(
    @process.collect_stdout("xcrun", ["--find", "lldb-dap"], inherit_env=true),
  ) catch {
    _ => None
  }
  match discovered {
    Some((0, output)) => {
      let path = output.text().trim().to_owned() catch { _ => "" }
      if path == "" {
        "lldb-dap"
      } else {
        path
      }
    }
    _ => "lldb-dap"
  }
}

///|
fn adapter_preparation_timeout_ms() -> Int {
  let default_timeout_ms = 10000
  guard @env.get_env_var("MOONDBG_ADAPTER_TIMEOUT_MS") is Some(value) else {
    return default_timeout_ms
  }
  let parsed = Some(@string.parse_int(value, base=10)) catch { _ => None }
  guard parsed is Some(timeout_ms) && timeout_ms > 0 else {
    return default_timeout_ms
  }
  timeout_ms
}

///|
async fn run(
  executable : String,
  debug_context : @debugger.DebugContext,
) -> Unit {
  let executable = validate_executable(executable)
  @async.with_task_group(async fn(group) {
    let backend = @lldb_dap.LldbDapBackend::start(
      group,
      adapter_command(),
      preparation_timeout_ms=adapter_preparation_timeout_ms(),
    )
    let session = @debugger.DebugSession::new(
      executable,
      backend.as_debug_backend(),
      debug_context~,
    )
    defer session.close()
    let history = @readline.History::new()
    let editor = @readline.LineEditor::open(history~)
    let registry = default_command_registry()
    let renderer = HumanRenderer::new()
    let context = ReplContext::new(session, registry, renderer)
    run_repl(editor, history, context)
    session.disconnect()
    println("Debugger exited.")
  })
}

///|
async fn main {
  // Dispatch before argparse: help/error output from the human CLI must never
  // enter the DAP stdout stream. The existing executable remains the launcher.
  let argv = @env.args()
  if argv.length() > 1 && argv[1] == "--build-worker" {
    let code = @launch.run_worker(argv[2:].to_owned()) catch {
      error => {
        @stdio.stderr.write(
          "moondbg build worker: \{@debug.to_string(error)}\n",
        ) catch {
          _ => ()
        }
        1
      }
    }
    exit_process(code)
    return
  }
  if argv.length() > 1 && argv[1] == "--dap" {
    guard argv.length() == 2 else {
      @stdio.stderr.write("moondbg --dap accepts no additional arguments.\n") catch {
        _ => ()
      }
      exit_process(2)
      return
    }
    @dap_frontend.serve(@stdio.stdin, @stdio.stdout) catch {
      error => {
        @stdio.stderr.write("moondbg DAP: \{@debug.to_string(error)}\n") catch {
          _ => ()
        }
        exit_process(1)
      }
    }
    return
  }
  try {
    let matches = command().parse()
    let target = target_argument(matches)
    let explicit_context = debug_context_argument(matches)
    let prepared = @launch.prepare_target(target, output=(_, text) => {
      @stdio.stderr.write(text)
    })
    let executable = prepared.executable
    let debug_context = prepared.debug_context
    let debug_context = if matches.values.contains("current-package") {
      explicit_context
    } else {
      debug_context
    }
    run(executable, debug_context)
  } catch {
    error => {
      let message = match error {
        @debugger.AdapterFailure(message) =>
          "debugger adapter failed: \{message}"
        @launch.LaunchError(message) => message
        _ => @debug.to_string(error)
      }
      @stdio.stderr.write("moondbg: \{message}\n") catch {
        _ => ()
      }
      exit_process(1)
    }
  }
}

///|
extern "c" fn exit_process(code : Int) = "exit"