// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
fn raise_help(text : String) -> Unit raise DisplayHelp {
  raise Message(text)
}

///|
fn raise_version(text : String) -> Unit raise DisplayVersion {
  raise Message(text)
}

///|
fn[T] raise_unknown_long(
  name : StringView,
  long_index : Map[String, Arg],
) -> T raise ArgParseError {
  let hint = suggest_long(name, long_index)
  raise UnknownArgument("--\{name}", hint)
}

///|
fn[T] raise_unknown_short(
  short : Char,
  short_index : Map[Char, Arg],
) -> T raise ArgParseError {
  let hint = suggest_short(short, short_index)
  raise UnknownArgument("-\{short}", hint)
}

///|
fn[T] raise_subcommand_conflict(name : String) -> T raise ArgParseError {
  raise InvalidArgument(
    "subcommand '\{name}' cannot be used with positional arguments",
  )
}

///|
fn help_context_command(
  cmd : Command,
  inherited_globals : Array[Arg],
  command_path : String,
) -> Command {
  let help_name = if command_path == "" { cmd.name } else { command_path }
  { ..cmd, args: inherited_globals + cmd.args, name: help_name }
}

///|
fn render_help_for_context(
  cmd : Command,
  inherited_globals : Array[Arg],
  command_path : String,
) -> String {
  let help_cmd = help_context_command(cmd, inherited_globals, command_path)
  render_help(help_cmd)
}

///|
fn raise_context_help(
  cmd : Command,
  inherited_globals : Array[Arg],
  command_path : String,
) -> Unit raise DisplayHelp {
  raise_help(render_help_for_context(cmd, inherited_globals, command_path))
}

///|
fn default_argv() -> ArrayView[String] {
  let args = @env.args()
  if args.length() > 1 {
    args[1:]
  } else {
    []
  }
}

///|
fn merge_global_defs(
  inherited_globals : Array[Arg],
  globals_here : Array[Arg],
) -> Array[Arg] {
  let merged = inherited_globals.copy()
  for global in globals_here {
    match merged.search_by(arg => arg.name == global.name) {
      Some(idx) => merged[idx] = global
      None => merged.push(global)
    }
  }
  merged
}

///|
fn env_resolution_args(
  inherited_globals : Array[Arg],
  args : Array[Arg],
) -> Array[Arg] {
  let merged_globals = merge_global_defs(
    inherited_globals,
    collect_globals(args),
  )
  let local_only = args.filter(arg => {
    !(arg.global && arg.info is (FlagInfo(_) | OptionInfo(_)))
  })
  merged_globals + local_only
}

///|
fn format_error_with_help(
  msg : String,
  cmd : Command,
  inherited_globals : Array[Arg],
  command_path : String,
) -> String {
  (
    $|\{msg}
    $|
    $|\{render_help_for_context(cmd, inherited_globals, command_path)}
  )
}

///|
fn arg_usage_token_for_group(arg : Arg) -> String {
  let base = match arg.info {
    OptionInfo(long~, short~, ..) | FlagInfo(long~, short~, ..) =>
      if long is Some(long) {
        "--\{long}"
      } else if short is Some(short) {
        "-\{short}"
      } else {
        arg.name
      }
    PositionalInfo(_) =>
      if arg.multiple {
        "<\{arg.name}...>"
      } else {
        "<\{arg.name}>"
      }
  }
  if arg.info is OptionInfo(_) {
    "\{base} <\{arg.name}>"
  } else {
    base
  }
}

///|
fn group_usage_expr(
  groups : Array[ArgGroup],
  args : Array[Arg],
  name : String,
) -> String? {
  for group in groups {
    if group.name != name {
      continue
    }
    let members = [
      for member_name in group.args if args.search_by(arg => {
        arg.name == member_name
      })
      is Some(idx) &&
      !args[idx].hidden => arg_usage_token_for_group(args[idx])
    ]
    if members.is_empty() {
      return None
    }
    let joined = members.join("|")
    return Some("<\{joined}>")
  }
  None
}

///|
fn missing_group_error_message(
  cmd : Command,
  inherited_globals : Array[Arg],
  command_path : String,
  group_name : String,
) -> String {
  let help_cmd = help_context_command(cmd, inherited_globals, command_path)
  if group_usage_expr(help_cmd.groups, help_cmd.args, group_name) is Some(expr) {
    (
      $|error: the following required arguments were not provided:
      $|  \{expr}
    )
  } else {
    MissingGroup(group_name).arg_parse_error_message()
  }
}

///|
fn arg_error_for_parse_failure(
  err : ArgParseError,
  cmd : Command,
  inherited_globals : Array[Arg],
  command_path : String,
) -> ArgError {
  let message = match err {
    MissingGroup(name) =>
      missing_group_error_message(cmd, inherited_globals, command_path, name)
    TooManyPositionals(value, _) => {
      let candidates = [ for sub in cmd.subcommands => sub.name ]
      if help_subcommand_enabled(cmd) {
        candidates.push("help")
      }
      if suggest_name(value, candidates) is Some(best) {
        (
          $|\{err.arg_parse_error_message()}
          $|
          $|  tip: a similar subcommand exists: '\{best}'
        )
      } else {
        err.arg_parse_error_message()
      }
    }
    _ => err.arg_parse_error_message()
  }
  Message(format_error_with_help(message, cmd, inherited_globals, command_path))
}

///|
fn parse_command(
  cmd : Command,
  argv : ArrayView[String],
  env : Map[String, String],
  inherited_globals : Array[Arg],
  inherited_version_long : Map[String, String],
  inherited_version_short : Map[Char, String],
  command_path : String,
  seed_matches? : Matches = new_matches_parse_state(),
) -> Matches raise {
  parse_command_impl(
    cmd, argv, env, inherited_globals, inherited_version_long, inherited_version_short,
    command_path, seed_matches,
  ) catch {
    UnknownArgument(arg, hint) =>
      raise arg_error_for_parse_failure(
        UnknownArgument(arg, hint),
        cmd,
        inherited_globals,
        command_path,
      )
    InvalidArgument(msg) =>
      raise arg_error_for_parse_failure(
        InvalidArgument(msg),
        cmd,
        inherited_globals,
        command_path,
      )
    MissingValue(name) =>
      raise arg_error_for_parse_failure(
        MissingValue(name),
        cmd,
        inherited_globals,
        command_path,
      )
    MissingRequired(name, by) =>
      raise arg_error_for_parse_failure(
        MissingRequired(name, by),
        cmd,
        inherited_globals,
        command_path,
      )
    TooFewValues(name, got, min) =>
      raise arg_error_for_parse_failure(
        TooFewValues(name, got, min),
        cmd,
        inherited_globals,
        command_path,
      )
    TooManyValues(name, got, max) =>
      raise arg_error_for_parse_failure(
        TooManyValues(name, got, max),
        cmd,
        inherited_globals,
        command_path,
      )
    TooManyPositionals(value, arg) =>
      raise arg_error_for_parse_failure(
        TooManyPositionals(value, arg),
        cmd,
        inherited_globals,
        command_path,
      )
    InvalidValue(msg) =>
      raise arg_error_for_parse_failure(
        InvalidValue(msg),
        cmd,
        inherited_globals,
        command_path,
      )
    MissingGroup(name) =>
      raise arg_error_for_parse_failure(
        MissingGroup(name),
        cmd,
        inherited_globals,
        command_path,
      )
    GroupConflict(name) =>
      raise arg_error_for_parse_failure(
        GroupConflict(name),
        cmd,
        inherited_globals,
        command_path,
      )
    Unsupported(msg) =>
      raise ArgError::Message(
        "error: command definition validation failed: \{msg}",
      )
    err => raise err
  }
}

///|
fn parse_command_impl(
  cmd : Command,
  argv : ArrayView[String],
  env : Map[String, String],
  inherited_globals : Array[Arg],
  inherited_version_long : Map[String, String],
  inherited_version_short : Map[Char, String],
  command_path : String,
  seed_matches : Matches,
) -> Matches raise {
  if cmd.build_error is Some(err) {
    raise err
  }
  let args = cmd.args
  let groups = cmd.groups
  let subcommands = cmd.subcommands
  if cmd.arg_required_else_help && argv.is_empty() {
    raise_context_help(cmd, inherited_globals, command_path)
  }
  let matches = seed_matches
  let globals_here = collect_globals(args)
  let child_globals = merge_global_defs(inherited_globals, globals_here)
  let child_version_long = inherited_version_long.copy()
  let child_version_short = inherited_version_short.copy()
  for global in globals_here {
    if global.info is FlagInfo(long~, short~, action=Version, ..) {
      if long is Some(name) {
        child_version_long[name] = command_version(cmd)
      }
      if short is Some(short) {
        child_version_short[short] = command_version(cmd)
      }
    }
  }
  let long_index = build_long_index(inherited_globals, args)
  let short_index = build_short_index(inherited_globals, args)
  let builtin_help_short = help_flag_enabled(cmd) &&
    short_index.get('h') is None
  let builtin_help_long = help_flag_enabled(cmd) &&
    long_index.get("help") is None
  let builtin_version_short = version_flag_enabled(cmd) &&
    short_index.get('V') is None
  let builtin_version_long = version_flag_enabled(cmd) &&
    long_index.get("version") is None
  let positionals = positional_args(args)
  let positional_values = []
  let mut i = 0
  let mut positional_arg_found = false
  let default_subcommand = match cmd.default_subcommand {
    Some(default_name) =>
      subcommands
      .iter()
      .find_first(sub => sub.name == default_name && !sub.hidden)
    None => None
  }
  fn dispatch_subcommand(
    sub : Command,
    rest : ArrayView[String],
  ) -> Matches raise _ {
    let sub_path = if command_path == "" {
      sub.name
    } else {
      "\{command_path} \{sub.name}"
    }
    let child_local_non_globals = collect_non_global_names(sub.args)
    let child_seed = seed_child_globals_from_parent(
      matches, child_globals, child_local_non_globals,
    )
    let sub_matches = parse_command(
      sub,
      rest,
      env,
      child_globals,
      child_version_long,
      child_version_short,
      sub_path,
      seed_matches=child_seed,
    )
    matches.parsed_subcommand = Some((sub.name, sub_matches))
    // Merge argv-provided globals from the subcommand parse into the parent
    // so globals work even when they appear after the subcommand name.
    merge_globals_from_child(
      matches, sub_matches, child_globals, child_local_non_globals,
    )
    let env_args = env_resolution_args(inherited_globals, args)
    let parent_matches = finalize_matches(
      cmd, args, groups, matches, positionals, positional_values, env_args, env,
    )
    validate_relationships(parent_matches, args)
    if parent_matches.parsed_subcommand is Some((sub_name, sub_m)) {
      // After parent parsing, copy the final globals into the subcommand.
      propagate_globals_to_child(
        parent_matches, sub_m, child_globals, child_local_non_globals,
      )
      parent_matches.parsed_subcommand = Some((sub_name, sub_m))
    }
    parent_matches
  }
  while i < argv.length() {
    let arg = argv[i]
    if arg == "--" {
      if default_subcommand is Some(sub) {
        return dispatch_subcommand(sub, argv[i:])
      }
      if i + 1 < argv.length() {
        positional_arg_found = true
      }
      for rest in argv[i + 1:] {
        positional_values.push(rest)
      }
      break
    }
    if builtin_help_short && arg == "-h" {
      raise_context_help(cmd, inherited_globals, command_path)
    }
    if builtin_help_long && arg == "--help" {
      raise_context_help(cmd, inherited_globals, command_path)
    }
    if builtin_version_short && arg == "-V" {
      raise_version(command_version(cmd))
    }
    if builtin_version_long && arg == "--version" {
      raise_version(command_version(cmd))
    }
    if should_parse_as_positional(
        arg, positionals, positional_values, long_index, short_index,
      ) {
      positional_values.push(arg)
      positional_arg_found = true
      i += 1
      continue
    }
    if arg.has_prefix("--") {
      let (name, inline) = split_long(arg)
      if builtin_help_long && name == "help" {
        if inline is Some(_) {
          raise ArgParseError::InvalidArgument(arg)
        }
        raise_context_help(cmd, inherited_globals, command_path)
      }
      if builtin_version_long && name == "version" {
        if inline is Some(_) {
          raise ArgParseError::InvalidArgument(arg)
        }
        raise_version(command_version(cmd))
      }
      match long_index.get_from_string(name) {
        None =>
          // Support `--no-` when the underlying flag is marked `negatable`.
          if name is [.. "no-", .. target] &&
            long_index.get_from_string(target)
            is Some({ info: FlagInfo(negatable=true, action~, ..), .. } as spec) {
            if inline is Some(_) {
              raise ArgParseError::InvalidArgument(arg)
            }
            let value = match action {
              SetFalse => true
              _ => false
            }
            if action is Count {
              matches.counts[spec.name] = 0
            }
            matches.flags[spec.name] = value
            matches.flag_sources[spec.name] = Argv
          } else if default_subcommand is Some(sub) {
            return dispatch_subcommand(sub, argv[i:])
          } else {
            raise_unknown_long(name, long_index)
          }
        Some(spec) =>
          if spec.info is (OptionInfo(_) | PositionalInfo(_)) {
            check_duplicate_set_occurrence(matches, spec)
            if inline is Some(v) {
              assign_value(matches, spec, v, Argv)
            } else {
              let can_take_next = i + 1 < argv.length() &&
                !should_stop_option_value(
                  argv[i + 1],
                  spec,
                  long_index,
                  short_index,
                )
              if can_take_next {
                i += 1
                assign_value(matches, spec, argv[i], Argv)
              } else {
                raise ArgParseError::MissingValue("--\{name}")
              }
            }
          } else {
            if inline is Some(_) {
              raise ArgParseError::InvalidArgument(arg)
            }
            match spec.info {
              FlagInfo(action=Help, ..) =>
                raise_context_help(cmd, inherited_globals, command_path)
              FlagInfo(action=Version, ..) =>
                raise_version(
                  version_text_for_long_action(
                    cmd, name, inherited_version_long,
                  ),
                )
              _ => apply_flag(matches, spec, Argv)
            }
          }
      }
      i += 1
      continue
    }
    if arg.has_prefix("-") && arg != "-" {
      // Parse short groups like `-abc` and short values like `-c3`.
      let chars = arg.iter()
      ignore(chars.next())
      let mut consumed_next = false
      while chars.next() is Some(short) {
        if short == 'h' && builtin_help_short {
          raise_context_help(cmd, inherited_globals, command_path)
        }
        if short == 'V' && builtin_version_short {
          raise_version(command_version(cmd))
        }
        let spec = match short_index.get(short) {
          Some(v) => v
          None =>
            if default_subcommand is Some(sub) {
              let remaining = Array::new(capacity=argv.length() - i)
              remaining.push("-\{short}\{String::from_iter(chars)}")
              for rest in argv[i + 1:] {
                remaining.push(rest)
              }
              return dispatch_subcommand(sub, remaining)
            } else {
              raise_unknown_short(short, short_index)
            }
        }
        if spec.info is (OptionInfo(_) | PositionalInfo(_)) {
          check_duplicate_set_occurrence(matches, spec)
          let rest = String::from_iter(chars)
          if rest != "" {
            let inline = match rest.strip_prefix("=") {
              Some(view) => view.to_owned()
              None => rest
            }
            assign_value(matches, spec, inline, Argv)
          } else {
            let can_take_next = i + 1 < argv.length() &&
              !should_stop_option_value(
                argv[i + 1],
                spec,
                long_index,
                short_index,
              )
            if can_take_next {
              consumed_next = true
              assign_value(matches, spec, argv[i + 1], Argv)
            } else {
              raise ArgParseError::MissingValue("-\{short}")
            }
          }
          break
        } else {
          match spec.info {
            FlagInfo(action=Help, ..) =>
              raise_context_help(cmd, inherited_globals, command_path)
            FlagInfo(action=Version, ..) =>
              raise_version(
                version_text_for_short_action(
                  cmd, short, inherited_version_short,
                ),
              )
            _ => apply_flag(matches, spec, Argv)
          }
        }
      }
      i += 1 + (if consumed_next { 1 } else { 0 })
      continue
    }
    if help_subcommand_enabled(cmd) && arg == "help" {
      if positional_arg_found {
        raise_subcommand_conflict("help")
      }
      let rest = argv[i + 1:]
      let (target, target_globals, target_path) = resolve_help_target(
        cmd, rest, builtin_help_short, builtin_help_long, inherited_globals, command_path,
      )
      let text = render_help_for_context(target, target_globals, target_path)
      raise_help(text)
    }
    if subcommands.iter().find_first(sub => sub.name == arg) is Some(sub) {
      if positional_arg_found {
        raise_subcommand_conflict(sub.name)
      }
      return dispatch_subcommand(sub, argv[i + 1:])
    }
    if default_subcommand is Some(sub) {
      return dispatch_subcommand(sub, argv[i:])
    }

    positional_values.push(arg)
    positional_arg_found = true
    i += 1
  }
  if default_subcommand is Some(sub) {
    return dispatch_subcommand(sub, [])
  }
  let env_args = env_resolution_args(inherited_globals, args)
  let final_matches = finalize_matches(
    cmd, args, groups, matches, positionals, positional_values, env_args, env,
  )
  validate_relationships(final_matches, args)
  final_matches
}

///|
fn finalize_matches(
  cmd : Command,
  args : Array[Arg],
  groups : Array[ArgGroup],
  matches : Matches,
  positionals : Array[Arg],
  positional_values : Array[String],
  env_args : Array[Arg],
  env : Map[String, String],
) -> Matches raise ArgParseError {
  assign_positionals(matches, positionals, positional_values)
  apply_env(matches, env_args, env)
  apply_defaults(matches, env_args)
  validate_values(args, matches)
  validate_groups(args, groups, matches)
  validate_command_policies(cmd, matches)
  matches
}

///|
fn help_subcommand_enabled(cmd : Command) -> Bool {
  !cmd.disable_help_subcommand && !cmd.subcommands.is_empty()
}

///|
fn help_flag_enabled(cmd : Command) -> Bool {
  !cmd.disable_help_flag
}

///|
fn version_flag_enabled(cmd : Command) -> Bool {
  !cmd.disable_version_flag && cmd.version is Some(_)
}

///|
fn command_version(cmd : Command) -> String {
  cmd.version.unwrap_or("")
}

///|
fn version_text_for_long_action(
  cmd : Command,
  long : StringView,
  inherited_version_long : Map[String, String],
) -> String {
  for arg in cmd.args {
    if arg.info is FlagInfo(long=Some(name), action=Version, ..) &&
      name[:] == long {
      return command_version(cmd)
    }
  }
  inherited_version_long.get_from_string(long).unwrap_or(command_version(cmd))
}

///|
fn version_text_for_short_action(
  cmd : Command,
  short : Char,
  inherited_version_short : Map[Char, String],
) -> String {
  for arg in cmd.args {
    if arg.info is FlagInfo(short=Some(value), action=Version, ..) &&
      value == short {
      return command_version(cmd)
    }
  }
  inherited_version_short.get(short).unwrap_or(command_version(cmd))
}