#!/usr/bin/env bash

import "$SHELLOPS_DIR/lib/xplat/api.sh"

: <<'DOC'
Cross-platform system package management API.

Detects the current distro and available package managers, then delegates
install/remove/update/exists/add_repo operations using a data-driven config.
Manager priority is determined by distro (native first, third-party last).

When installing, tries each available manager in priority order until one
has the package. Falls back to non-preferred managers with a warning and
interactive confirmation prompt.

Version pinning is attempted on managers that support it. When the pinned
version is unavailable, falls back to latest with a warning.

Third-party repo setup is supported for apt, dnf, and pacman via
system_packages_add_repo. The repo config is keyed by manager name so a
single ops.lock entry can declare per-manager repo setup. URL strings
support {distro}, {arch}, {version}, and {codename} placeholders.

Supported managers: apt, brew, dnf, pacman
Supported distros:  ubuntu, debian, fedora, rhel, centos, arch, manjaro, macos
DOC

# ── Manager config ───────────────────────────────────────────────────
#
# Each manager defines its commands via associative array keys in the
# format "manager~operation". Adding a new manager means adding rows
# here and an entry in the distro table below.

declare -gA __system_packages__config=(
  [apt~detect]="apt"
  [apt~exists]="apt-cache show"
  [apt~install]="sudo apt install -y"
  [apt~remove]="sudo apt remove -y"
  [apt~update]="sudo apt update"
  [apt~version_check]="apt-cache madison"
  [apt~version_format]="{package}={version}*"

  [brew~detect]="brew"
  [brew~exists]="brew info"
  [brew~install]="brew install"
  [brew~remove]="brew uninstall"
  [brew~update]="brew update"

  [dnf~detect]="dnf"
  [dnf~exists]="dnf info"
  [dnf~install]="sudo dnf install -y"
  [dnf~remove]="sudo dnf remove -y"
  [dnf~update]="sudo dnf check-update || true"
  [dnf~version_check]="dnf list available"
  [dnf~version_format]="{package}-{version}*"

  [pacman~detect]="pacman"
  [pacman~exists]="pacman -Si"
  [pacman~install]="sudo pacman -S --noconfirm"
  [pacman~remove]="sudo pacman -R --noconfirm"
  [pacman~update]="sudo pacman -Sy"

  [apt~add_repo]="__system_packages__add_repo_apt"
  [brew~add_repo]="__system_packages__add_repo_brew"
  [dnf~add_repo]="__system_packages__add_repo_dnf"
  [pacman~add_repo]="__system_packages__add_repo_pacman"
)

# ── Distro → manager priority ───────────────────────────────────────
#
# Space-separated list of managers in order of preference per distro.
# Native package manager first, third-party (brew) last.

declare -gA __system_packages__distros=(
  [ubuntu]="apt brew"
  [debian]="apt brew"
  [linuxmint]="apt brew"
  [pop]="apt brew"
  [fedora]="dnf brew"
  [rhel]="dnf brew"
  [centos]="dnf brew"
  [arch]="pacman brew"
  [manjaro]="pacman brew"
  [macos]="brew"
)

# ── State ────────────────────────────────────────────────────────────

__system_packages__available=()
__system_packages__distro=""
__system_packages__detected=false
__system_packages__indexes_refreshed=false
__system_packages__os_version=""
__system_packages__os_codename=""

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

function __system_packages__cfg() {
  local key="${1}~${2}"
  echo "${__system_packages__config[$key]:-}"
}

# ── Detection ────────────────────────────────────────────────────────

function __system_packages__detect_distro() {
  if [[ "$__system_packages__detected" == true ]]; then
    return 0
  fi

  __system_packages__distro="$(__system_packages__read_distro_id)"
  __system_packages__read_os_release_fields

  local priority="${__system_packages__distros[$__system_packages__distro]:-}"
  __system_packages__available=()

  if [[ -n "$priority" ]]; then
    # shellcheck disable=SC2086
    __system_packages__collect_available $priority
  fi

  # shellcheck disable=SC2086
  __system_packages__collect_remaining $priority

  if (( ${#__system_packages__available[@]} == 0 )); then
    error "No supported system package manager found"
    return 1
  fi

  __system_packages__detected=true
}

function __system_packages__read_distro_id() {
  local id

  if [[ "$(uname -s)" == "Darwin" ]]; then
    echo "macos"
    return
  fi

  if [[ ! -f /etc/os-release ]]; then
    error "No /etc/os-release defined"
    echo "unknown"
    return
  fi

  id=$(. /etc/os-release && echo "${ID:-}")
  if [[ -z "$id" ]]; then
    error "No release ID defined in /etc/os-release"
    echo "unknown"
    return
  fi

  echo "$id"
}

function __system_packages__read_os_release_fields() {
  [[ -f /etc/os-release ]] || return 0

  __system_packages__os_version="$(. /etc/os-release && echo "${VERSION_ID:-}")"
  __system_packages__os_codename="$(. /etc/os-release && echo "${VERSION_CODENAME:-}")"
}

function __system_packages__collect_available() {
  local manager
  for manager in "$@"; do
    local detect_cmd
    detect_cmd=$(__system_packages__cfg "$manager" "detect")
    if [[ -n "$detect_cmd" ]] && command -v "$detect_cmd" >/dev/null 2>&1; then
      __system_packages__available+=("$manager")
    fi
  done
}

function __system_packages__collect_remaining() {
  local skip_list=" $* "
  local key
  for key in "${!__system_packages__config[@]}"; do
    if [[ "$key" != *"~detect" ]]; then
      continue
    fi

    local manager="${key%%~detect}"
    if [[ "$skip_list" == *" $manager "* ]]; then
      continue
    fi

    local detect_cmd
    detect_cmd=$(__system_packages__cfg "$manager" "detect")
    if [[ -n "$detect_cmd" ]] && command -v "$detect_cmd" >/dev/null 2>&1; then
      __system_packages__available+=("$manager")
    fi
  done
}

# ── Index refresh ────────────────────────────────────────────────────

function __system_packages__refresh_indexes() {
  if [[ "$__system_packages__indexes_refreshed" == true ]]; then
    return 0
  fi

  local manager update_cmd
  for manager in "${__system_packages__available[@]}"; do
    update_cmd=$(__system_packages__cfg "$manager" "update")
    if [[ -n "$update_cmd" ]]; then
      $update_cmd >/dev/null 2>&1 || true
    fi
  done

  __system_packages__indexes_refreshed=true
}

# ── Manager operations ───────────────────────────────────────────────

function __system_packages__package_exists() {
  local manager="$1"
  local package="$2"
  local cmd

  cmd=$(__system_packages__cfg "$manager" "exists")
  if [[ -z "$cmd" ]]; then
    error "Manager $manager does not support package existence checks"
    return 1
  fi

  $cmd "$package" >/dev/null 2>&1
}

function __system_packages__run_install() {
  local manager="$1"
  local package="$2"
  local version="${3:-}"
  local install_cmd version_fmt spec

  install_cmd=$(__system_packages__cfg "$manager" "install")
  version_fmt=$(__system_packages__cfg "$manager" "version_format")

  if [[ -n "$version" && -z "$version_fmt" ]]; then
    warn "$manager does not support version pinning; installing latest $package"
    $install_cmd "$package"
    return
  fi

  if [[ -z "$version" || -z "$version_fmt" ]]; then
    $install_cmd "$package"
    return
  fi

  if ! __system_packages__version_available "$manager" "$package" "$version"; then
    warn "version $version not available for $package via $manager; installing latest"
    $install_cmd "$package"
    return
  fi

  spec="${version_fmt//\{package\}/$package}"
  spec="${spec//\{version\}/$version}"
  $install_cmd "$spec"
}

function __system_packages__version_available() {
  local manager="$1"
  local package="$2"
  local version="$3"

  local check_cmd
  check_cmd=$(__system_packages__cfg "$manager" "version_check")
  if [[ -z "$check_cmd" ]]; then
    return 1
  fi

  $check_cmd "$package" 2>/dev/null | grep -q "$version"
}

# ── Fallback resolution ─────────────────────────────────────────────

function __system_packages__find_manager_for() {
  local package="$1"
  local primary="${__system_packages__available[0]}"
  local manager

  for manager in "${__system_packages__available[@]}"; do
    if ! __system_packages__package_exists "$manager" "$package"; then
      continue
    fi

    if [[ "$manager" != "$primary" ]]; then
      warn "$package not found in $primary; found in $manager"
      __system_packages__confirm_fallback "$package" "$manager" || return 1
    fi

    echo "$manager"
    return 0
  done

  error "$package not found in any available package manager (checked: ${__system_packages__available[*]})"
  return 1
}

function __system_packages__confirm_fallback() {
  local package="$1"
  local manager="$2"

  if [[ ! -t 0 ]]; then
    error "Non-interactive shell; cannot confirm fallback to $manager for $package"
    return 1
  fi

  local reply
  read -r -p "Install $package via $manager instead? [y/N] " reply
  [[ "$reply" =~ ^[Yy]$ ]]
}

# ── Placeholder expansion ────────────────────────────────────────

function __system_packages__expand_placeholders() {
  local str="$1"

  str="${str//\{distro\}/$__system_packages__distro}"
  str="${str//\{arch\}/$(xplat_arch)}"
  str="${str//\{version\}/$__system_packages__os_version}"
  str="${str//\{codename\}/$__system_packages__os_codename}"
  echo "$str"
}

# ── Repo setup: apt ─────────────────────────────────────────────

function __system_packages__add_repo_apt() {
  local name="$1" config="$2"
  local url gpg component codename arch
  local keyring="/etc/apt/keyrings/${name}.gpg"
  local sources="/etc/apt/sources.list.d/${name}.list"

  [[ -f "$sources" ]] && return 0

  url=$(__system_packages__expand_placeholders "$(jq_get "$config" '.url')")
  gpg=$(__system_packages__expand_placeholders "$(jq_get "$config" '.gpg')")
  component=$(jq_get "$config" '.component')
  codename=$(jq_get "$config" '.codename' '')

  if ! jq_defined "$codename"; then
    codename="$__system_packages__os_codename"
  fi

  arch="$(xplat_arch)"

  sudo mkdir -p /etc/apt/keyrings
  curl -fsSL "$gpg" | sudo gpg --dearmor -o "$keyring"

  echo "deb [arch=$arch signed-by=$keyring] $url $codename $component" \
    | sudo tee "$sources" > /dev/null

  __system_packages__indexes_refreshed=false
}

# ── Repo setup: brew ─────────────────────────────────────────────

function __system_packages__add_repo_brew() {
  local name="$1" config="$2"
  local tap

  tap=$(jq_get "$config" '.tap')
  jq_defined "$tap" || return 0

  brew tap "$tap" 2>/dev/null || true
}

# ── Repo setup: dnf ─────────────────────────────────────────────

function __system_packages__add_repo_dnf() {
  local name="$1" config="$2"
  local repo_file

  repo_file=$(jq_get "$config" '.repo_file' '')
  if jq_defined "$repo_file"; then
    __system_packages__add_repo_dnf_from_file "$name" "$repo_file"
    return
  fi

  __system_packages__add_repo_dnf_manual "$name" "$config"
}

function __system_packages__add_repo_dnf_from_file() {
  local name="$1" repo_file="$2"
  local dest="/etc/yum.repos.d/${name}.repo"

  [[ -f "$dest" ]] && return 0

  repo_file=$(__system_packages__expand_placeholders "$repo_file")
  sudo dnf config-manager addrepo --from-repofile="$repo_file" 2>/dev/null \
    || sudo dnf config-manager --add-repo "$repo_file"
}

function __system_packages__add_repo_dnf_manual() {
  local name="$1" config="$2"
  local dest="/etc/yum.repos.d/${name}.repo"

  [[ -f "$dest" ]] && return 0

  local url gpg repo_name
  url=$(__system_packages__expand_placeholders "$(jq_get "$config" '.url')")
  gpg=$(__system_packages__expand_placeholders "$(jq_get "$config" '.gpg')")
  repo_name=$(jq_get "$config" '.name' "$name")

  sudo tee "$dest" > /dev/null <<EOF
[$name]
name=$repo_name
baseurl=$url
gpgcheck=1
gpgkey=$gpg
enabled=1
EOF

  sudo rpm --import "$gpg"
}

# ── Repo setup: pacman ──────────────────────────────────────────

function __system_packages__add_repo_pacman() {
  local name="$1" config="$2"
  local server siglevel key

  if grep -q "^\[$name\]" /etc/pacman.conf 2>/dev/null; then
    return 0
  fi

  server=$(__system_packages__expand_placeholders "$(jq_get "$config" '.server')")
  siglevel=$(jq_get "$config" '.siglevel' 'Optional TrustedOnly')
  key=$(jq_get "$config" '.key' '')

  if jq_defined "$key"; then
    sudo pacman-key --recv-keys "$key"
    sudo pacman-key --lsign-key "$key"
  fi

  sudo tee -a /etc/pacman.conf > /dev/null <<EOF

[$name]
SigLevel = $siglevel
Server = $server
EOF

  __system_packages__indexes_refreshed=false
}

# ── Public API ───────────────────────────────────────────────────────

function __system_packages__normalize_build_tool() {
  local package="$1"
  local dist
  dist="$(__system_packages__read_distro_id)"
  if [[ "$package" != "build-essential" ]]; then
    echo "$package"
    return 0
  fi
  case "$dist" in
    ubuntu|debian|linuxmint|pop) echo build-essential ;;
    *) echo gcc ;;
  esac
}

function system_packages_exists() {
  local package="$1"
  local manager
  local normalized

  if [[ "$package" == "build-essential" ]] && command -v cc >/dev/null 2>&1; then
    return 0
  fi

  __system_packages__detect_distro || return 1
  normalized=$(__system_packages__normalize_build_tool "$package")

  for manager in "${__system_packages__available[@]}"; do
    if ! __system_packages__package_exists "$manager" "$normalized"; then
      continue
    fi

    return 0
  done

  return 1
}

function system_packages_install() {
  local package="$1"
  local version="${2:-}"
  local manager

  if [[ "$package" == "build-essential" ]] && command -v cc >/dev/null 2>&1; then
    return 0
  fi

  __system_packages__detect_distro || return 1
  package=$(__system_packages__normalize_build_tool "$package")
  __system_packages__refresh_indexes

  manager=$(__system_packages__find_manager_for "$package") || return 1

  __system_packages__run_install "$manager" "$package" "$version"
}

function system_packages_remove() {
  local package="$1"
  local primary
  local remove_cmd

  __system_packages__detect_distro || return 1

  package=$(__system_packages__normalize_build_tool "$package")

  primary="${__system_packages__available[0]}"
  remove_cmd=$(__system_packages__cfg "$primary" "remove")

  $remove_cmd "$package"
}

function system_packages_update() {
  __system_packages__detect_distro || return 1

  local primary="${__system_packages__available[0]}"
  local update_cmd
  update_cmd=$(__system_packages__cfg "$primary" "update")

  $update_cmd
}

: <<'DOC'
Configures a third-party package repository for the active manager.

The repo_json argument is a JSON object keyed by manager name (apt, dnf,
pacman). Only the entry matching the detected manager is used; others are
ignored. If the detected manager has no entry, this is a no-op.

Usage: system_packages_add_repo "$name" "$repo_json"
DOC
function system_packages_add_repo() {
  local name="$1" repo_json="$2"

  __system_packages__detect_distro || return 1

  local manager config handler
  manager="${__system_packages__available[0]}"
  config=$(jq_get "$repo_json" ".\"$manager\"")
  jq_defined "$config" || return 0

  handler=$(__system_packages__cfg "$manager" "add_repo")
  [[ -n "$handler" ]] || return 0

  "$handler" "$name" "$config"
}

: <<'DOC'
Returns the detected distro ID (e.g. ubuntu, fedora, macos).

Usage: system_packages_distro
DOC
function system_packages_distro() {
  __system_packages__detect_distro || return 1
  echo "$__system_packages__distro"
}
