///|
fn cli_usage() -> String {
  (
    #|MoonVCS - a minimal content-addressed version control system
    #|
    #|Usage:
    #|  moonvcs init
    #|  moonvcs clone  
    #|  moonvcs receive-init
    #|  moonvcs receive-policy-install 
    #|  moonvcs bundle    [target|HEAD]
    #|  moonvcs receive 
    #|  moonvcs push   
    #|  moonvcs add ...
    #|  moonvcs status
    #|  moonvcs diff [--staged|--cached|HEAD] [-- ...]
    #|  moonvcs branch [name]
    #|  moonvcs switch [--dry-run] 
    #|  moonvcs commit -m 
    #|  moonvcs log [--max-count N] [--offline]
    #|  moonvcs show  [--offline]
    #|  moonvcs storage-stats
    #|  moonvcs storage-state 
    #|  moonvcs recover --check
    #|  moonvcs --offline  ...
    #|
    #|Switch backups: .moonvcs/switch-history//before/
    #|Successful switches retain backups; disk usage grows. No automatic cleanup.
    #|Interrupted switch: preserve .moonvcs/switch-transaction/ and run recover --check.
    #|
  )
}

///|
fn current_directory() -> String raise VcsError {
  match @env.current_dir() {
    Some(path) => path
    None => raise VcsError::InvalidCommand("cannot determine current directory")
  }
}

///|
fn parse_commit_message(args : ArrayView[String]) -> String raise VcsError {
  if args.length() == 2 && (args[0] == "-m" || args[0] == "--message") {
    return args[1]
  }
  raise VcsError::InvalidCommand("commit requires -m ")
}

///|
async fn cli_path(root : String, cwd : String, input : String) -> String {
  let relative = if input == "." {
    ""
  } else {
    normalize_repository_path(input)
  }
  let canonical_cwd = @fs.realpath(cwd)
  if canonical_cwd == root {
    if relative == "" {
      "."
    } else {
      relative
    }
  } else {
    if !within_root(root, canonical_cwd) {
      raise VcsError::InvalidPath("current directory is outside repository")
    }
    let prefix = canonical_cwd[root.length() + 1:].to_owned()
    if relative == "" {
      prefix
    } else {
      prefix + "/" + relative
    }
  }
}

///|
async fn resolve_revision(repository : Repository, value : String) -> ObjectId {
  if value == "HEAD" {
    match repository.head() {
      Some(id) => id
      None => raise VcsError::InvalidCommand("HEAD does not contain a commit")
    }
  } else {
    ObjectId::parse(value)
  }
}

///|
fn print_object(id : ObjectId, object : VcsObject) -> Unit {
  match object {
    BlobObject(blob) => {
      println("blob \{id.hex}")
      println("size \{blob.content.length()}")
      println(safe_policy_text(@utf8.decode(blob.content))) catch {
        _ => println("")
      }
    }
    TreeObject(tree) => {
      println("tree \{id.hex}")
      for entry in tree.entries {
        let kind = match entry.kind {
          BlobEntry => "blob"
          TreeEntry => "tree"
        }
        println(
          "\{kind} \{entry.object_id.hex} " + safe_policy_text(entry.name),
        )
      }
    }
    CommitObject(commit) => {
      println("commit \{id.hex}")
      println("tree \{commit.tree_id.hex}")
      for parent in commit.parents {
        println("parent \{parent.hex}")
      }
      println("author " + safe_policy_text(commit.author))
      println("timestamp \{commit.timestamp}")
      println("")
      println("    " + safe_policy_text(commit.message))
    }
  }
}

///|
fn cli_expected_old(text : String) -> ObjectId? raise {
  if text == "none" {
    None
  } else {
    Some(ObjectId::parse(text))
  }
}

///|
fn print_receive_report(report : ReceiveReport) -> Unit {
  println("Push accepted")
  let value : Json = {
    "status": "accepted",
    "ref": report.ref_name,
    "target": report.target.hex,
    "checked_commits": report.checked_commits,
    "promoted_objects": report.promoted_objects,
    "policy_revision": report.policy_revision,
  }
  println(value.stringify())
}

///|
async fn receive_cli_executable() -> String {
  let program = @env.args()[0]
  if program.contains("/") {
    return @fs.realpath(program)
  }
  if @env.get_env_var("PATH") is Some(paths) {
    for directory in paths.split(":") {
      let path = directory.to_owned() + "/" + program
      if @fs.exists(path) {
        return @fs.realpath(path)
      }
    }
  }
  raise VcsError::ReceivePolicyError(
    "cannot locate installed receiver executable",
  )
}

///|
async fn invoke_receive_process(server_root : String, wire : Bytes) -> Unit {
  let executable = receive_cli_executable()
  let code = @async.with_task_group() <| group => {
    let (stdin, writer) = @process.write_to_process()
    group.spawn_bg(() => {
      defer writer.close()
      writer.write(wire)
    })
    @process.run(
      executable,
      ["receive", "--stdin"],
      cwd=server_root,
      stdin~,
      inherit_env=false,
      cancel_handler=@process.hard_cancel(),
    )
  }
  if code != 0 {
    raise VcsError::ReceiveProcessExit(code)
  }
}

///|
pub async fn run_cli(argv : ArrayView[String]) -> Unit {
  let mut offline = @env.get_env_var("MOONVCS_OFFLINE") == Some("1")
  let argv = if !argv.is_empty() && argv[0] == "--offline" {
    offline = true
    argv[1:]
  } else {
    argv
  }
  if argv.is_empty() || argv[0] == "help" || argv[0] == "--help" {
    println(cli_usage())
    return
  }
  let cwd = current_directory()
  if [
      "receive-init", "receive-policy-install", "bundle", "receive", "push", "add",
      "status", "diff", "commit", "log", "show", "branch", "switch",
    ].contains(argv[0]) {
    assert_no_switch(find_repository_root(cwd))
  }
  match argv[0] {
    "recover" => {
      if argv.length() != 2 || argv[1] != "--check" {
        raise VcsError::InvalidCommand(
          "only read-only recover --check is supported",
        )
      }
      println(recovery_report(find_repository_root(cwd)))
    }
    "receive-init" => {
      if argv.length() != 1 {
        raise VcsError::InvalidCommand("receive-init takes no arguments")
      }
      let server = Repository::at(find_repository_root(cwd))
      install_receive_policy(
        server,
        default_policy_configuration(),
        initialize=true,
      )
      println("Initialized trusted server on-receive policy")
    }
    "receive-policy-install" => {
      if argv.length() != 2 {
        raise VcsError::InvalidCommand(
          "receive-policy-install requires a trusted local configuration",
        )
      }
      let text = @utf8.decode(read_bounded_storage_file(argv[1], 65536))
      install_receive_policy(Repository::at(find_repository_root(cwd)), text)
      println("Installed trusted server on-receive policy")
    }
    "bundle" => {
      if argv.length() != 4 && argv.length() != 5 {
        raise VcsError::InvalidCommand(
          "bundle requires output, ref, expected-old and optional target",
        )
      }
      let source = Repository::at(
        find_repository_root(cwd),
        offline~,
        fetch_events=true,
      )
      let target = resolve_revision(
        source,
        if argv.length() == 5 {
          argv[4]
        } else {
          "HEAD"
        },
      )
      let request = prepare_receive_request(
        source,
        argv[2],
        cli_expected_old(argv[3]),
        target,
      )
      @fs.write_file(
        argv[1],
        encode_receive_request(request),
        create_mode=CreateNew,
        sync=Full,
      )
      println("Bundle created " + request.target.hex)
    }
    "receive" => {
      if argv.length() != 2 {
        raise VcsError::InvalidCommand("receive requires one bundle file")
      }
      let server = Repository::at(find_repository_root(cwd), offline=true)
      if argv[1] == "--stdin" {
        let bytes = @async.with_timeout(10000, () => {
          read_policy_pipe(@stdio.stdin, max_receive_bundle_bytes)
        }) catch {
          _ =>
            raise VcsError::ReceiveInvalid(
              "invalid oversized or timed-out receive input",
            )
        }
        print_receive_report(
          receive_candidate(server, decode_receive_request(bytes)),
        )
      } else {
        print_receive_report(receive_bundle_file(server, argv[1]))
      }
    }
    "push" => {
      if argv.length() != 4 || offline {
        raise VcsError::InvalidCommand(
          "push requires server, ref and expected-old (or none)",
        )
      }
      let source = Repository::at(find_repository_root(cwd), fetch_events=true)
      let target = resolve_revision(source, "HEAD")
      let request = prepare_receive_request(
        source,
        argv[2],
        cli_expected_old(argv[3]),
        target,
      )
      // Independent receiver process: only bounded wire bytes cross the entry.
      // Production still requires an authenticated endpoint and OS separation.
      let wire = encode_receive_request(request)
      invoke_receive_process(@fs.realpath(argv[1]), wire)
    }
    "clone" => {
      if argv.length() != 3 || offline {
        raise VcsError::InvalidCommand(
          "clone requires a remote and new directory; not available offline",
        )
      }
      let repository = clone_repository(argv[1], argv[2], fetch_events=true)
      let stats = repository.store.transfer_stats()
      println("Cloned " + safe_policy_text(repository.root))
      println(
        "Clone downloaded objects=" +
        stats.received_objects.to_string() +
        " object_bytes=" +
        stats.received_bytes.to_string(),
      )
    }
    "init" => {
      if argv.length() != 1 {
        raise VcsError::InvalidCommand("init does not accept arguments")
      }
      let repository = init_repository(cwd)
      println(
        "Initialized empty MoonVCS repository in " +
        safe_policy_text(repository.root()) +
        "/.moonvcs",
      )
    }
    "add" => {
      let root = find_repository_root(cwd)
      let repository = Repository::at(root, offline~, fetch_events=true)
      let paths : Array[String] = []
      for input in argv[1:] {
        paths.push(cli_path(root, cwd, input))
      }
      let added = repository.add(paths)
      for entry in added {
        println(
          "staged " + safe_policy_text(entry.path) + " \{entry.blob_id.hex}",
        )
      }
    }
    "status" => {
      if argv.length() != 1 {
        raise VcsError::InvalidCommand("status does not accept arguments")
      }
      let repository = Repository::at(
        find_repository_root(cwd),
        offline~,
        fetch_events=true,
      )
      let lines = repository.status()
      if lines.is_empty() {
        println("clean")
      } else {
        for line in lines {
          println(safe_policy_text(line))
        }
      }
    }
    "diff" => {
      let root = find_repository_root(cwd)
      let mut mode = WorkingDiff
      let mut selected = false
      let mut paths_only = false
      let paths : Array[String] = []
      for arg in argv[1:] {
        if paths_only {
          paths.push(cli_path(root, cwd, arg))
        } else if arg == "--" {
          paths_only = true
        } else if !selected && (arg == "--staged" || arg == "--cached") {
          mode = StagedDiff
          selected = true
        } else if !selected && arg == "HEAD" {
          mode = HeadDiff
          selected = true
        } else {
          raise VcsError::DiffFailed(
            "usage: moonvcs diff [--staged|--cached|HEAD] [-- ...]",
          )
        }
      }
      // No LazyObjectStore.get: even offline lazy reads can repair metadata.
      @stdio.stdout.write(Repository::at(root, offline~).diff(mode, paths~))
    }
    "branch" => {
      let repository = Repository::at(find_repository_root(cwd), offline~)
      if argv.length() == 1 {
        for line in repository.branches() {
          println(line)
        }
      } else if argv.length() == 2 {
        let id = repository.create_branch(argv[1])
        println("Created branch " + argv[1] + " at " + id.hex)
      } else {
        raise VcsError::CheckoutFailed("usage: moonvcs branch [name]")
      }
    }
    "switch" => {
      let repository = Repository::at(find_repository_root(cwd), offline~)
      let plan = if argv.length() == 3 && argv[1] == "--dry-run" {
        repository.switch_plan(argv[2])
      } else if argv.length() == 2 {
        repository.switch_branch(argv[1])
      } else {
        raise VcsError::CheckoutFailed(
          "usage: moonvcs switch [--dry-run] ",
        )
      }
      println(@json.to_json(plan).stringify())
      if argv.length() == 2 {
        println("Switched to " + argv[1])
      }
    }
    "commit" => {
      let message = parse_commit_message(argv[1:])
      let author = match @env.get_env_var("MOONVCS_AUTHOR") {
        Some(value) => value
        None => "MoonVCS User"
      }
      let repository = Repository::at(
        find_repository_root(cwd),
        offline~,
        fetch_events=true,
      )
      let id = repository.commit(author, @env.now(), message)
      println("Policy passed")
      println("Commit created")
      println("Committed \{id.hex}")
    }
    "log" => {
      let mut max_count = 2147483647
      let mut index = 1
      while index < argv.length() {
        if argv[index] == "--offline" {
          offline = true
          index = index + 1
        } else if argv[index] == "--max-count" && index + 1 < argv.length() {
          let mut count = 0
          let text = argv[index + 1]
          if text.is_empty() || text.length() > 6 {
            raise VcsError::InvalidCommand("invalid log max-count")
          }
          for char in text {
            if char < '0' || char > '9' {
              raise VcsError::InvalidCommand("invalid log max-count")
            }
            count = count * 10 + (char.to_int() - 48)
          }
          max_count = count
          index = index + 2
        } else {
          raise VcsError::InvalidCommand(
            "log accepts --max-count N and --offline",
          )
        }
      }
      let repository = Repository::at(
        find_repository_root(cwd),
        offline~,
        fetch_events=true,
      )
      let commits = repository.log(max_count~)
      if commits.is_empty() {
        println("No commits")
      } else {
        for (id, commit) in commits {
          println("commit \{id.hex}")
          println("Author: " + safe_policy_text(commit.author))
          println("Date:   \{commit.timestamp}")
          println("")
          println("    " + safe_policy_text(commit.message))
          println("")
        }
      }
    }
    "show" => {
      if argv.length() == 3 && argv[2] == "--offline" {
        offline = true
      } else if argv.length() != 2 {
        raise VcsError::InvalidCommand("show requires one object id or HEAD")
      }
      let repository = Repository::at(
        find_repository_root(cwd),
        offline~,
        fetch_events=true,
      )
      let id = resolve_revision(repository, argv[1])
      print_object(id, repository.get_object(id))
    }
    "storage-stats" => {
      if argv.length() != 1 {
        raise VcsError::InvalidCommand("storage-stats takes no arguments")
      }
      let store = LazyObjectStore::new(find_repository_root(cwd), offline~)
      println(@json.to_json(store.transfer_stats()).stringify())
    }
    "storage-state" => {
      if argv.length() != 2 {
        raise VcsError::InvalidCommand("storage-state requires one ObjectId")
      }
      let store = LazyObjectStore::new(find_repository_root(cwd), offline~)
      let state = store.state(ObjectId::parse(argv[1]))
      println(
        match state {
          Local => "Local"
          PromisedRemote => "PromisedRemote"
          Unknown => "Unknown"
        },
      )
    }
    command =>
      raise VcsError::InvalidCommand(
        "unknown command: \{command}\n\n\{cli_usage()}",
      )
  }
}