#!/usr/bin/env bash
# core/framework/command.sh
#
# Command definition, routing, and lazy loading.
#
# Usage in on_load:
#   command::define show  "Show logs"          [*]
#   command::define clean "Remove keepalives"  [c, dump]
#   command::define watch "Watch live"         [w, follow]
#
# Routing:
#   wgctl logs          → cmd::logs::show::run   (default)
#   wgctl logs clean    → cmd::logs::clean::run
#   wgctl logs c        → cmd::logs::clean::run  (alias)
#   wgctl logs --fw     → cmd::logs::show::run   (default, flag passed through)

# ── Storage ───────────────────────────────────────────────────────────────────

# Command definitions: "cmd:subcmd" → "desc|aliases|is_default"
declare -gA _COMMAND_DEFS=()

# Alias map: "cmd:alias" → "subcmd"
declare -gA _COMMAND_ALIASES_MAP=()

# Default subcommand per command: "cmd" → "subcmd"
declare -gA _COMMAND_DEFAULT=()

# Currently loading command context (set by load_command)
declare -g _CURRENT_LOADING_CMD=""

# Currently executing command context (set by command::run)
declare -g _CURRENT_COMMAND=""

# ── Definition ────────────────────────────────────────────────────────────────

# command::define <subcmd> "description" [*] or [alias1, alias2]
# [*] marks as default subcommand
function command::define() {
  local subcmd="${1:-}"
  local desc="${2:-}"
  local meta="${3:-}"   # [*] or [alias1, alias2] or [*, alias1]
  local cmd="${_CURRENT_LOADING_CMD:-}"

  [[ -z "$cmd" || -z "$subcmd" ]] && return 1

  local is_default=false
  local aliases=""

  if [[ -n "$meta" ]]; then
    # Strip brackets
    local inner="${meta#[}"
    inner="${inner%]}"

    # Check for * (default marker)
    if [[ "$inner" == *"*"* ]]; then
      is_default=true
      inner="${inner//\*/}"
      inner="${inner//,/}"
      inner="${inner// /}"
    fi

    # Remaining are aliases
    aliases="$inner"
    # Clean up leading/trailing commas and spaces
    aliases=$(echo "$aliases" | sed 's/^[, ]*//' | sed 's/[, ]*$//' | tr -s ' ,')
  fi

  # Store definition: desc|aliases|is_default
  _COMMAND_DEFS["${cmd}:${subcmd}"]="${desc}|${aliases}|${is_default}"

  # Register as default
  if $is_default; then
    _COMMAND_DEFAULT["$cmd"]="$subcmd"
  fi

  # Register aliases
  if [[ -n "$aliases" ]]; then
    local alias
    # Split aliases on comma or space
    while IFS=',' read -ra alias_list; do
      for alias in "${alias_list[@]}"; do
        alias="${alias// /}"
        [[ -n "$alias" ]] && _COMMAND_ALIASES_MAP["${cmd}:${alias}"]="$subcmd"
      done
    done <<< "$aliases"
  fi
}

# ── Routing ───────────────────────────────────────────────────────────────────

# command::route <cmd> <args...>
# Determines which subcommand to run and routes to it.
# Returns: sets _ROUTED_SUBCMD and _ROUTED_ARGS
declare -g _ROUTED_SUBCMD=""
declare -ga _ROUTED_ARGS=()

function command::route() {
  local cmd="$1"
  shift
  local -a args=("$@")

  _ROUTED_SUBCMD=""
  _ROUTED_ARGS=()

  # Check if _COMMAND_DEFS has any entries for this cmd
  local has_defs=false
  local key
  for key in "${!_COMMAND_DEFS[@]}"; do
    [[ "$key" == "${cmd}:"* ]] && has_defs=true && break
  done

  if ! $has_defs; then
    # No subcommands defined — run directly
    _ROUTED_SUBCMD=""
    _ROUTED_ARGS=("${args[@]:-}")
    return 0
  fi

  # Find first non-flag arg
  local first_nonflag="" first_nonflag_idx=-1
  local i
  for (( i=0; i<${#args[@]}; i++ )); do
    if [[ "${args[$i]}" != --* && "${args[$i]}" != -* ]]; then
      first_nonflag="${args[$i]}"
      first_nonflag_idx=$i
      break
    fi
  done

  local matched_subcmd=""

  if [[ -n "$first_nonflag" ]]; then
    # Check direct match
    if [[ -n "${_COMMAND_DEFS[${cmd}:${first_nonflag}]+x}" ]]; then
      matched_subcmd="$first_nonflag"
    # Check alias
    elif [[ -n "${_COMMAND_ALIASES_MAP[${cmd}:${first_nonflag}]+x}" ]]; then
      matched_subcmd="${_COMMAND_ALIASES_MAP[${cmd}:${first_nonflag}]}"
    fi
  fi

  if [[ -n "$matched_subcmd" ]]; then
    # Remove matched subcmd from args
    local -a new_args=()
    for (( i=0; i<${#args[@]}; i++ )); do
      [[ $i -eq $first_nonflag_idx ]] && continue
      new_args+=("${args[$i]}")
    done
    _ROUTED_SUBCMD="$matched_subcmd"
    _ROUTED_ARGS=("${new_args[@]:-}")
  else
    # Use default subcommand
    local default_subcmd="${_COMMAND_DEFAULT[$cmd]:-}"
    _ROUTED_SUBCMD="$default_subcmd"
    _ROUTED_ARGS=("${args[@]:-}")
  fi
}

# ── Loading ───────────────────────────────────────────────────────────────────

# command::load_subcmd <cmd> <subcmd>
# Lazy-loads a subcommand's file and calls its on_load
function command::load_subcmd() {
  local cmd="$1" subcmd="$2"
  # log::debug "load_subcmd: cmd=$cmd subcmd=$subcmd"
  [[ -z "$cmd" || -z "$subcmd" ]] && return 1

  # Determine file path — commands/<cmd>/<subcmd>.sh
  local cmd_dir
  cmd_dir="${WGCTL_DIR}/commands/${cmd}"

  local subcmd_file="${cmd_dir}/${subcmd}.sh"
  # log::debug "looking for: $subcmd_file exists=$([ -f "$subcmd_file" ] && echo yes || echo no)"
  [[ ! -f "$subcmd_file" ]] && return 1

  local fn_subcmd="${subcmd//-/_}"
  local on_load_fn="cmd::${cmd}::${fn_subcmd}::on_load"

  _CURRENT_LOADING_CMD="${cmd}::${subcmd}"
  _CURRENT_COMMAND="${cmd}::${subcmd}"
  source "$subcmd_file"

  # log::debug "sourced: $subcmd_file exit=$?"
  # log::debug "on_load_fn=$on_load_fn exists=$(declare -f "$on_load_fn" &>/dev/null && echo yes || echo no)"

  # log::debug "load_subcmd: calling on_load, _CURRENT_COMMAND=$_CURRENT_COMMAND _CURRENT_LOADING_CMD=$_CURRENT_LOADING_CMD"
  if declare -f "$on_load_fn" &>/dev/null; then
    "$on_load_fn"
  fi

  _CURRENT_LOADING_CMD=""
}

function command::helpers() {
  local file="${1:-}"
  local cmd="${_CURRENT_LOADING_CMD:-}"
  [[ -z "$file" || -z "$cmd" ]] && return 1
  local cmd_name="${cmd%%::*}"
  local path="$(ctx::commands)/${cmd_name}/${file}"
  [[ -f "$path" ]] && source "$path"
}

# ── Run ───────────────────────────────────────────────────────────────────────

# command::run_routed <cmd> <subcmd> <args...>
# Runs a subcommand after routing is resolved.
function command::run_routed() {
  local cmd="$1" subcmd="$2"
  shift 2
  local -a args=("$@")

  _CURRENT_COMMAND="${cmd}::${subcmd}"

  # Apply command defaults (only for default subcommand)
  local default_subcmd="${_COMMAND_DEFAULT[$cmd]:-}"
  local -a default_args=()
  if [[ "$subcmd" == "$default_subcmd" || -z "$default_subcmd" ]]; then
    local defaults="${_COMMAND_DEFAULTS[$cmd]:-}"
    if [[ -n "$defaults" ]]; then
      read -ra default_args <<< "$defaults"
    fi
  fi

  local -a user_args=()
  [[ ${#args[@]} -gt 0 ]] && user_args=("${args[@]}")

  # Resolve exclusive group conflicts
  local groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}"
  if [[ -n "$groups" && ${#default_args[@]} -gt 0 && ${#user_args[@]} -gt 0 ]]; then
    command::_resolve_conflicts default_args user_args "$groups"
  fi

  # Combine args
  local -a final_args=()
  for _d in "${default_args[@]:-}"; do
    [[ -n "$_d" ]] && final_args+=("$_d")
  done
  for _u in "${user_args[@]:-}"; do
    [[ -n "$_u" ]] && final_args+=("$_u")
  done

  # Preprocess mixin flags (--json, --no-color etc)
  command::_preprocess_flags final_args

  # Run
  local fn_subcmd="${subcmd//-/_}"
  local run_fn="cmd::${cmd}::${fn_subcmd}::run"
  if declare -f "$run_fn" &>/dev/null; then
    if [[ ${#final_args[@]} -gt 0 ]]; then
      "$run_fn" "${final_args[@]}"
    else
      "$run_fn"
    fi
  else
    log::error "Run function not found: ${run_fn}"
    return 1
  fi
}

# ── Helpers ───────────────────────────────────────────────────────────────────

# command::has_subcmds <cmd>
# Returns 0 if command has defined subcommands
function command::has_subcmds() {
  local cmd="$1" key
  for key in "${!_COMMAND_DEFS[@]}"; do
    [[ "$key" == "${cmd}:"* ]] && return 0
  done
  return 1
}

# command::subcmds <cmd>
# Prints list of subcommand names
function command::subcmds() {
  local cmd="$1" key
  for key in "${!_COMMAND_DEFS[@]}"; do
    [[ "$key" == "${cmd}:"* ]] && echo "${key#${cmd}:}"
  done
}

# ============================================
# Command Registry
# ============================================

declare -A _LOADED_COMMANDS=()

readonly _COMMAND_NAMESPACE="cmd"
readonly _COMMAND_AUTO_LOAD_HOOK="on_load"
_CURRENT_LOADING_CMD=""

# ============================================
# Helpers
# ============================================

function command::loaded() { [[ -n "${_LOADED_COMMANDS["$1"]:-}" ]]; }

# Convert path-style name to namespace
# e.g. service/wireguard -> service::wireguard
function command::to_namespace() { echo "${1//\//:}"; }

# Build fully qualified function name
# e.g. command::fn "add" "run" -> cmd::add::run
# e.g. command::fn "service/wg" "run" -> cmd::service::wg::run
function command::fn() {
  local name namespace
  namespace=$(command::to_namespace "$1")
  echo "${_COMMAND_NAMESPACE}::${namespace}::${2}"
}

function command::has_function() { declare -F "$(command::fn "$1" "$2")" >/dev/null 2>&1; }
function command::is_auto_load() { declare -F "$(command::fn "$1" on_load)" >/dev/null 2>&1; }
function command::exists() {
  local name="$1"
  # New-style: has subcommands defined
  command::has_subcmds "$name" && return 0
  # Legacy: has cmd::name::run function
  declare -f "$(command::fn "$name" run)" &>/dev/null
}

# ============================================
# Runner
# ============================================

function command::run() {
  local cmd="$1"
  shift

  command::_reset_mixin_state

  # Check if this command uses new directory-based routing
  if command::has_subcmds "$cmd"; then
    # Route to subcommand
    command::route "$cmd" "$@"
    local subcmd="$_ROUTED_SUBCMD"
    local -a routed_args=("${_ROUTED_ARGS[@]:-}")

    if [[ -z "$subcmd" ]]; then
      hook::fire "command:help:${cmd}" "$cmd" ""
      return 0
    fi

    # log::debug "about to load_subcmd: cmd=$cmd subcmd=$subcmd"
    # Lazy load subcommand file
    command::load_subcmd "$cmd" "$subcmd"

    for arg in "$@"; do
      [[ "$arg" == "--help" || "$arg" == "-h" ]] && {
        hook::fire "command:help:${cmd}" "$cmd" "$_ROUTED_SUBCMD"
        return 0
      }
    done

    command::run_routed "$cmd" "$subcmd" "${routed_args[@]:-}"
    return $?
  fi

  # Legacy path — existing command::run logic
  local -a default_args=()
  local defaults="${_COMMAND_DEFAULTS[$cmd]:-}"
  if [[ -n "$defaults" ]]; then
    read -ra default_args <<< "$defaults"
  fi

  local -a user_args=()
  [[ $# -gt 0 ]] && user_args=("$@")

  local groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}"
  if [[ -n "$groups" && ${#default_args[@]} -gt 0 && ${#user_args[@]} -gt 0 ]]; then
    command::_resolve_conflicts default_args user_args "$groups"
  fi

  local -a args=()
  for _d in "${default_args[@]:-}"; do [[ -n "$_d" ]] && args+=("$_d"); done
  for _u in "${user_args[@]:-}"; do [[ -n "$_u" ]] && args+=("$_u"); done

  command::_preprocess_flags args

  local fn
  fn=$(command::fn "$cmd" run)
  if [[ ${#args[@]} -gt 0 ]]; then
    core::call_function "$fn" "${args[@]}"
  else
    core::call_function "$fn"
  fi
}
 

function core::call_function() {
  local fn="$1"
  shift
  "$fn" "$@"
}

# ============================================
# Loader
# ============================================

function load_command() {
  local name="$1"
  command::loaded "$name" && return 0

  # Check for new directory-based structure first
  local cmd_dir
  cmd_dir="$(ctx::commands)/${name}"
  local cmd_file="${cmd_dir}/${name}.sh"

  if [[ -d "$cmd_dir" && -f "$cmd_file" ]]; then
    source "$cmd_file"
    _LOADED_COMMANDS["$name"]=1
    _CURRENT_LOADING_CMD="$name"
    core::call_if_exists "cmd::${name}::on_load"
    _CURRENT_LOADING_CMD=""
    return 0
  fi

  # Fall back to legacy .command.sh
  local path
  path="$(ctx::commands)/${name}.command.sh"
  if [[ ! -f "$path" ]]; then
    log::error "Command not found: ${name} (${path})"
    return 1
  fi
  source "$path"
  _LOADED_COMMANDS["$name"]=1
  _CURRENT_LOADING_CMD="$name"
  core::call_if_exists "$(command::fn "$name" on_load)"
  _CURRENT_LOADING_CMD=""
  return 0
}
