#!/usr/bin/env bash
set -euo pipefail

VERSION="0.6.0"
MANIFEST_FILE=".skillpull.json"
TMPDIR_PREFIX="skillpull"
CONFIG_DIR="$HOME/.config/skillpull"
CONFIG_FILE="$CONFIG_DIR/config.json"
SHARED_HUB="$HOME/.agents/skills"

# ── Agent targets (bash 3 compatible) ──
agent_project() {
  case "$1" in
    claude) echo ".claude/skills" ;;
    codex)  echo ".codex/skills" ;;
    kiro)   echo ".kiro/skills" ;;
    cursor) echo ".cursor/rules" ;;
  esac
}

agent_global() {
  case "$1" in
    claude) echo "$HOME/.claude/skills" ;;
    codex)  echo "$HOME/.codex/skills" ;;
    kiro)   echo "$HOME/.kiro/skills" ;;
    cursor) echo "" ;;
  esac
}

agent_format() {
  case "$1" in
    claude) echo "skill" ;;
    codex)  echo "skill" ;;
    kiro)   echo "skill" ;;
    cursor) echo "mdc" ;;
  esac
}

DEFAULT_AGENT="claude"

# ── Colors ──
RED='\033[1;31m'; GREEN='\033[1;32m'; YELLOW='\033[1;33m'
CYAN='\033[1;36m'; DIM='\033[2m'; RESET='\033[0m'

info()  { [[ "${QUIET:-0}" == "1" ]] || printf "  ${GREEN}✓${RESET} %s\n" "$*"; }
warn()  { printf "  ${YELLOW}!${RESET} %s\n" "$*" >&2; }
err()   { printf "  ${RED}✗${RESET} %s\n" "$*" >&2; }
dim()   { [[ "${QUIET:-0}" == "1" ]] || printf "  ${DIM}%s${RESET}\n" "$*"; }

# ── Cross-platform sed -i ──
sedi() {
  if [[ "$(uname)" == "Darwin" ]]; then
    sed -i '' "$@"
  else
    sed -i "$@"
  fi
}

# ── Interactive multi-select menu (bash 3 compatible) ──
# Usage: select_menu "prompt" item1 item2 item3 ...
# Sets SELECTED_ITEMS=("item1" "item3") with user's choices
SELECTED_ITEMS=()

_read_key() {
  local old_settings; old_settings="$(stty -g 2>/dev/null)" || true
  stty -icanon -echo min 1 time 0 2>/dev/null || true
  local char; char="$(dd bs=1 count=1 2>/dev/null)"
  # Check for escape sequence
  if [[ "$char" == $'\033' ]]; then
    stty -icanon -echo min 0 time 1 2>/dev/null || true
    local seq; seq="$(dd bs=2 count=1 2>/dev/null)"
    char="${char}${seq}"
  fi
  [[ -n "$old_settings" ]] && stty "$old_settings" 2>/dev/null || true
  printf '%s' "$char"
}

select_menu() {
  local prompt="$1"; shift
  local items=("$@")
  local count=${#items[@]}
  local cursor=0
  local i

  # Track selected state with a simple string: "0" = unselected, "1" = selected
  local selected=""
  for ((i=0; i<count; i++)); do
    selected="${selected}0"
  done

  SELECTED_ITEMS=()

  # Hide cursor
  printf '\033[?25l' >&2

  # Print prompt
  printf "\n  ${CYAN}%s${RESET}\n" "$prompt" >&2
  printf "  ${DIM}j/k move  Space select  Enter confirm${RESET}\n\n" >&2

  # Draw initial list
  for ((i=0; i<count; i++)); do
    local marker="[ ]"
    if [[ "${selected:$i:1}" == "1" ]]; then marker="[x]"; fi
    if [[ $i -eq $cursor ]]; then
      printf "  ${CYAN}> %s %s${RESET}\n" "$marker" "${items[$i]}" >&2
    else
      printf "    %s %s\n" "$marker" "${items[$i]}" >&2
    fi
  done

  # Input loop
  while true; do
    local key; key="$(_read_key)"

    case "$key" in
      $'\033[A'|k) # Up
        [[ $cursor -gt 0 ]] && cursor=$((cursor - 1))
        ;;
      $'\033[B'|j) # Down
        [[ $cursor -lt $((count - 1)) ]] && cursor=$((cursor + 1))
        ;;
      ' ') # Space - toggle selection
        if [[ "${selected:$cursor:1}" == "0" ]]; then
          selected="${selected:0:$cursor}1${selected:$((cursor+1))}"
        else
          selected="${selected:0:$cursor}0${selected:$((cursor+1))}"
        fi
        ;;
      ''|$'\n'|$'\r') # Enter - confirm
        break
        ;;
    esac

    # Redraw: move cursor up by $count lines
    printf '\033[%dA' "$count" >&2

    for ((i=0; i<count; i++)); do
      printf '\033[2K' >&2  # Clear line
      local marker="[ ]"
      if [[ "${selected:$i:1}" == "1" ]]; then marker="[x]"; fi
      if [[ $i -eq $cursor ]]; then
        printf "  ${CYAN}> %s %s${RESET}\n" "$marker" "${items[$i]}" >&2
      else
        printf "    %s %s\n" "$marker" "${items[$i]}" >&2
      fi
    done
  done

  # Show cursor
  printf '\033[?25h' >&2

  # Collect selected items
  for ((i=0; i<count; i++)); do
    if [[ "${selected:$i:1}" == "1" ]]; then
      SELECTED_ITEMS+=("${items[$i]}")
    fi
  done
}

# ── Cleanup ──
_TMPDIR=""
cleanup() { [[ -n "${_TMPDIR:-}" && -d "${_TMPDIR:-}" ]] && rm -rf "$_TMPDIR" || true; }
trap cleanup EXIT

make_tmp() { _TMPDIR="$(mktemp -d "/tmp/${TMPDIR_PREFIX}.XXXXXX")"; }

# ── Config helpers ──
ensure_config() {
  mkdir -p "$CONFIG_DIR"
  [[ -f "$CONFIG_FILE" ]] || echo '{"aliases":{},"registry":""}' > "$CONFIG_FILE"
  # Migrate: remove "project" key from global config (moved to .skillpullrc)
  if grep -q '"project"' "$CONFIG_FILE" 2>/dev/null; then
    sedi 's/,"project":"[^"]*"//g' "$CONFIG_FILE"
    sedi 's/"project":"[^"]*",//g' "$CONFIG_FILE"
  fi
}

read_config_key() {
  local key="$1"
  [[ ! -f "$CONFIG_FILE" ]] && { echo ""; return 0; }
  local val
  val="$(grep -o "\"${key}\":\"[^\"]*\"" "$CONFIG_FILE" 2>/dev/null | head -1 | \
    sed "s/\"${key}\":\"//" | sed 's/"$//')" || true
  echo "$val"
}

read_alias() {
  local name="$1"
  [[ ! -f "$CONFIG_FILE" ]] && { echo ""; return 0; }
  local val
  val="$(sed -n '/"aliases"/,/}/p' "$CONFIG_FILE" 2>/dev/null | \
    grep -o "\"${name}\":\"[^\"]*\"" | head -1 | \
    sed "s/\"${name}\":\"//" | sed 's/"$//')" || true
  echo "$val"
}

write_alias() {
  local name="$1" url="$2"
  ensure_config
  local esc_url; esc_url="$(_json_escape "$url")"
  if grep -q "\"${name}\"" "$CONFIG_FILE" 2>/dev/null; then
    sedi "s|\"${name}\":\"[^\"]*\"|\"${name}\":\"${esc_url}\"|" "$CONFIG_FILE"
  else
    # Insert into aliases object
    sedi "s|\"aliases\":{|\"aliases\":{\"${name}\":\"${esc_url}\",|" "$CONFIG_FILE"
    # Clean trailing comma before }
    sedi 's/,}/}/g' "$CONFIG_FILE"
  fi
}

remove_alias() {
  local name="$1"
  ensure_config
  if ! grep -q "\"${name}\"" "$CONFIG_FILE" 2>/dev/null; then
    err "Alias '$name' not found"
    return 1
  fi
  # Remove the alias entry (try with trailing comma first, then without)
  sedi "s|\"${name}\":\"[^\"]*\",||" "$CONFIG_FILE"
  sedi "s|\"${name}\":\"[^\"]*\"||" "$CONFIG_FILE"
  # Clean up double commas and trailing commas
  sedi 's/,,/,/g; s/,}/}/g; s/{,/{/g' "$CONFIG_FILE"
  info "Alias '$name' removed"
}

list_aliases() {
  ensure_config
  local aliases
  aliases="$(sed -n '/"aliases"/,/}/p' "$CONFIG_FILE" 2>/dev/null)" || true
  local entries
  entries="$(echo "$aliases" | grep -oP '"[^"]+":"[^"]+"' | grep -v '"aliases"')" || true
  if [[ -z "$entries" ]]; then
    warn "No aliases configured"
    return 0
  fi
  printf "\n  ${CYAN}%-20s %s${RESET}\n" "ALIAS" "URL"
  printf "  %-20s %s\n" "────────────────────" "────────────────────────────────────────"
  echo "$entries" | while IFS= read -r line; do
    local aname aurl
    aname="$(echo "$line" | sed 's/"\([^"]*\)".*/\1/')"
    aurl="$(echo "$line" | sed 's/[^:]*:"\([^"]*\)"/\1/')"
    printf "  %-20s %s\n" "$aname" "$aurl"
  done
  echo
}

set_registry() {
  local url="$1"
  ensure_config
  local esc_url; esc_url="$(_json_escape "$url")"
  sedi "s|\"registry\":\"[^\"]*\"|\"registry\":\"${esc_url}\"|" "$CONFIG_FILE"
  info "Default registry set to: $url"
}

# ── Project-level config (.skillpullrc) ──
PROJECT_RC=".skillpullrc"

read_project_rc() {
  local key="$1"
  [[ ! -f "$PROJECT_RC" ]] && { echo ""; return 0; }
  local val
  val="$(grep -o "\"${key}\":\"[^\"]*\"" "$PROJECT_RC" 2>/dev/null | head -1 | \
    sed "s/\"${key}\":\"//" | sed 's/"$//')" || true
  echo "$val"
}

write_project_rc() {
  local project="$1" registry="${2:-}"
  local esc_project; esc_project="$(_json_escape "$project")"
  local esc_registry; esc_registry="$(_json_escape "$registry")"
  echo "{\"registry\":\"${esc_registry}\",\"project\":\"${esc_project}\"}" > "$PROJECT_RC"
}

# Resolve a config value: CLI > .skillpullrc > global config
resolve_config() {
  local key="$1" cli_value="${2:-}"
  # CLI flag takes priority
  if [[ -n "$cli_value" ]]; then
    echo "$cli_value"
    return 0
  fi
  # Project-level .skillpullrc
  local rc_val; rc_val="$(read_project_rc "$key")"
  if [[ -n "$rc_val" ]]; then
    echo "$rc_val"
    return 0
  fi
  # Global config
  read_config_key "$key"
}

# ── URL resolution ──
# Supports: full URL, user/repo shortname, @alias, bare skill name (from registry)
resolve_repo_url() {
  local input="$1"

  # @alias
  if [[ "$input" == @* ]]; then
    local alias_name="${input#@}"
    local alias_url; alias_url="$(read_alias "$alias_name")"
    if [[ -z "$alias_url" ]]; then
      err "Alias '$alias_name' not found. Use 'skillpull alias list' to see aliases."
      return 1
    fi
    echo "$alias_url"
    return
  fi

  # Full URL (contains :// or starts with git@)
  if [[ "$input" == *"://"* || "$input" == git@* ]]; then
    echo "$input"
    return
  fi

  # user/repo shortname (contains exactly one /)
  if [[ "$input" == */* && "$(echo "$input" | tr -cd '/' | wc -c)" == "1" ]]; then
    echo "https://github.com/${input}.git"
    return
  fi

  # Bare name -> try registry
  local registry; registry="$(resolve_config "registry")"
  if [[ -n "$registry" ]]; then
    # Treat registry as a repo URL, skill name as filter
    echo "$registry"
    return
  fi

  # Nothing matched
  err "Cannot resolve '$input'. Use a URL, user/repo, or @alias."
  return 1
}

# ── Frontmatter parsing ──
parse_frontmatter() {
  local file="$1" key="$2"
  local block
  block="$(sed -n '1{/^---$/!q};1,/^---$/{/^---$/d;p}' "$file" 2>/dev/null)" || true
  local val
  val="$(echo "$block" | grep -E "^${key}:" | head -1 | \
    sed "s/^${key}:[[:space:]]*//" | sed "s/^['\"]//;s/['\"]$//")" || true
  echo "$val"
}

# ── Skill discovery ──
discover_skills() {
  local dir="$1"
  find "$dir" -name "SKILL.md" -type f \
    -not -path "*/.git/*" \
    -not -path "*/node_modules/*" \
    -not -path "*/.skillpull.json" \
    -exec dirname {} \; 2>/dev/null | sort -u
}

# Scoped discovery: skills/ folder (common) + optional project-specific skills
# When project_only=1, skip the common skills/ folder entirely
discover_skills_scoped() {
  local dir="$1" project="${2:-}" project_only="${3:-0}"
  local results=()

  # Common skills: $dir/skills/*/SKILL.md (skip when project_only)
  if [[ "$project_only" != "1" ]]; then
    local d
    for d in "$dir/skills"/*/SKILL.md; do
      [[ -f "$d" ]] || continue
      local skill_dir; skill_dir="$(dirname "$d")"
      results+=("$skill_dir")
    done
  fi

  # Project-specific skills: $dir/$project/*/SKILL.md
  if [[ -n "$project" ]]; then
    for d in "$dir/$project"/*/SKILL.md; do
      [[ -f "$d" ]] || continue
      local skill_dir; skill_dir="$(dirname "$d")"
      results+=("$skill_dir")
    done
  fi

  printf '%s\n' "${results[@]}" 2>/dev/null | sort -u
}

# ── SKILL.md → .mdc conversion (for Cursor) ──
convert_skill_to_mdc() {
  local skill_md="$1" output_file="$2"
  local name desc body
  name="$(parse_frontmatter "$skill_md" "name")"
  desc="$(parse_frontmatter "$skill_md" "description")"

  # Extract body (everything after the second ---)
  body="$(awk 'BEGIN{n=0} /^---$/{n++;next} n>=2{print}' "$skill_md")" || true

  # Write .mdc with Cursor frontmatter
  {
    echo "---"
    echo "description: \"${desc}\""
    echo "globs: "
    echo "alwaysApply: false"
    echo "---"
    echo ""
    [[ -n "$name" ]] && echo "# ${name}" && echo ""
    echo "$body"
  } > "$output_file"
}

resolve_target_dir() {
  local agent="$1" is_global="$2" custom_path="$3"
  if [[ -n "$custom_path" ]]; then
    echo "$custom_path"
    return
  fi
  if [[ "$is_global" == "1" ]]; then
    local gdir; gdir="$(agent_global "$agent")"
    if [[ -z "$gdir" ]]; then
      err "$agent does not support global rules (no file-based global path)"
      return 1
    fi
    echo "$gdir"
  else
    echo "$(agent_project "$agent")"
  fi
}

skill_display_name() {
  local skill_dir="$1"
  local name
  name="$(parse_frontmatter "$skill_dir/SKILL.md" "name")"
  [[ -z "$name" ]] && name="$(basename "$skill_dir")"
  echo "$name"
}

# ── Manifest (.skillpull.json) ──
read_manifest_entry() {
  local manifest="$1" skill_name="$2" key="$3"
  [[ ! -f "$manifest" ]] && { echo ""; return 0; }
  local val
  # Extract the line containing the skill, then pull out the key's value
  val="$(grep "\"${skill_name}\"" "$manifest" 2>/dev/null | \
    grep -o "\"${key}\":\"[^\"]*\"" | head -1 | \
    sed "s/\"${key}\":\"//" | sed 's/"$//')" || true
  echo "$val"
}

_json_escape() {
  local s="$1"
  s="${s//\\/\\\\}"
  s="${s//\"/\\\"}"
  echo "$s"
}

write_manifest_entry() {
  local manifest="$1" skill_name="$2" repo="$3" branch="$4" commit="$5" version="$6" scope="${7:-common}"
  local ts; ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  local esc_repo; esc_repo="$(_json_escape "$repo")"
  local entry
  entry=$(printf '    "%s": {"repo":"%s","branch":"%s","commit":"%s","version":"%s","scope":"%s","pulled_at":"%s"}' \
    "$skill_name" "$esc_repo" "$branch" "$commit" "$version" "$scope" "$ts")

  if [[ ! -f "$manifest" ]]; then
    printf '{\n  "skills": {\n%s\n  }\n}\n' "$entry" > "$manifest"
    return
  fi

  # Remove old entry if exists, then append new one
  local tmp; tmp="$(mktemp)"
  if grep -q "\"${skill_name}\"" "$manifest" 2>/dev/null; then
    # Replace existing entry
    awk -v name="\"${skill_name}\"" -v new="$entry" '
      $0 ~ name { found=1; print new; next }
      found && /}/ { found=0; next }
      found { next }
      { print }
    ' "$manifest" > "$tmp"
    mv "$tmp" "$manifest"
  else
    # Append new entry before closing braces using awk
    awk -v new="$entry" '
      /^  }/ && !done { printf "%s,\n", new; done=1 }
      { print }
    ' "$manifest" > "$tmp"
    mv "$tmp" "$manifest"
    # Fix trailing comma before closing brace
    local tmp2; tmp2="$(mktemp)"
    awk '
      { lines[NR] = $0; n = NR }
      END {
        for (i=1; i<=n; i++) {
          if (i < n && lines[i] ~ /,$/ && lines[i+1] ~ /^  }/) {
            sub(/,$/, "", lines[i])
          }
          print lines[i]
        }
      }
    ' "$manifest" > "$tmp2"
    mv "$tmp2" "$manifest"
  fi
}

# ── Clone helper ──
clone_repo() {
  local url="$1" branch="${2:-}" tmpdir="$3"
  local cmd=(git clone --depth 1 --quiet)
  [[ -n "$branch" ]] && cmd+=(--branch "$branch")
  cmd+=("$url" "$tmpdir")
  local output
  if ! output=$("${cmd[@]}" 2>&1); then
    err "Git clone failed: $url"
    [[ -n "$output" ]] && err "$output"
    return 1
  fi
}

get_commit() {
  local dir="$1"
  git -C "$dir" rev-parse --short HEAD 2>/dev/null || echo "unknown"
}

get_branch() {
  local dir="$1"
  git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown"
}

# ── Commands ──

cmd_pull() {
  local repo_url="$1" skill_filter="${2:-}" target_dir="$3" force="${4:-0}" dry_run="${5:-0}" agent="${6:-claude}" project="${7:-}" project_only="${8:-0}"
  local fmt; fmt="$(agent_format "$agent")"
  make_tmp
  local tmpdir="$_TMPDIR"

  dim "Cloning $repo_url ..."
  clone_repo "$repo_url" "${BRANCH:-}" "$tmpdir" || return 1

  local commit; commit="$(get_commit "$tmpdir")"
  local branch; branch="$(get_branch "$tmpdir")"
  local skills=()

  # Use scoped discovery if project is set, otherwise discover all
  if [[ -n "$project" ]]; then
    while IFS= read -r d; do
      [[ -n "$d" ]] && skills+=("$d")
    done < <(discover_skills_scoped "$tmpdir" "$project" "$project_only")
    if [[ "$project_only" == "1" ]]; then
      dim "Project scope: $project (only)"
    else
      dim "Project scope: $project"
    fi
  else
    while IFS= read -r d; do
      [[ -n "$d" ]] && skills+=("$d")
    done < <(discover_skills "$tmpdir")
  fi

  if [[ ${#skills[@]} -eq 0 ]]; then
    err "No skills found in repository"
    return 1
  fi

  # Filter by name if specified
  if [[ -n "$skill_filter" ]]; then
    local matched=()
    for sd in "${skills[@]}"; do
      local sn; sn="$(skill_display_name "$sd")"
      local dn; dn="$(basename "$sd")"
      if [[ "$sn" == "$skill_filter" || "$dn" == "$skill_filter" ]]; then
        matched+=("$sd")
      fi
    done
    if [[ ${#matched[@]} -eq 0 ]]; then
      err "Skill '$skill_filter' not found. Available skills:"
      for sd in "${skills[@]}"; do
        local sn; sn="$(skill_display_name "$sd")"
        printf "    - %s\n" "$sn" >&2
      done
      return 1
    fi
    skills=("${matched[@]}")
  fi

  mkdir -p "$target_dir"
  local manifest="$target_dir/$MANIFEST_FILE"
  local installed=0

  for sd in "${skills[@]}"; do
    local sn; sn="$(skill_display_name "$sd")"
    local ver; ver="$(parse_frontmatter "$sd/SKILL.md" "version")"

    # Determine scope: common (skills/) or project-specific
    local scope="common"
    if [[ -n "$project" ]]; then
      case "$sd" in
        "$tmpdir/$project/"*) scope="$project" ;;
      esac
    fi

    if [[ "$fmt" == "mdc" ]]; then
      # Cursor: convert SKILL.md → .mdc file
      local dest="$target_dir/${sn}.mdc"

      if [[ -f "$dest" && "$force" != "1" ]]; then
        warn "Rule '$sn.mdc' already exists, use --force to overwrite"
        continue
      fi

      if [[ "$dry_run" == "1" ]]; then
        info "[dry-run] Would install: $sn -> $dest"
        continue
      fi

      convert_skill_to_mdc "$sd/SKILL.md" "$dest"
      write_manifest_entry "$manifest" "$sn" "$repo_url" "$branch" "$commit" "${ver:-}" "$scope"
      info "Installed: $sn -> $dest (converted to .mdc)"
    else
      # Claude/Codex/Kiro: write to shared hub, symlink from agent dir
      local dest="$target_dir/$sn"

      if [[ (-d "$dest" || -L "$dest") && "$force" != "1" ]]; then
        local check_dir="$dest"
        [[ -L "$dest" ]] && check_dir="$(readlink -f "$dest")"
        local old_ver; old_ver="$(parse_frontmatter "$check_dir/SKILL.md" "version" 2>/dev/null)"
        warn "Skill '$sn' already exists (${old_ver:-unknown} -> ${ver:-unknown}), use --force to overwrite"
        continue
      fi

      if [[ "$dry_run" == "1" ]]; then
        info "[dry-run] Would install: $sn -> $SHARED_HUB/$sn (symlink from $dest)"
        continue
      fi

      # Write to shared hub (single source of truth)
      local hub_dest="$SHARED_HUB/$sn"
      mkdir -p "$SHARED_HUB"
      rm -rf "$hub_dest"
      cp -r "$sd" "$hub_dest"
      rm -rf "$hub_dest/.git"
      if [[ -d "$hub_dest/scripts" ]]; then
        chmod +x "$hub_dest/scripts/"* 2>/dev/null || true
      fi

      # Symlink from agent dir (or skip if target IS the hub)
      if [[ "$(cd "$target_dir" 2>/dev/null && pwd -P)" != "$(cd "$SHARED_HUB" 2>/dev/null && pwd -P)" ]]; then
        mkdir -p "$target_dir"
        rm -rf "$dest"
        ln -s "$hub_dest" "$dest"
      fi

      write_manifest_entry "$manifest" "$sn" "$repo_url" "$branch" "$commit" "${ver:-}" "$scope"
      info "Installed: $sn${ver:+ (v$ver)} -> $hub_dest (symlink from $dest)"
    fi
    installed=$((installed + 1))
  done

  [[ "$dry_run" != "1" ]] && info "Done. $installed skill(s) installed to $target_dir"
}

cmd_list() {
  local repo_url="$1" project="${2:-}" project_only="${3:-0}"
  make_tmp
  local tmpdir="$_TMPDIR"

  dim "Cloning $repo_url ..."
  clone_repo "$repo_url" "${BRANCH:-}" "$tmpdir" || return 1

  local skills=()
  if [[ -n "$project" ]]; then
    while IFS= read -r d; do
      [[ -n "$d" ]] && skills+=("$d")
    done < <(discover_skills_scoped "$tmpdir" "$project" "$project_only")
    if [[ "$project_only" == "1" ]]; then
      dim "Project scope: $project (only)"
    else
      dim "Project scope: $project"
    fi
  else
    while IFS= read -r d; do
      [[ -n "$d" ]] && skills+=("$d")
    done < <(discover_skills "$tmpdir")
  fi

  if [[ ${#skills[@]} -eq 0 ]]; then
    err "No skills found in repository"
    return 1
  fi

  printf "\n  ${CYAN}%-25s %-10s %s${RESET}\n" "NAME" "VERSION" "DESCRIPTION"
  printf "  %-25s %-10s %s\n" "─────────────────────────" "──────────" "────────────────────────────────"

  for sd in "${skills[@]}"; do
    local sn; sn="$(skill_display_name "$sd")"
    local ver; ver="$(parse_frontmatter "$sd/SKILL.md" "version")"
    local desc; desc="$(parse_frontmatter "$sd/SKILL.md" "description")"
    # Truncate description
    [[ ${#desc} -gt 50 ]] && desc="${desc:0:47}..."
    printf "  %-25s %-10s %s\n" "$sn" "${ver:--}" "${desc:--}"
  done
  echo
}

cmd_installed() {
  local target_dir="$1"
  if [[ ! -d "$target_dir" ]]; then
    warn "No skills directory found at $target_dir"
    return 0
  fi

  local skills=()
  while IFS= read -r d; do
    [[ -n "$d" ]] && skills+=("$d")
  done < <(discover_skills "$target_dir")

  if [[ ${#skills[@]} -eq 0 ]]; then
    warn "No skills installed in $target_dir"
    return 0
  fi

  local manifest="$target_dir/$MANIFEST_FILE"
  printf "\n  ${CYAN}%-25s %-10s %-35s %s${RESET}\n" "NAME" "VERSION" "SOURCE" "PULLED"
  printf "  %-25s %-10s %-35s %s\n" "─────────────────────────" "──────────" "───────────────────────────────────" "──────────────────"

  for sd in "${skills[@]}"; do
    local sn; sn="$(skill_display_name "$sd")"
    local ver; ver="$(parse_frontmatter "$sd/SKILL.md" "version")"
    local repo; repo="$(read_manifest_entry "$manifest" "$sn" "repo")"
    local pulled; pulled="$(read_manifest_entry "$manifest" "$sn" "pulled_at")"
    # Truncate repo URL
    [[ ${#repo} -gt 33 ]] && repo="${repo:0:30}..."
    printf "  %-25s %-10s %-35s %s\n" "$sn" "${ver:--}" "${repo:--}" "${pulled:--}"
  done
  echo
}

cmd_remove() {
  local skill_name="${1:-}" target_dir="$2" agent="${3:-claude}"
  local fmt; fmt="$(agent_format "$agent")"

  # No skill name provided -> interactive select
  if [[ -z "$skill_name" && -t 0 ]]; then
    if [[ ! -d "$target_dir" ]]; then
      warn "No skills directory found at $target_dir"
      return 0
    fi
    local installed_skills=()
    local installed_names=()
    while IFS= read -r d; do
      [[ -n "$d" ]] && installed_skills+=("$d") && installed_names+=("$(skill_display_name "$d")")
    done < <(discover_skills "$target_dir")

    if [[ ${#installed_skills[@]} -eq 0 ]]; then
      warn "No skills installed in $target_dir"
      return 0
    fi

    select_menu "Select skills to remove:" "${installed_names[@]}"
    if [[ ${#SELECTED_ITEMS[@]} -eq 0 ]]; then
      warn "No skills selected"
      return 0
    fi

    for sel in "${SELECTED_ITEMS[@]}"; do
      cmd_remove "$sel" "$target_dir" "$agent"
    done
    return 0
  fi

  if [[ "$fmt" == "mdc" ]]; then
    local dest="$target_dir/${skill_name}.mdc"
    if [[ ! -f "$dest" ]]; then
      err "Rule '$skill_name.mdc' not found in $target_dir"
      return 1
    fi
    rm -f "$dest"
  else
    local dest="$target_dir/$skill_name"
    if [[ ! -d "$dest" && ! -L "$dest" ]]; then
      local found=""
      while IFS= read -r d; do
        local sn; sn="$(skill_display_name "$d")"
        if [[ "$sn" == "$skill_name" ]]; then
          found="$d"; break
        fi
      done < <(discover_skills "$target_dir")

      if [[ -z "$found" ]]; then
        err "Skill '$skill_name' not found in $target_dir"
        return 1
      fi
      dest="$found"
    fi
    rm -rf "$dest"

    # Clean up shared hub if no other symlinks reference it
    local hub_dest="$SHARED_HUB/$skill_name"
    if [[ -d "$hub_dest" ]]; then
      local refs; refs=$(find "$HOME" -maxdepth 4 -type l -lname "$hub_dest" 2>/dev/null | wc -l)
      if [[ "$refs" -eq 0 ]]; then
        rm -rf "$hub_dest"
        dim "Removed from shared hub: $hub_dest"
      fi
    fi
  fi

  # Clean up manifest entry
  local manifest="$target_dir/$MANIFEST_FILE"
  if [[ -f "$manifest" ]] && grep -q "\"${skill_name}\"" "$manifest" 2>/dev/null; then
    local tmp; tmp="$(mktemp)"
    grep -v "\"${skill_name}\"" "$manifest" > "$tmp" || true
    mv "$tmp" "$manifest"
    # Fix trailing comma before closing brace
    local tmp2; tmp2="$(mktemp)"
    awk '
      { lines[NR] = $0; n = NR }
      END {
        for (i=1; i<=n; i++) {
          if (i < n && lines[i] ~ /,$/ && lines[i+1] ~ /^  }/) {
            sub(/,$/, "", lines[i])
          }
          print lines[i]
        }
      }
    ' "$manifest" > "$tmp2"
    mv "$tmp2" "$manifest"
  fi

  info "Removed: $skill_name from $target_dir"
}

cmd_update() {
  local target_dir="$1" force="$2" agent="$3"
  local manifest="$target_dir/$MANIFEST_FILE"

  if [[ ! -f "$manifest" ]]; then
    err "No manifest found at $target_dir. Nothing to update."
    return 1
  fi

  # Collect unique repos from manifest
  local repos=()
  while IFS= read -r repo; do
    [[ -n "$repo" ]] && repos+=("$repo")
  done < <(grep -oP '"repo":"[^"]*"' "$manifest" | sed 's/"repo":"//;s/"$//' | sort -u)

  if [[ ${#repos[@]} -eq 0 ]]; then
    warn "No skills tracked in manifest"
    return 0
  fi

  local updated=0
  for repo_url in "${repos[@]}"; do
    # Find all skills from this repo
    local skill_names=()
    while IFS= read -r sn; do
      [[ -n "$sn" ]] && skill_names+=("$sn")
    done < <(grep -B1 "\"repo\":\"${repo_url}\"" "$manifest" | grep -oP '^\s*"[^"]+":' | sed 's/[" :]//g')

    dim "Updating from $repo_url ..."
    for sn in "${skill_names[@]}"; do
      local old_commit; old_commit="$(read_manifest_entry "$manifest" "$sn" "commit")"
      cmd_pull "$repo_url" "$sn" "$target_dir" "1" "0" "$agent" 2>/dev/null && {
        local new_commit; new_commit="$(read_manifest_entry "$manifest" "$sn" "commit")"
        if [[ "$old_commit" != "$new_commit" ]]; then
          info "Updated: $sn ($old_commit -> $new_commit)"
          updated=$((updated + 1))
        else
          dim "Already up to date: $sn"
        fi
      }
    done
  done

  info "Done. $updated skill(s) updated."
}

cmd_push() {
  local target_dir="$1" repo_url="$2" agent="$3" skill_filter="${4:-}"
  local fmt; fmt="$(agent_format "$agent")"

  if [[ ! -d "$target_dir" ]]; then
    err "No skills directory found at $target_dir"
    return 1
  fi

  # Resolve push target repo
  if [[ -z "$repo_url" ]]; then
    repo_url="$(resolve_config "registry")"
    if [[ -z "$repo_url" ]]; then
      err "No target repo specified and no registry set."
      err "Usage: skillpull push <user/repo> or set registry first."
      return 1
    fi
  fi

  local resolved; resolved="$(resolve_repo_url "$repo_url")" || return 1

  # Clone the remote repo
  make_tmp
  local tmpdir="$_TMPDIR"
  dim "Cloning $resolved ..."

  # Try clone; if empty repo, init fresh
  if ! clone_repo "$resolved" "${BRANCH:-}" "$tmpdir" 2>/dev/null; then
    git init --quiet "$tmpdir"
    git -C "$tmpdir" remote add origin "$resolved"
  fi

  local branch; branch="$(git -C "$tmpdir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "master")"

  # Read project name from .skillpullrc
  local project_name; project_name="$(read_project_rc "project")"

  # Discover local skills
  local manifest="$target_dir/$MANIFEST_FILE"
  local skills=()
  if [[ "$fmt" == "mdc" ]]; then
    while IFS= read -r f; do
      [[ -n "$f" ]] && skills+=("$f")
    done < <(find "$target_dir" -name "*.mdc" -type f 2>/dev/null)
  else
    while IFS= read -r d; do
      [[ -n "$d" ]] && skills+=("$d")
    done < <(discover_skills "$target_dir")
  fi

  if [[ ${#skills[@]} -eq 0 ]]; then
    err "No skills found in $target_dir"
    return 1
  fi

  # Filter by name if specified
  if [[ -n "$skill_filter" ]]; then
    local matched=()
    for sd in "${skills[@]}"; do
      local sn
      if [[ "$fmt" == "mdc" ]]; then
        sn="$(basename "$sd" .mdc)"
      else
        sn="$(skill_display_name "$sd")"
      fi
      local dn; dn="$(basename "$sd")"
      if [[ "$sn" == "$skill_filter" || "$dn" == "$skill_filter" ]]; then
        matched+=("$sd")
      fi
    done
    if [[ ${#matched[@]} -eq 0 ]]; then
      err "Skill '$skill_filter' not found locally. Available skills:"
      for sd in "${skills[@]}"; do
        local sn
        if [[ "$fmt" == "mdc" ]]; then
          sn="$(basename "$sd" .mdc)"
        else
          sn="$(skill_display_name "$sd")"
        fi
        printf "    - %s\n" "$sn" >&2
      done
      return 1
    fi
    skills=("${matched[@]}")
  fi

  # Classify skills by scope
  local pushed=0
  local untracked_names=()
  local untracked_dirs=()

  for sd in "${skills[@]}"; do
    local sn
    if [[ "$fmt" == "mdc" ]]; then
      sn="$(basename "$sd" .mdc)"
    else
      sn="$(skill_display_name "$sd")"
    fi

    # Check manifest for scope
    local scope; scope="$(read_manifest_entry "$manifest" "$sn" "scope")"

    if [[ -n "$scope" ]]; then
      # Known skill: route by scope
      local dest_dir
      if [[ "$scope" == "common" ]]; then
        dest_dir="$tmpdir/skills/$sn"
      else
        dest_dir="$tmpdir/$scope/$sn"
      fi
      mkdir -p "$(dirname "$dest_dir")"
      rm -rf "$dest_dir"
      cp -r "$sd" "$dest_dir"
      rm -rf "$dest_dir/.git"
      info "Staged: $sn -> ${scope}/"
      pushed=$((pushed + 1))
    else
      # Untracked skill: collect for user choice
      untracked_names+=("$sn")
      untracked_dirs+=("$sd")
    fi
  done

  # Handle untracked skills: let user choose destination
  if [[ ${#untracked_names[@]} -gt 0 && -t 0 ]]; then
    for ((idx=0; idx<${#untracked_names[@]}; idx++)); do
      local sn="${untracked_names[$idx]}"
      local sd="${untracked_dirs[$idx]}"
      local choice_dest=""

      if [[ -n "$project_name" ]]; then
        printf "\n  ${CYAN}New skill: %s${RESET}\n" "$sn" >&2
        printf "  Push to:\n" >&2
        printf "    ${CYAN}1${RESET}) skills/        (common)\n" >&2
        printf "    ${CYAN}2${RESET}) %s/    (project)\n" "$project_name" >&2
        printf "  Choice [1/2]: " >&2
        read -r choice
        case "$choice" in
          2) choice_dest="$tmpdir/$project_name/$sn" ;;
          *) choice_dest="$tmpdir/skills/$sn" ;;
        esac
      else
        choice_dest="$tmpdir/skills/$sn"
      fi

      mkdir -p "$(dirname "$choice_dest")"
      rm -rf "$choice_dest"
      cp -r "$sd" "$choice_dest"
      rm -rf "$choice_dest/.git"
      info "Staged: $sn"
      pushed=$((pushed + 1))
    done
  elif [[ ${#untracked_names[@]} -gt 0 ]]; then
    # Non-interactive: default to common
    for ((idx=0; idx<${#untracked_names[@]}; idx++)); do
      local sn="${untracked_names[$idx]}"
      local sd="${untracked_dirs[$idx]}"
      local dest="$tmpdir/skills/$sn"
      mkdir -p "$tmpdir/skills"
      rm -rf "$dest"
      cp -r "$sd" "$dest"
      rm -rf "$dest/.git"
      info "Staged: $sn -> skills/ (default)"
      pushed=$((pushed + 1))
    done
  fi

  if [[ "$pushed" -eq 0 ]]; then
    dim "No skills to push."
    return 0
  fi

  # Commit and push
  git -C "$tmpdir" add -A
  if git -C "$tmpdir" diff --cached --quiet 2>/dev/null; then
    dim "No changes to push. Remote is already up to date."
    return 0
  fi

  git -C "$tmpdir" commit --quiet -m "skillpull push: $pushed skill(s) updated"
  dim "Pushing to $resolved ..."

  if ! git -C "$tmpdir" push origin "$branch" 2>&1; then
    err "Push failed. Check your permissions for $resolved"
    return 1
  fi

  info "Done. $pushed skill(s) pushed to $resolved"
}

cmd_uninstall() {
  local self; self="$(realpath "$0" 2>/dev/null || echo "$0")"
  local local_bin="${SKILLPULL_INSTALL_DIR:-$HOME/.local/bin}/skillpull"
  local removed=0

  printf "\n  ${CYAN}Uninstall skillpull${RESET}\n\n"

  # Remove local binary (~/.local/bin)
  if [[ -f "$local_bin" ]]; then
    rm -f "$local_bin"
    info "Removed $local_bin"
    removed=1
  fi

  # Remove npm global package (this is likely where the command lives)
  if command -v npm &>/dev/null; then
    local npm_pkg; npm_pkg="$(npm ls -g skillpull --parseable 2>/dev/null)" || true
    if [[ -n "$npm_pkg" ]]; then
      npm uninstall -g skillpull 2>/dev/null
      info "Removed npm global package"
      removed=1
    fi
  fi

  # Remove the running binary itself if still exists and wasn't already cleaned
  if [[ -f "$self" && "$self" != "$local_bin" ]]; then
    rm -f "$self" 2>/dev/null || true
    info "Removed $self"
    removed=1
  fi

  # Remove config
  if [[ -d "$CONFIG_DIR" ]]; then
    printf "  Remove config (~/.config/skillpull)? (y/n) "
    read -r yn
    if [[ "$yn" == "y" || "$yn" == "Y" ]]; then
      rm -rf "$CONFIG_DIR"
      info "Removed $CONFIG_DIR"
    else
      dim "Config kept at $CONFIG_DIR"
    fi
  fi

  echo ""
  if [[ "$removed" == "1" ]]; then
    info "skillpull uninstalled."
  else
    warn "skillpull not found. Already uninstalled?"
  fi
}

cmd_init_global() {
  ensure_config
  local current; current="$(read_config_key "registry")"

  printf "\n  ${CYAN}Global setup${RESET}\n\n"
  printf "  Set a default skill repository so you can pull skills by name.\n\n"
  printf "  ${DIM}Examples:${RESET}\n"
  printf "  ${DIM}  tianhaocui/ai-skills                     GitHub shortname${RESET}\n"
  printf "  ${DIM}  https://github.com/user/repo             Full URL${RESET}\n"
  printf "  ${DIM}  git@github.com:user/repo.git             SSH URL${RESET}\n"
  echo ""

  if [[ -n "$current" ]]; then
    printf "  Current: ${GREEN}%s${RESET}\n\n" "$current"
    printf "  New skill repo (Enter to keep current): "
  else
    printf "  Skill repo: "
  fi

  read -r repo_input

  # Trim whitespace and control characters
  repo_input="$(echo "$repo_input" | tr -d '[:cntrl:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"

  if [[ -z "$repo_input" ]]; then
    if [[ -n "$current" ]]; then
      dim "Keeping current registry: $current"
    else
      warn "No registry set. Run 'skillpull init --global' again when ready."
      return 0
    fi
  else
    local resolved; resolved="$(resolve_repo_url "$repo_input")" || return 1
    set_registry "$resolved"
  fi

  echo ""
  printf "  ${CYAN}What's next:${RESET}\n"
  printf "    skillpull init              Setup project config (.skillpullrc)\n"
  printf "    skillpull list              List available skills\n"
  printf "    skillpull --help            See all commands\n"
  echo ""
}

cmd_init_project() {
  # Read existing .skillpullrc if present
  local current_project; current_project="$(read_project_rc "project")"
  local current_registry; current_registry="$(read_project_rc "registry")"

  printf "\n  ${CYAN}Project setup${RESET} ${DIM}(%s)${RESET}\n\n" "$PWD"

  # ── Project name ──
  printf "  ${DIM}Set the project name to match a subfolder in your skill repo.${RESET}\n"
  printf "  ${DIM}Skills from both skills/ (common) and the project subfolder will be pulled.${RESET}\n\n"

  if [[ -n "$current_project" ]]; then
    printf "  Current: ${GREEN}%s${RESET}\n\n" "$current_project"
    printf "  Project name (Enter to keep, 'none' to clear): "
  else
    printf "  Project name: "
  fi

  read -r project_input
  project_input="$(echo "$project_input" | tr -d '[:cntrl:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"

  local new_project="$current_project"
  if [[ -n "$project_input" ]]; then
    if [[ "$project_input" == "none" ]]; then
      new_project=""
    else
      new_project="$project_input"
    fi
  fi

  # ── Optional registry override ──
  echo ""
  printf "  ${DIM}Optional: override the global skill repo for this project.${RESET}\n"
  printf "  ${DIM}Leave empty to use the global registry.${RESET}\n\n"

  if [[ -n "$current_registry" ]]; then
    printf "  Current: ${GREEN}%s${RESET}\n\n" "$current_registry"
    printf "  Skill repo override (Enter to keep, 'none' to clear): "
  else
    printf "  Skill repo override (Enter to skip): "
  fi

  read -r registry_input
  registry_input="$(echo "$registry_input" | tr -d '[:cntrl:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"

  local new_registry="$current_registry"
  if [[ -n "$registry_input" ]]; then
    if [[ "$registry_input" == "none" ]]; then
      new_registry=""
    else
      local resolved; resolved="$(resolve_repo_url "$registry_input")" || return 1
      new_registry="$resolved"
    fi
  fi

  write_project_rc "$new_project" "$new_registry"

  echo ""
  if [[ -n "$new_project" ]]; then
    info "Project: $new_project"
  fi
  if [[ -n "$new_registry" ]]; then
    info "Registry override: $new_registry"
  fi
  info "Saved to $PROJECT_RC"

  echo ""
  printf "  ${CYAN}What's next:${RESET}\n"
  printf "    skillpull list              List available skills\n"
  printf "    skillpull <skill-name>      Pull a skill into your project\n"
  printf "    skillpull --help            See all commands\n"
  echo ""
}

cmd_alias() {
  local subcmd="${1:-list}" name="${2:-}" url="${3:-}"
  case "$subcmd" in
    add)
      [[ -z "$name" || -z "$url" ]] && { err "Usage: skillpull alias add <name> <url>"; return 1; }
      local resolved; resolved="$(resolve_repo_url "$url")" || return 1
      write_alias "$name" "$resolved"
      info "Alias '@$name' -> $resolved"
      ;;
    rm|remove)
      [[ -z "$name" ]] && { err "Usage: skillpull alias rm <name>"; return 1; }
      remove_alias "$name"
      ;;
    list|"")
      list_aliases
      ;;
    *)
      err "Unknown alias subcommand: $subcmd"
      return 1
      ;;
  esac
}

cmd_search() {
  local keyword="${1:-}"
  if [[ -z "$keyword" && -t 0 ]]; then
    printf "  Search keyword: "
    read -r keyword
  fi
  [[ -z "$keyword" ]] && { err "Usage: skillpull search <keyword>"; return 1; }

  dim "Searching GitHub for '$keyword' ..."
  local encoded; encoded="$(echo "$keyword" | sed 's/ /+/g')"
  local api_url="https://api.github.com/search/repositories?q=${encoded}+skills+in:name,description,readme&sort=stars&per_page=15"
  local result
  result="$(curl -sS -H "Accept: application/vnd.github.v3+json" "$api_url" 2>/dev/null)" || {
    err "GitHub API request failed"
    return 1
  }

  local count
  count="$(echo "$result" | grep '"total_count"' | head -1 | grep -o '[0-9]*')" || true

  if [[ "${count:-0}" == "0" ]]; then
    warn "No results found for '$keyword'"
    return 0
  fi

  printf "\n  ${CYAN}%-30s %-8s %s${RESET}\n" "REPO" "STARS" "DESCRIPTION"
  printf "  %-30s %-8s %s\n" "──────────────────────────────" "────────" "────────────────────────────────"

  # Parse multi-line JSON with grep/sed
  local names stars descs
  names="$(echo "$result" | grep '"full_name"' | sed 's/.*"full_name": *"//;s/".*//' | head -15)"
  stars="$(echo "$result" | grep '"stargazers_count"' | sed 's/.*"stargazers_count": *//;s/,.*//' | head -15)"
  descs="$(echo "$result" | grep '"description"' | grep -v '"description": *null' | sed 's/.*"description": *"//;s/".*//' | head -15)"

  paste <(echo "$names") <(echo "$stars") <(echo "$descs") | while IFS=$'\t' read -r n s d; do
    [[ -z "$n" ]] && continue
    [[ ${#d} -gt 40 ]] && d="${d:0:37}..."
    printf "  %-30s %-8s %s\n" "$n" "${s:--}" "${d:--}"
  done
  echo
  dim "Install with: skillpull <user/repo>"
}

# ── Usage ──
usage() {
  cat <<'HELP'
skillpull — Sync AI agent skills from Git repositories

USAGE:
  skillpull init --global
  skillpull init
  skillpull <source> [skill-name] [options]
  skillpull list <source>
  skillpull search <keyword>
  skillpull alias add <name> <url>
  skillpull alias list
  skillpull alias rm <name>
  skillpull registry <repo-url>
  skillpull update [options]
  skillpull push [target-repo] [skill-name] [options]
  skillpull installed [--global]
  skillpull remove <skill-name> [--global]
  skillpull uninstall

SOURCE FORMATS:
  https://github.com/user/repo    Full URL (GitHub, GitLab, any Git host)
  git@github.com:user/repo.git    SSH URL
  user/repo                       GitHub shortname (auto-expands)
  @myalias                        Alias (configured via 'alias add')

TARGETS:
  --claude              Claude Code (default): .claude/skills/
  --codex               OpenAI Codex CLI: .codex/skills/
  --kiro                Kiro: .kiro/skills/
  --cursor              Cursor: .cursor/rules/ (auto-converts to .mdc)
  --all                 Install to all supported targets at once

OPTIONS:
  --global, -g          Install to user-level directory / global init
  --path <dir>          Install to a custom directory
  --branch <ref>        Use a specific branch/tag/commit
  --project <name>      Include project-specific skills from <name>/ subfolder
                        (defaults to value in .skillpullrc)
  --project-only        Only pull from the --project subfolder, skip skills/
  --force, -f           Overwrite existing skills
  --dry-run             Preview without making changes
  --quiet, -q           Suppress non-error output
  --help, -h            Show this help
  --version             Show version

CONFIG:
  Global:   ~/.config/skillpull/config.json   (registry, aliases)
  Project:  .skillpullrc                      (project name, optional registry override)

  Resolution order: CLI flags > .skillpullrc > global config

EXAMPLES:
  skillpull init --global                            # Setup default skill repo
  skillpull init                                     # Setup project config (.skillpullrc)
  skillpull tianhaocui/ai-skills                     # GitHub shortname
  skillpull @work plan-first-development --global    # From alias
  skillpull search coding-standards                  # Search GitHub
  skillpull alias add work git@github.com:me/skills  # Save alias
  skillpull registry tianhaocui/ai-skills            # Set default source
  skillpull tianhaocui/ai-skills --all               # All tools at once
  skillpull tianhaocui/ai-skills --cursor            # Convert to .mdc
  skillpull update                                   # Update all installed skills
  skillpull push tianhaocui/ai-skills                # Push all local skills to remote
  skillpull push tianhaocui/ai-skills my-skill       # Push a single skill to remote
  skillpull user/repo --project my-app               # Common + project-specific skills
  skillpull user/repo --project livechat --project-only  # Only livechat/ skills
HELP
}

# ── Argument parsing & dispatch ──
main() {
  local cmd="" repo_url="" skill_name="" custom_path=""
  local force=0 dry_run=0 is_global=0 use_all=0 agent_explicit=0 project_name="" project_only=0
  local agents=()
  local alias_args=()
  QUIET=0; BRANCH=""

  if [[ $# -eq 0 ]]; then
    # If running via npx or not installed globally, auto-install
    local self; self="$(realpath "$0" 2>/dev/null || echo "$0")"
    local global_bin="${SKILLPULL_INSTALL_DIR:-$HOME/.local/bin}/skillpull"

    if [[ "$self" != "$global_bin" && ! -f "$global_bin" ]]; then
      info "Installing skillpull v${VERSION} ..."
      mkdir -p "$(dirname "$global_bin")"
      cp "$self" "$global_bin"
      chmod +x "$global_bin"
      info "Installed to $global_bin"
      if ! echo "$PATH" | tr ':' '\n' | grep -qx "$(dirname "$global_bin")"; then
        warn "Add to PATH: export PATH=\"$(dirname "$global_bin"):\$PATH\""
      fi
      echo ""
      info "Run 'skillpull init --global' to set up your default skill repo."
      exit 0
    fi

    # Already installed, no args -> treat as pull (will use registry + interactive select)
    cmd="pull"
  fi

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --help|-h)    usage; exit 0;;
      --version)    echo "skillpull $VERSION"; exit 0 ;;
      --global|-g)  is_global=1; shift ;;
      --path)       [[ $# -lt 2 ]] && { err "--path requires a directory"; exit 1; }; custom_path="$2"; shift 2 ;;
      --branch)     [[ $# -lt 2 ]] && { err "--branch requires a ref"; exit 1; }; BRANCH="$2"; shift 2 ;;
      --project)    [[ $# -lt 2 ]] && { err "--project requires a name"; exit 1; }; project_name="$2"; shift 2 ;;
      --project-only) project_only=1; shift ;;
      --force|-f)   force=1; shift ;;
      --dry-run)    dry_run=1; shift ;;
      --quiet|-q)   QUIET=1; shift ;;
      --claude)     agents+=("claude"); agent_explicit=1; shift ;;
      --codex)      agents+=("codex"); agent_explicit=1; shift ;;
      --kiro)       agents+=("kiro"); agent_explicit=1; shift ;;
      --cursor)     agents+=("cursor"); agent_explicit=1; shift ;;
      --all)        use_all=1; agent_explicit=1; shift ;;
      list)         cmd="list"; shift ;;
      installed)    cmd="installed"; shift ;;
      remove)       cmd="remove"; shift ;;
      update)       cmd="update"; shift ;;
      push)         cmd="push"; shift ;;
      init)         cmd="init"; shift ;;
      uninstall)    cmd="uninstall"; shift ;;
      search)       cmd="search"; shift ;;
      alias)        cmd="alias"; shift
                    # Collect remaining args for alias subcommand
                    while [[ $# -gt 0 ]]; do alias_args+=("$1"); shift; done
                    ;;
      registry)     cmd="registry"; shift ;;
      -*)           err "Unknown option: $1"; usage; exit 1 ;;
      *)
        if [[ -z "$cmd" && -z "$repo_url" ]]; then
          repo_url="$1"; cmd="pull"
        elif [[ "$cmd" == "pull" && -n "$repo_url" && -z "$skill_name" ]]; then
          skill_name="$1"
        elif [[ "$cmd" == "list" && -z "$repo_url" ]]; then
          repo_url="$1"
        elif [[ "$cmd" == "remove" && -z "$skill_name" ]]; then
          skill_name="$1"
        elif [[ "$cmd" == "search" && -z "$skill_name" ]]; then
          skill_name="$1"
        elif [[ "$cmd" == "registry" && -z "$repo_url" ]]; then
          repo_url="$1"
        elif [[ "$cmd" == "push" && -z "$repo_url" ]]; then
          repo_url="$1"
        elif [[ "$cmd" == "push" && -n "$repo_url" && -z "$skill_name" ]]; then
          skill_name="$1"
        else
          warn "Ignoring unexpected argument: $1"
        fi
        shift ;;
    esac
  done

  # Default to claude if no agent specified
  if [[ "$use_all" == "1" ]]; then
    agents=("claude" "codex" "kiro" "cursor")
  elif [[ ${#agents[@]} -eq 0 ]]; then
    if [[ "$agent_explicit" == "0" && -t 0 && ( "$cmd" == "pull" || "$cmd" == "" || "$cmd" == "remove" || "$cmd" == "push" ) ]]; then
      # Interactive terminal, no agent flag -> let user choose
      select_menu "Install to which tools?" "claude (.claude/skills)" "codex (.codex/skills)" "kiro (.kiro/skills)" "cursor (.cursor/rules)"
      if [[ ${#SELECTED_ITEMS[@]} -eq 0 ]]; then
        warn "No tools selected"
        exit 0
      fi
      for sel in "${SELECTED_ITEMS[@]}"; do
        local agent_name; agent_name="$(echo "$sel" | awk '{print $1}')"
        agents+=("$agent_name")
      done
    else
      agents=("$DEFAULT_AGENT")
    fi
  fi

  case "${cmd:-}" in
    pull|"")
      # Resolve project: CLI > .skillpullrc > (none)
      if [[ -z "$project_name" ]]; then
        project_name="$(read_project_rc "project")"
      fi
      if [[ "$project_only" == "1" && -z "$project_name" ]]; then
        err "--project-only requires --project <name>"
        exit 1
      fi
      if [[ -z "$repo_url" ]]; then
        # Resolve registry: .skillpullrc > global config
        local reg; reg="$(resolve_config "registry")"
        if [[ -z "$reg" ]]; then
          err "No source specified and no registry set."
          err "Run 'skillpull init --global' to set a default skill repo."
          exit 1
        fi
        repo_url="$reg"
      fi
      local resolved; resolved="$(resolve_repo_url "$repo_url")" || exit 1
      # If resolve_repo_url returned registry URL for bare name, use original as skill filter
      if [[ "$resolved" != "$repo_url" && "$repo_url" != @* && "$repo_url" != */* && "$repo_url" != *"://"* && "$repo_url" != git@* ]]; then
        skill_name="$repo_url"
      fi
      for agent in "${agents[@]}"; do
        local target_dir
        target_dir="$(resolve_target_dir "$agent" "$is_global" "$custom_path")" || continue
        dim "Target: $agent -> $target_dir"
        cmd_pull "$resolved" "$skill_name" "$target_dir" "$force" "$dry_run" "$agent" "$project_name" "$project_only"
      done
      ;;
    list)
      # Resolve project: CLI > .skillpullrc > (none)
      if [[ -z "$project_name" ]]; then
        project_name="$(read_project_rc "project")"
      fi
      if [[ "$project_only" == "1" && -z "$project_name" ]]; then
        err "--project-only requires --project <name>"
        exit 1
      fi
      if [[ -z "$repo_url" ]]; then
        local reg; reg="$(resolve_config "registry")"
        if [[ -z "$reg" ]]; then
          err "No source specified and no registry set."
          err "Run 'skillpull init --global' to set a default skill repo."
          exit 1
        fi
        repo_url="$reg"
      fi
      local resolved; resolved="$(resolve_repo_url "$repo_url")" || exit 1
      cmd_list "$resolved" "$project_name" "$project_only"
      ;;
    search)
      cmd_search "${skill_name:-}"
      ;;
    init)
      if [[ "$is_global" == "1" ]]; then
        cmd_init_global
      else
        cmd_init_project
      fi
      ;;
    uninstall)
      cmd_uninstall
      ;;
    alias)
      cmd_alias "${alias_args[@]}"
      ;;
    registry)
      if [[ -z "$repo_url" ]]; then
        local current; current="$(read_config_key "registry")"
        if [[ -n "$current" ]]; then
          info "Current registry: $current"
        else
          warn "No default registry set. Usage: skillpull registry <user/repo>"
        fi
      else
        local resolved; resolved="$(resolve_repo_url "$repo_url")" || exit 1
        set_registry "$resolved"
      fi
      ;;
    installed)
      for agent in "${agents[@]}"; do
        local target_dir
        target_dir="$(resolve_target_dir "$agent" "$is_global" "$custom_path")" || continue
        dim "[$agent] $target_dir"
        cmd_installed "$target_dir"
      done
      ;;
    remove)
      for agent in "${agents[@]}"; do
        local target_dir
        target_dir="$(resolve_target_dir "$agent" "$is_global" "$custom_path")" || continue
        cmd_remove "$skill_name" "$target_dir" "$agent"
      done
      ;;
    update)
      for agent in "${agents[@]}"; do
        local target_dir
        target_dir="$(resolve_target_dir "$agent" "$is_global" "$custom_path")" || continue
        dim "[$agent] $target_dir"
        cmd_update "$target_dir" "$force" "$agent"
      done
      ;;
    push)
      for agent in "${agents[@]}"; do
        local target_dir
        target_dir="$(resolve_target_dir "$agent" "$is_global" "$custom_path")" || continue
        cmd_push "$target_dir" "$repo_url" "$agent" "$skill_name"
      done
      ;;
    *)
      err "Unknown command: $cmd"; usage; exit 1 ;;
  esac
}

main "$@"
