#!/usr/bin/env bash
set -euo pipefail
source "$SHELLOPS_DIR/.envrc"
import \
  "$SHELLOPS_DIR/lib/system_packages.api.sh" \
  "$SHELLOPS_DIR/lib/shell_rc.api.sh"

: <<'DOC'
CLI for declarative tool installation.

Reads tool definitions from ops.lock, discovers script-channel
.installer.sh/.uninstaller.sh files, loads channel plugins,
validates parity, resolves dependency ordering, and runs
install/uninstall/check operations.

When running in a consumer project, $SHELLOPS_DIR/ops.lock is loaded
as a base layer before $PROJ/ops.lock. Consumer entries take precedence.

Channels are loaded from *.channel.sh plugin files discovered in
$SHELLOPS_DIR/plugins/channels and $PROJ/plugins/channels. Each plugin
self-registers by calling register_channel with flags.

The "script" channel (aliased as "local" for backwards compatibility)
runs .installer.sh/.uninstaller.sh files discovered from
$SHELLOPS_DIR/plugins/tools, then $PROJ/plugins/tools
(or TOOLS_SCRIPTS_DIR). Consumer scripts take precedence.

Lockfile format (ops.lock):
  {
    "<channel>": {
      "<tool-binary-name>": {
        "version": "<version>",
        "needs": ["<other-tool>"],   // optional
        "args": "<extra-args>",      // optional, channel-specific
        "crate": "<crate-name>",     // optional, cargo channels
        "pkg": "<go-module-path>"    // required for go-install
      }
    }
  }
DOC

__tools_api__lockfile="$PROJ/ops.lock"
__tools_api__scripts_dir="${TOOLS_SCRIPTS_DIR:-$PROJ/shell/scripts}"

: <<'DOC'
Registration function called by channel plugins during source.

Flags:
  --name NAME              Channel name (repeatable for aliases)
  --installer FN           Function name for install
  --uninstaller FN         Function name for uninstall
  --dependency TOOL        (optional) Tool this channel requires
  --dependency-default JSON (optional) Default registration for that tool
DOC
function register_channel() {
  local names=() install_fn="" uninstall_fn=""
  local channel_dependency="" channel_dependency_default=""
  local name

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --name)               names+=("$2"); shift ;;
      --installer)          install_fn="$2"; shift ;;
      --uninstaller)        uninstall_fn="$2"; shift ;;
      --dependency)         channel_dependency="$2"; shift ;;
      --dependency-default) channel_dependency_default="$2"; shift ;;
      *) error "register_channel: unknown flag '$1'"; return 1 ;;
    esac
    shift
  done

  if (( ${#names[@]} == 0 )); then
   error "register_channel: at least one --name is required"
   return 1
  fi

  if [[ -z "$install_fn" ]]; then
    error "register_channel: --installer is required for '${names[0]}'"
    return 1
  fi

  if [[ -z "$uninstall_fn" ]]; then
    error "register_channel: --uninstaller is required for '${names[0]}'"
    return 1
  fi

  local name
  for name in "${names[@]}"; do
    resolver="$(jq_set "$resolver" \
      ".channels.\"$name\".install_fn" "$install_fn" \
      ".channels.\"$name\".uninstall_fn" "$uninstall_fn"
    )"
  done

  [[ -z "$channel_dependency" ]] && return 0

  for name in "${names[@]}"; do
    resolver="$(jq_set "$resolver" \
      ".channels.\"$name\".dependency" "$channel_dependency"
    )"
  done

  [[ -z "$channel_dependency_default" ]] && return 0

  for name in "${names[@]}"; do
    resolver="$(jq_set "$resolver" \
      ".channels.\"$name\".dependency_default" "$channel_dependency_default"
    )"
  done
}

function main() {
  local resolver order targets
  local __tools_api__FLAG_CHECK=false
  local __tools_api__FLAG_FORCE=false
  local __tools_api__FLAG_UNINSTALL=false

  resolver="$(jq_obj \
    '.registry' '{}' \
    '.order'    '[]' \
    '.channels' '{}' \
    '.tools'    '[]'
  )"

  __tools_api__load_channels
  __tools_api__parse_manifest
  __tools_api__inject_channel_dependencies
  __tools_api__register_script_paths

  order=$(__tools_api__resolve_order "$(jq_get "$resolver" '.registry')")
  resolver=$(jq_set "$resolver" '.order' "$order")

  __tools_api__parse_args "$@"

  if [[ "$__tools_api__FLAG_UNINSTALL" == true ]] && [[ "$__tools_api__FLAG_CHECK" == true ]]; then
    error "--uninstall cannot be combined with --check"
    return 1
  fi

  targets=$(jq_get "$resolver" '.tools')

  if [[ "$__tools_api__FLAG_CHECK" == true ]]; then
    tools_check "$targets"
    return 0
  fi

  if [[ "$__tools_api__FLAG_UNINSTALL" == true ]]; then
    tools_uninstall "$targets"
    log; log "Done."
    return 0
  fi

  [[ "$__tools_api__FLAG_FORCE" == true ]] && tools_uninstall "$targets"
  tools_install "$targets"
  log; log "Done."
}

function tools_install() {
  local targets="$1"

  if (( $(jq_len "$targets") > 0 )); then
    targets=$(__tools_api__expand_deps "$targets")
  else
    targets=$(jq_get "$resolver" '.order')
  fi

  jq_foreach __tools_api__install_tool "$targets"
}

function tools_uninstall() {
  local targets="$1"

  if (( $(jq_len "$targets") == 0 )); then
    targets=$(jq_get "$resolver" '.order')
  fi

  jq_foreach __tools_api__uninstall_tool "$targets"
}

function __tools_api__install_tool() {
  __tools_api__dispatch_tool "$1" \
    install_fn \
    install \
    __tools_api__skip_installed \
  || return 1

  __tools_api__run_postinstall "$1"
}

function __tools_api__run_postinstall() {
  local tool_json="$1"
  local tool requirements postinstall origin_dir script_path

  tool=$(jq_unwrap "$tool_json")
  [[ -z "$tool" ]] && return 0

  requirements=$(jq_get "$resolver" ".registry.\"$tool\"")
  postinstall=$(jq_get "$requirements" '.postinstall' '')
  jq_defined "$postinstall" || return 0

  origin_dir=$(jq_get "$requirements" '.origin_dir' '')
  script_path="${origin_dir}/${postinstall}"

  if [[ ! -f "$script_path" ]]; then
    error "postinstall script not found: $script_path"
    return 1
  fi

  source "$script_path"
}

function __tools_api__uninstall_tool() {
  __tools_api__dispatch_tool "$1" \
    uninstall_fn \
    uninstall \
    __tools_api__skip_not_installed
}

function __tools_api__dispatch_tool() {
  local tool_json="$1"
  local fn_field="$2"
  local verb="$3"
  local should_skip="$4"
  local tool requirements channel fn_name
  local Verb="${verb^}"

  tool=$(jq_unwrap "$tool_json")
  [[ -z "$tool" ]] && return 0

  "$should_skip" "$tool" && return 0

  requirements=$(jq_get "$resolver" ".registry.\"$tool\"")
  channel=$(jq_get "$requirements" '.channel')
  log "  ${Verb}ing $tool (channel: $channel)..."

  fn_name=$(jq_get "$resolver" ".channels.\"$channel\".$fn_field" '')
  if [[ -z "$fn_name" ]]; then
    error "No $verb function registered for channel '$channel'"
    return 1
  fi

  if ! "$fn_name" "$tool" "$requirements"; then
    error "Failed to $verb $tool"
    return 1
  fi
}

function __tools_api__skip_installed() {
  local tool="$1"
  local channel

  if [[ "$__tools_api__FLAG_FORCE" == true ]]; then
    return 1
  fi

  channel=$(jq_get "$resolver" ".registry.\"$tool\".channel")
  if [[ "$channel" == "system" ]]; then
    if system_packages_exists "$tool"; then
      log "  $tool: already installed, skipping"
      return 0
    fi
    return 1
  fi

  if command -v "$tool" >/dev/null 2>&1; then
    log "  $tool: already installed, skipping"
    return 0
  fi
  return 1
}

function __tools_api__skip_not_installed() {
  local tool="$1"
  local channel

  channel=$(jq_get "$resolver" ".registry.\"$tool\".channel")
  if [[ "$channel" == "system" ]]; then
    if ! system_packages_exists "$tool"; then
      log "  $tool: not installed, skipping"
      return 0
    fi
    return 1
  fi

  if ! command -v "$tool" >/dev/null 2>&1; then
    log "  $tool: not installed, skipping"
    return 0
  fi
  return 1
}

function tools_check() {
  local targets="$1"
  local col_status=10
  local col_tool=22
  local col_version=14
  local tool marker version channel

  if (( $(jq_len "$targets") == 0 )); then
    targets=$(jq_get "$resolver" '.order')
  fi

  printf "%-${col_status}s %-${col_tool}s %-${col_version}s %s\n" \
    "Status" "Tool" "Version" "Channel" >&2
  printf '%s\n' "$(printf '%*s' 60 '' | tr ' ' '-')" >&2

  while IFS= read -r tool; do
    [[ -z "$tool" ]] && continue

    channel=$(jq_get "$resolver" ".registry.\"$tool\".channel")
    version=$(jq_get "$resolver" ".registry.\"$tool\".version")

    if command -v "$tool" >/dev/null 2>&1; then
      marker="  ✓"
    else
      marker="  ✗"
    fi

    printf "%-${col_status}s %-${col_tool}s %-${col_version}s %s\n" \
      "$marker" "$tool" "$version" "$channel" >&2
  done < <(jq_get "$targets" '.[]')
}

function __tools_api__parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -c|--check)     __tools_api__FLAG_CHECK=true ;;
      -f|--force)     __tools_api__FLAG_FORCE=true ;;
      -u|--uninstall) __tools_api__FLAG_UNINSTALL=true ;;
      -h|--help)      __tools_api__usage; exit 0 ;;
      -*)
        error "Unknown option: $1"
        __tools_api__usage
        exit 1
        ;;
      *)
        if ! jq_defined "$(jq_get "$resolver" ".registry.\"$1\"")"; then
          error "Unknown tool: $1"
          log "Registered tools: $(jq_get "$resolver" '.registry|keys|join(" ")')"
          exit 1
        fi
        resolver=$(jq_push "$resolver" '.tools' "\"$1\"")
        ;;
    esac
    shift
  done
}

function __tools_api__usage() {
  local tools
  tools=$(jq_get "$resolver" '.registry|keys|join(" ")')

  cat >&2 <<EOF
Tool installer powered by shellops.

Usage: ops install [flags...] [tools...]

Arguments:
  tools...          Install only the specified tools.
                    If omitted, all registered tools are installed.

Flags:
  -c, --check       Check install status without installing
  -f, --force       Force re-installation (uninstall first)
  -u, --uninstall   Uninstall specified tools
  -h, --help        Show this help text

Registered tools:
  $tools
EOF
}

function __tools_api__load_channels() {
  local shellops_channels="$SHELLOPS_DIR/plugins/channels"
  local consumer_channels
  consumer_channels="$(dirname "$__tools_api__scripts_dir")/channels"

  import_glob "$shellops_channels" '*.channel.sh'

  if [[ "$consumer_channels" != "$shellops_channels" ]]; then
    import_glob "$consumer_channels" '*.channel.sh'
  fi
}

function __tools_api__parse_manifest() {
  local shellops_lockfile="$SHELLOPS_DIR/ops.lock"
  local found=false

  if [[ "$SHELLOPS_DIR" != "$PROJ" && -f "$shellops_lockfile" ]]; then
    __tools_api__parse_lockfile "$shellops_lockfile"
    found=true
  fi

  if [[ -f "$__tools_api__lockfile" ]]; then
    __tools_api__parse_lockfile "$__tools_api__lockfile"
    found=true
  fi

  if [[ "$found" == false ]]; then
    error "No ops.lock found (checked $__tools_api__lockfile)"
    return 1
  fi
}

function __tools_api__parse_lockfile() {
  local path="$1"
  local lockfile channel origin_dir
  lockfile=$(<"$path")
  origin_dir="$(cd "$(dirname "$path")" && pwd)"

  if ! jq_defined "$lockfile"; then
    warn "Empty lockfile: $path"
    return 0
  fi

  while IFS= read -r channel; do
    [[ -n "$channel" ]] && __tools_api__parse_channel "$lockfile" "$channel" "$origin_dir"
  done < <(jq_get "$lockfile" '.|keys[]')
}

function __tools_api__parse_channel() {
  local lockfile="$1" channel="$2" origin_dir="$3"
  local channel_json tool tool_json
  channel_json=$(jq_get "$lockfile" ".\"$channel\"")

  while IFS= read -r tool; do
    [[ -z "$tool" ]] && continue

    tool_json=$(jq_get "$channel_json" ".\"$tool\"")
    tool_json=$(jq_set "$tool_json" '.channel' "$channel")
    tool_json=$(jq_set "$tool_json" '.origin_dir' "$origin_dir")
    resolver=$(jq_set "$resolver" ".registry.\"$tool\"" "$tool_json")
  done < <(jq_get "$channel_json" '.|keys[]')
}

function __tools_api__register_script_paths() {
  local installers='{}' uninstallers='{}'
  local shellops_scripts_dir="$SHELLOPS_DIR/plugins/tools"

  if [[ -d "$shellops_scripts_dir" ]]; then
    __tools_api__scan_scripts "$shellops_scripts_dir" installers uninstallers
  fi

  if [[ -d "$__tools_api__scripts_dir" && "$__tools_api__scripts_dir" != "$shellops_scripts_dir" ]]; then
    __tools_api__scan_scripts "$__tools_api__scripts_dir" installers uninstallers
  fi

  __tools_api__validate_script_channel "$installers" "$uninstallers" || return 1
  __tools_api__merge_script_paths "$installers" "installer_path"
  __tools_api__merge_script_paths "$uninstallers" "uninstaller_path"
}

function __tools_api__scan_scripts() {
  local dir="$1"
  local -n _installers=$2 _uninstallers=$3

  local file tool_name
  while IFS= read -r -d '' file; do
    tool_name="$(basename "$file" .installer.sh)"
    _installers=$(jq_set "$_installers" ".\"$tool_name\"" "$file")
  done < <(find "$dir" -name '*.installer.sh' -print0 2>/dev/null)

  while IFS= read -r -d '' file; do
    tool_name="$(basename "$file" .uninstaller.sh)"
    _uninstallers=$(jq_set "$_uninstallers" ".\"$tool_name\"" "$file")
  done < <(find "$dir" -name '*.uninstaller.sh' -print0 2>/dev/null)
}

function __tools_api__validate_script_channel() {
  local installers="$1" uninstallers="$2"
  local errors=0

  __tools_api__check_registry_has_scripts "$installers" "$uninstallers" errors
  __tools_api__check_script_pairs "$installers" "$uninstallers" "installer.sh" "uninstaller.sh" errors
  __tools_api__check_script_pairs "$uninstallers" "$installers" "uninstaller.sh" "installer.sh" errors

  (( errors > 0 )) && return 1
  return 0
}

function __tools_api__check_registry_has_scripts() {
  local installers="$1" uninstallers="$2"
  local -n _errors=$3
  local tool channel

  while IFS= read -r tool; do
    [[ -z "$tool" ]] && continue

    channel=$(jq_get "$resolver" ".registry.\"$tool\".channel")
    [[ "$channel" != "script" && "$channel" != "local" ]] && continue

    if ! jq_defined "$(jq_get "$installers" ".\"$tool\"")"; then
      error "script tool '$tool' is missing a .installer.sh in $__tools_api__scripts_dir"
      ((_errors++))
    fi

    if ! jq_defined "$(jq_get "$uninstallers" ".\"$tool\"")"; then
      error "script tool '$tool' is missing a .uninstaller.sh in $__tools_api__scripts_dir"
      ((_errors++))
    fi
  done < <(jq_get "$resolver" '.registry|keys[]')
}

function __tools_api__check_script_pairs() {
  local present="$1" counterpart="$2"
  local present_suffix="$3" counterpart_suffix="$4"
  local -n _pair_errors=$5
  local tool

  while IFS= read -r tool; do
    [[ -z "$tool" ]] && continue

    if ! jq_defined "$(jq_get "$counterpart" ".\"$tool\"")"; then
      error "Found ${tool}.${present_suffix} but no matching ${tool}.${counterpart_suffix}"
      ((_pair_errors++))
    fi
  done < <(jq_get "$present" '.|keys[]')
}

function __tools_api__merge_script_paths() {
  local scripts="$1" field="$2"
  local tool path

  while IFS= read -r tool; do
    if [[ -n "$tool" ]] && jq_defined "$(jq_get "$resolver" ".registry.\"$tool\"")"; then
      path=$(jq_get "$scripts" ".\"$tool\"")
      resolver=$(jq_set "$resolver" ".registry.\"$tool\".$field" "$path")
    fi
  done < <(jq_get "$scripts" '.|keys[]')
}

function __tools_api__inject_channel_dependencies() {
  local tool channel dependency_tool

  while IFS= read -r tool; do
    [[ -z "$tool" ]] && continue

    channel=$(jq_get "$resolver" ".registry.\"$tool\".channel")
    dependency_tool=$(jq_get "$resolver" ".channels.\"$channel\".dependency" '')
    [[ -z "$dependency_tool" ]] && continue

    __tools_api__ensure_channel_dependency "$dependency_tool"
    __tools_api__append_need "$tool" "$dependency_tool"
  done < <(jq_get "$resolver" '.registry|keys[]')
}

function __tools_api__ensure_channel_dependency() {
  local tool="$1"
  local default_json channel parent

  if ! jq_defined "$(jq_get "$resolver" ".registry.\"$tool\"")"; then
    default_json=$(__tools_api__find_dependency_default "$tool")
    if [[ -z "$default_json" ]]; then
      error "No channel dependency default defined for '$tool'"
      return 1
    fi
    resolver=$(jq_set "$resolver" ".registry.\"$tool\"" "$default_json")
  fi

  channel=$(jq_get "$resolver" ".registry.\"$tool\".channel")
  parent=$(jq_get "$resolver" ".channels.\"$channel\".dependency" '')
  if [[ -n "$parent" ]]; then
    __tools_api__ensure_channel_dependency "$parent"
    __tools_api__append_need "$tool" "$parent"
  fi
}

function __tools_api__find_dependency_default() {
  local tool="$1"
  jq -c --arg t "$tool" \
    '[.channels[] | select(.dependency == $t) | .dependency_default] | .[0] // empty' \
    <<< "$resolver"
}

function __tools_api__append_need() {
  local tool="$1"
  local need="$2"
  local meta needs

  meta=$(jq_get "$resolver" ".registry.\"$tool\"")
  needs=$(jq_get "$meta" '.needs' '[]')

  __tools_api__needs_contains "$needs" "$need" && return 0

  if ! jq_defined "$(jq_get "$meta" '.needs')"; then
    meta=$(jq_set "$meta" '.needs' '[]')
  fi

  meta=$(jq_push "$meta" '.needs' "$(jq_wrap "$need")")
  resolver=$(jq_set "$resolver" ".registry.\"$tool\"" "$meta")
}

function __tools_api__needs_contains() {
  local needs="$1"
  local need="$2"
  local needle

  needle=$(jq_push '[]' "$(jq_wrap "$need")")
  (( $(jq_len "$(jq_intersect "$needs" "$needle")") > 0 ))
}

function __tools_api__resolve_order() {
  local registry="$1"
  # shellcheck disable=SC2034
  local visited='{}' in_stack='{}' order='[]'
  local tool

  while IFS= read -r tool; do
    if [[ -n "$tool" ]] && ! jq_defined "$(jq_get "$visited" ".\"$tool\"")"; then
      __tools_api__topo_visit "$tool" registry visited in_stack order || return 1
    fi
  done < <(jq_get "$registry" '.|keys[]')

  echo "$order"
}

function __tools_api__topo_visit() {
  local tool="$1"
  local reg_var="$2" vis_var="$3" stack_var="$4" ord_var="$5"
  local -n _registry="$reg_var" _visited="$vis_var" _in_stack="$stack_var" _order="$ord_var"
  local needs_json dep

  if jq_defined "$(jq_get "$_in_stack" ".\"$tool\"")"; then
    error "Circular dependency detected involving '$tool'"
    return 1
  fi

  if jq_defined "$(jq_get "$_visited" ".\"$tool\"")"; then
    return 0
  fi

  _in_stack=$(jq_set "$_in_stack" ".\"$tool\"" "true")

  needs_json=$(jq_get "$_registry" ".\"$tool\".needs")

  if jq_defined "$needs_json"; then
    while IFS= read -r dep; do
      if ! jq_defined "$(jq_get "$_registry" ".\"$dep\"")"; then
        error "Tool '$tool' needs '$dep', which is not registered"
        return 1
      fi

      __tools_api__topo_visit "$dep" "$reg_var" "$vis_var" "$stack_var" "$ord_var" || return 1
    done < <(jq_get "$needs_json" '.[]')
  fi

  _in_stack=$(jq_set "$_in_stack" ".\"$tool\"" null)
  _visited=$(jq_set "$_visited" ".\"$tool\"" "true")
  _order=$(jq_push "$_order" "\"$tool\"")
}

function __tools_api__expand_deps() {
  local needed='{}'
  local queue="$1"
  local i=0
  local queue_len tool needs_json dep result order
  queue_len=$(jq_len "$queue")

  while (( i < queue_len )); do
    tool=$(jq_get "$queue" ".[$i]")
    ((i++))

    if jq_defined "$(jq_get "$needed" ".\"$tool\"")"; then
      continue
    fi

    needed=$(jq_set "$needed" ".\"$tool\"" "true")

    needs_json=$(jq_get "$resolver" ".registry.\"$tool\".needs")

    if jq_defined "$needs_json"; then
      while IFS= read -r dep; do
        if [[ -n "$dep" ]] && ! jq_defined "$(jq_get "$needed" ".\"$dep\"")"; then
          queue=$(jq_push "$queue" "\"$dep\"")
          queue_len=$(jq_len "$queue")
        fi
      done < <(jq_get "$needs_json" '.[]')
    fi
  done

  order=$(jq_get "$resolver" '.order')
  result='[]'
  while IFS= read -r tool; do
    if jq_defined "$(jq_get "$needed" ".\"$tool\"")"; then
      result=$(jq_push "$result" "\"$tool\"")
    fi
  done < <(jq_get "$order" '.[]')

  echo "$result"
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi
