///|
priv struct LnOptions {
  force : Bool
  interactive : Bool
  no_target_directory : Bool
  no_dereference_target : Bool
  relative : Bool
  verbose : Bool
  backup : @fsops.BackupControl
  suffix : String
}

///|
fn normalized_args(
  args : ArrayView[String],
) -> Array[String] raise @cli.CliError {
  let result : Array[String] = []
  let mut options = true
  let mut value_next = false
  for arg in args {
    if value_next {
      result.push(arg)
      value_next = false
      continue
    }
    if options &&
      (arg == "--backup-control" || arg.has_prefix("--backup-control=")) {
      raise @cli.CliError(
        kind=UnknownOption,
        option=arg,
        message="unknown option",
      )
    }
    if options && arg == "--" {
      options = false
      result.push(arg)
    } else if options && arg == "--backup" {
      result.push("--backup-control=existing")
    } else if options && arg.has_prefix("--backup=") {
      result.push("--backup-control=" + arg[9:].to_owned())
    } else {
      result.push(arg)
    }
    if options {
      value_next = ["--suffix", "--target-directory"].contains(arg)
      if arg.has_prefix("-") && !arg.has_prefix("--") {
        for offset in 1.. String {
  let target_path = @path.Path(target).resolve()
  let link_dir = @path.Path(@fsops.dirname(link_path)).resolve()
  target_path.relative(base=link_dir).to_string()
}

///|
async fn confirm_overwrite(path : String) -> Bool {
  @stdio.stderr.write("ln: replace '\{path}'? ")
  match @stdio.stdin.read_until("\n") {
    Some(answer) => answer.has_prefix("y") || answer.has_prefix("Y")
    None => false
  }
}

///|
async fn symlink_entry(link_path : String, target : String) -> Unit {
  @fs.symlink(link_path, target~)
}

///|
// Rollback stays cancellable because cancellation may follow a committed link.
#warnings("-cancellable_defer")
async fn create_symlink(
  link_path : String,
  target : String,
  backup_path : String?,
  create~ : async (String, String) -> Unit = symlink_entry,
  restore~ : async (String, String) -> Unit = @fsops.restore_backup,
) -> Unit {
  let mut failure_message = "link creation did not complete"
  errdefer (if backup_path is Some(backup) {
    restore(link_path, backup) catch {
      recovery => {
        let recovery_message = match recovery {
          @fsops.FsOpError(message) => message
          _ => recovery.to_string()
        }
        raise @fsops.FsOpError(
          "\{failure_message}; cannot restore '\{link_path}' from backup '\{backup}': \{recovery_message}",
        )
      }
    }
  })
  let created = try {
    create(link_path, target)
    Ok(())
  } catch {
    primary => Err(primary)
  }
  match created {
    Ok(_) => ()
    Err(primary) => {
      failure_message = match primary {
        @fsops.FsOpError(message) => message
        _ => primary.to_string()
      }
      raise primary
    }
  }
}

///|
async fn create_link(
  target : String,
  link_path : String,
  options : LnOptions,
) -> Bool {
  let existing = @fsops.kind_if_exists(link_path)
  if existing == Some(Directory) {
    raise @fsops.FsOpError("refusing to replace directory: '\{link_path}'")
  }
  let mut backup_path : String? = None
  if existing is Some(_) {
    if options.interactive {
      if !confirm_overwrite(link_path) {
        return false
      }
    }
    let backup = @fsops.backup_destination(
      link_path,
      options.backup,
      suffix=options.suffix,
    )
    backup_path = backup
    if backup is None && !options.force && !options.interactive {
      raise @fsops.FsOpError("destination exists: '\{link_path}'")
    }
    if backup is None {
      @fsops.remove_path(link_path, false)
    }
  }
  let actual_target = if options.relative {
    relative_target(target, link_path)
  } else {
    target
  }
  create_symlink(link_path, actual_target, backup_path)
  if options.verbose {
    @stdio.stdout.write("'\{link_path}' -> '\{actual_target}'\n")
  }
  true
}

///|
async fn main {
  let metadata_args = @env.args()[1:]
  if metadata_args.length() == 1 && metadata_args[0] == "--version" {
    @stdio.stdout.write("ln 0.2.6\n")
    return
  }
  let parsed = @cli.parse(normalized_args(@env.args()[1:]), [
    @cli.flag("symbolic", short='s'),
    @cli.flag("force", short='f'),
    @cli.flag("interactive", short='i'),
    @cli.flag("relative", short='r'),
    @cli.flag("no-dereference", short='n'),
    @cli.flag("no-target-directory", short='T'),
    @cli.option("target-directory", short='t'),
    @cli.flag("backup", short='b'),
    @cli.option("backup-control"),
    @cli.option("suffix", short='S'),
    @cli.flag("verbose", short='v'),
    @cli.flag("help"),
  ]) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("ln: \{message}: '\{option}'\n")
      @sys.exit(1)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write(
      "Usage: ln -s [-f|-i] [-r] [-n] [-T] [-b|--backup[=CONTROL]] [-S SUFFIX] TARGET... LINK_NAME\n       ln -s [-f|-i] [-r] [-n] [-b|--backup[=CONTROL]] [-S SUFFIX] -t DIRECTORY TARGET...\n      --help      display this help and exit\n      --version   display version and exit\nThe last -f/--force or -i/--interactive selects overwrite behavior.\nDeclining an interactive overwrite returns status 1 without replacing the link.\nOrdinary option/operand errors exit 1; documented capability refusals retain their own status.\nOption values and operands after -- remain literal during alias normalization.\nNumbered backups advance after the highest valid positive decimal version.\nFailed backup recovery reports both errors and destination/backup paths.\n",
    )
    return
  }
  if !parsed.contains("symbolic") {
    @stdio.stderr.write(
      "ln: hard links are unavailable in the portable filesystem API; use -s\n",
    )
    @sys.exit(1)
    return
  }
  if !@platform.symbolic_link_creation_supported() {
    @stdio.stderr.write("ln: unsupported capability: symbolic links\n")
    @sys.exit(1)
    return
  }
  let operands = parsed.operands
  let target_directory = parsed.last_value("target-directory")
  let backup_text = match parsed.last_occurrence(["backup", "backup-control"]) {
    Some("backup") => "existing"
    Some("backup-control") => parsed.last_value("backup-control").unwrap()
    _ => "none"
  }
  let options : LnOptions = {
    force: parsed.last_occurrence(["force", "interactive"]) == Some("force"),
    interactive: parsed.last_occurrence(["force", "interactive"]) ==
    Some("interactive"),
    no_target_directory: parsed.contains("no-target-directory"),
    no_dereference_target: parsed.contains("no-dereference"),
    relative: parsed.contains("relative"),
    verbose: parsed.contains("verbose"),
    backup: @fsops.parse_backup_control(backup_text) catch {
      @fsops.FsOpError(message) => {
        @stdio.stderr.write("ln: \{message}\n")
        @sys.exit(1)
        return
      }
    },
    suffix: parsed.last_value("suffix").unwrap_or("~"),
  }
  let (targets, destination) = match target_directory {
    Some(directory) => {
      if operands.is_empty() {
        @stdio.stderr.write("ln: missing operand\n")
        @sys.exit(1)
        return
      }
      if options.no_target_directory {
        @stdio.stderr.write("ln: -t cannot be combined with -T\n")
        @sys.exit(1)
        return
      }
      (operands, Some(directory))
    }
    None => {
      if operands.length() < 2 {
        @stdio.stderr.write("ln: missing operand\n")
        @sys.exit(1)
        return
      }
      (
        operands[0:operands.length() - 1].to_owned(),
        Some(operands[operands.length() - 1]),
      )
    }
  }
  guard destination is Some(destination) else { return }
  let destination_lstat = @fsops.kind_if_exists(destination) catch {
    err => {
      @stdio.stderr.write("ln: cannot inspect '\{destination}': \{err}\n")
      @sys.exit(1)
      return
    }
  }
  let destination_is_directory = if options.no_target_directory {
    false
  } else if options.no_dereference_target && destination_lstat == Some(SymLink) {
    false
  } else {
    (@fs.kind(destination) catch { _ => Unknown }) == Directory
  }
  if target_directory is Some(_) && !destination_is_directory {
    @stdio.stderr.write(
      "ln: target directory is not a directory: '\{destination}'\n",
    )
    @sys.exit(1)
    return
  }
  if target_directory is None &&
    targets.length() > 1 &&
    !destination_is_directory {
    @stdio.stderr.write("ln: target is not a directory: '\{destination}'\n")
    @sys.exit(1)
    return
  }
  let mut failed = false
  for target in targets {
    let link_path = if destination_is_directory {
      @fsops.join(destination, @fsops.basename(target))
    } else {
      destination
    }
    let created = create_link(target, link_path, options) catch {
      @fsops.FsOpError(message) => {
        @stdio.stderr.write("ln: failed to create '\{link_path}': \{message}\n")
        failed = true
        continue
      }
      err => {
        @stdio.stderr.write("ln: failed to create '\{link_path}': \{err}\n")
        failed = true
        continue
      }
    }
    if !created {
      failed = true
    }
  }
  if failed {
    @sys.exit(1)
  }
}