#!/bin/bash

# omarchy:summary=Manage Omarchy shell plugins and bar widgets
# omarchy:group=plugin
# omarchy:args=<list|rescan|enable|disable|clone|edit|bar> [...]
# omarchy:examples=omarchy plugin list | omarchy plugin clone omarchy.clock local.clock --with ai | omarchy plugin edit local.clock --with editor | omarchy plugin enable acme.weather --section right | omarchy plugin bar settings

set -euo pipefail

CONFIG_FILE="$HOME/.config/omarchy/shell.json"

usage() {
  cat <<USAGE
Usage: omarchy-plugin <command> [args...]

Plugin commands:
  list [--json]                         List discovered shell plugins
  rescan                                Rescan ~/.config/omarchy/plugins
  enable <id> [placement]               Enable a plugin
  disable <id>                          Disable a plugin
  clone [source] [new-id] [options]      Clone a built-in or user plugin
  edit [id] [--with <ai|editor>] [--prompt <prompt>]
                                        Open a user plugin for editing

Add from sources (see 'omarchy plugin <command> --help'):
  source <add|list|remove|refresh>      Manage trusted plugin source repos
  available                             List plugins offered by your sources
  add [id] [--from <src>] [--enable] [--review] [--yes]
                                        Add a plugin from a trusted source
  update [id] [--all] [--review] [--yes]
                                        Update added plugins (shows a diff)
  remove [id] [--yes]                   Remove an installed plugin
  validate <plugin-folder>              Check a plugin's manifest (for authors)

  Source/add commands run their own binaries; the rest are handled here.
  Plugins are unsandboxed code — review what you add and enable.

Clone options:
  --name <name>                         Display name for the clone
  --with <ai|editor>                    Open the clone with AI or editor
  --prompt <prompt>                     Initial prompt when opening with AI
  --replace                             Replace the first source bar instance
  --add [placement]                     Add cloned bar widget to the bar
  --use                                 Use cloned bar options as the active bar

Bar widget commands:
  bar settings                          Open the inline bar config panel
  bar options [--json]                  List available bar options
  bar use <id>                          Use a bar option as the active bar
  bar reset                             Return to the built-in Omarchy bar
  bar list [--json]                     List current bar layout
  bar add <id> [placement] [--duplicate]
  bar move <id> [placement] [--from-section SECTION] [--from-index N]
  bar remove <id> [--section SECTION] [--all]
  bar remove --section SECTION --index N
  bar set <id> <key> <value> [--json] [--section SECTION] [--index N]

Placement options:
  --section <left|center|right>         Target bar section
  --index <n>                           Insert at index in the target section
  --before <id>                         Insert before the first matching widget
  --after <id>                          Insert after the first matching widget

Examples:
  omarchy plugin list
  omarchy plugin rescan
  omarchy plugin clone
  omarchy plugin clone omarchy.clock local.clock --name "My Clock" --replace --with ai --prompt "Customize this clock"
  omarchy plugin edit local.clock --with editor
  omarchy plugin edit local.clock --with ai --prompt "Customize this clock"
  omarchy plugin enable acme.weather --section right
  omarchy plugin bar options
  omarchy plugin bar use local.neon-bar
  omarchy plugin bar reset
  omarchy plugin bar add acme.weather --section right --before omarchy.tray
  omarchy plugin bar move omarchy.clock --section center --index 0
  omarchy plugin bar remove acme.weather
  omarchy plugin bar set omarchy.clock format HH:mm
  omarchy plugin bar set omarchy.indicators items '["Dnd","NightLight"]' --json
USAGE
}

fail() {
  echo "omarchy-plugin: $*" >&2
  exit 1
}

require_omarchy_path() {
  [[ -n ${OMARCHY_PATH:-} ]] || fail "OMARCHY_PATH is not set"
}

require_command() {
  local command_name="$1"
  command -v "$command_name" >/dev/null 2>&1 || fail "$command_name is required"
}

plugin_discovered() {
  local id="$1" plugins
  plugins=$(omarchy-shell shell listPlugins 2>/dev/null) || return 1
  jq -e --arg id "$id" 'any(.[]; .id == $id)' <<<"$plugins" >/dev/null 2>&1
}

wait_for_plugin_discovery() {
  local id="$1" attempt
  for (( attempt = 0; attempt < 40; attempt++ )); do
    plugin_discovered "$id" && return 0
    sleep 0.05
  done
  return 1
}

section_valid() {
  [[ $1 =~ ^(left|center|right)$ ]]
}

validate_section() {
  local section="$1"
  section_valid "$section" || fail "section must be left, center, or right"
}

validate_index() {
  local value="$1"
  [[ $value =~ ^[0-9]+$ ]] || fail "index must be a non-negative integer"
}

canonical_widget_id() {
  printf '%s' "$1"
}

validate_plugin_id() {
  local id="$1"
  [[ $id =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || fail "plugin id must contain only letters, numbers, dots, underscores, and dashes"
  [[ $id == *.* ]] || fail "plugin id must be namespaced, e.g. local.clock"
  [[ $id != omarchy.* ]] || fail "plugin ids beginning with omarchy. are reserved for built-ins"
  [[ $(canonical_widget_id "$id") == "$id" ]] || fail "$id is a reserved built-in widget id"
}

print_plugins() {
  local json="false"

  while (( $# > 0 )); do
    case "$1" in
    --json)
      json="true"
      ;;
    -h | --help)
      usage
      exit 0
      ;;
    *)
      fail "unknown list option: $1"
      ;;
    esac
    shift
  done

  local plugins
  plugins=$(omarchy-shell shell listPlugins) || fail "could not list plugins; is omarchy-shell running?"

  if [[ $json == "true" ]]; then
    printf '%s\n' "$plugins"
    return
  fi

  require_command jq
  jq -r '
    sort_by(.id)[] |
    [ .id,
      (if .enabled then "enabled" else "disabled" end),
      (if .firstParty then "first-party" else "third-party" end),
      ((.kinds // []) | join(",")),
      (.name // "")
    ] | @tsv
  ' <<<"$plugins" | awk -F '\t' '
    BEGIN { printf "%-32s %-9s %-11s %-18s %s\n", "ID", "STATE", "SOURCE", "KINDS", "NAME" }
    { printf "%-32s %-9s %-11s %-18s %s\n", $1, $2, $3, $4, $5 }
  '
}

rescan_plugins() {
  (( $# == 0 )) || fail "rescan does not take arguments"
  omarchy-shell shell rescanPlugins || fail "could not rescan plugins; is omarchy-shell running?"
  echo "Plugins rescanned"
}

plugin_enabled() {
  local enabled="$1"
  local id="${2:-}"
  [[ -n $id ]] || fail "plugin id is required"
  shift 2

  if [[ $enabled != "true" && $# -gt 0 ]]; then
    fail "disable does not take placement options"
  fi

  id=$(canonical_widget_id "$id")

  omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
  if [[ $enabled == "true" ]]; then
    require_command jq
    wait_for_plugin_discovery "$id" || fail "plugin '$id' was not discovered; is omarchy-shell running?"
  fi

  local result
  result=$(omarchy-shell shell setPluginEnabled "$id" "$enabled") || fail "could not update plugin; is omarchy-shell running?"
  [[ $result == "ok" ]] || fail "plugin '$id' is not known; run: omarchy plugin rescan"

  if [[ $enabled == "true" && $# -gt 0 ]]; then
    mutate_bar move --id "$id" "$@"
    echo "Enabled and moved $id"
  else
    if [[ $enabled == "true" ]]; then
      echo "Enabled $id"
    else
      echo "Disabled $id"
    fi
  fi
}

config_source() {
  require_omarchy_path
  local defaults_file="$OMARCHY_PATH/config/omarchy/shell.json"

  if [[ -s $CONFIG_FILE ]] && jq -e 'type == "object" and .version == 1' "$CONFIG_FILE" >/dev/null 2>&1; then
    printf '%s' "$CONFIG_FILE"
  elif [[ -s $defaults_file ]] && jq -e 'type == "object" and .version == 1' "$defaults_file" >/dev/null 2>&1; then
    printf '%s' "$defaults_file"
  else
    fail "could not find a valid shell config or defaults file"
  fi
}

bar_list() {
  local json="false"

  while (( $# > 0 )); do
    case "$1" in
    --json)
      json="true"
      ;;
    -h | --help)
      usage
      exit 0
      ;;
    *)
      fail "unknown bar list option: $1"
      ;;
    esac
    shift
  done

  require_command jq
  local source
  source=$(config_source)

  if [[ $json == "true" ]]; then
    jq '.bar.layout // { left: [], center: [], right: [] }' "$source"
    return
  fi

  jq -r '
    (.bar.layout // { left: [], center: [], right: [] }) as $layout |
    ["left", "center", "right"][] as $section |
    ($layout[$section] // []) as $entries |
    if ($entries | length) == 0 then
      "\($section)\t-\t-"
    else
      $entries | to_entries[] | "\($section)\t\(.key)\t\(.value.id // "<missing>")"
    end
  ' "$source" | awk '
    BEGIN { printf "%-8s %-5s %s\n", "SECTION", "INDEX", "WIDGET" }
    { printf "%-8s %-5s %s\n", $1, $2, $3 }
  '
}

mutate_bar() {
  require_command jq
  require_command python3
  require_omarchy_path

  local operation="$1"
  shift
  local id=""
  local section=""
  local index=""
  local before=""
  local after=""
  local from_section=""
  local from_index=""
  local duplicate="false"
  local all="false"
  local key=""
  local value=""
  local value_is_json="false"
  local new_id=""

  while (( $# > 0 )); do
    case "$1" in
    --id)
      id="${2-}"
      shift 2
      ;;
    --section)
      section="${2:-}"
      validate_section "$section"
      shift 2
      ;;
    --index)
      index="${2:-}"
      validate_index "$index"
      shift 2
      ;;
    --before)
      before="${2:-}"
      [[ -n $before ]] || fail "--before requires a widget id"
      shift 2
      ;;
    --after)
      after="${2:-}"
      [[ -n $after ]] || fail "--after requires a widget id"
      shift 2
      ;;
    --from-section)
      from_section="${2:-}"
      validate_section "$from_section"
      shift 2
      ;;
    --from-index)
      from_index="${2:-}"
      validate_index "$from_index"
      shift 2
      ;;
    --duplicate)
      duplicate="true"
      shift
      ;;
    --all)
      all="true"
      shift
      ;;
    --key)
      key="${2:-}"
      [[ -n $key ]] || fail "--key requires a value"
      shift 2
      ;;
    --new-id)
      new_id="${2:-}"
      [[ -n $new_id ]] || fail "--new-id requires a value"
      shift 2
      ;;
    --value)
      value="${2:-}"
      shift 2
      ;;
    --json)
      value_is_json="true"
      shift
      ;;
    *)
      fail "unknown option: $1"
      ;;
    esac
  done

  if [[ -n $before && -n $after ]]; then
    fail "use only one of --before or --after"
  fi

  local source tmp
  source=$(config_source)
  mkdir -p "$(dirname "$CONFIG_FILE")"
  tmp=$(mktemp)

  OPERATION="$operation" \
  WIDGET_ID="$id" \
  TARGET_SECTION="$section" \
  TARGET_INDEX="$index" \
  TARGET_BEFORE="$before" \
  TARGET_AFTER="$after" \
  FROM_SECTION="$from_section" \
  FROM_INDEX="$from_index" \
  ALLOW_DUPLICATE="$duplicate" \
  REMOVE_ALL="$all" \
  SET_KEY="$key" \
  SET_VALUE="$value" \
  SET_VALUE_IS_JSON="$value_is_json" \
  NEW_WIDGET_ID="$new_id" \
  python3 - "$source" >"$tmp" <<'PY'
import json
import os
import sys
from copy import deepcopy

source = sys.argv[1]
operation = os.environ["OPERATION"]
def canonical(value):
    return str(value or "")


widget_id = canonical(os.environ.get("WIDGET_ID", ""))
target_section = os.environ.get("TARGET_SECTION", "")
target_index = os.environ.get("TARGET_INDEX", "")
target_before = canonical(os.environ.get("TARGET_BEFORE", ""))
target_after = canonical(os.environ.get("TARGET_AFTER", ""))
from_section = os.environ.get("FROM_SECTION", "")
from_index = os.environ.get("FROM_INDEX", "")
allow_duplicate = os.environ.get("ALLOW_DUPLICATE") == "true"
remove_all = os.environ.get("REMOVE_ALL") == "true"
set_key = os.environ.get("SET_KEY", "")
set_value = os.environ.get("SET_VALUE", "")
set_value_is_json = os.environ.get("SET_VALUE_IS_JSON") == "true"
new_widget_id = os.environ.get("NEW_WIDGET_ID", "")
sections = ["left", "center", "right"]


def die(message):
    print(f"omarchy-plugin: {message}", file=sys.stderr)
    sys.exit(1)


def load_config(path):
    with open(path, encoding="utf-8") as handle:
        data = json.load(handle)
    if not isinstance(data, dict):
        die("shell config must be a JSON object")
    data = deepcopy(data)
    data["version"] = 1
    data.setdefault("bar", {})
    if not isinstance(data["bar"], dict):
        data["bar"] = {}
    data["bar"].setdefault("layout", {})
    if not isinstance(data["bar"]["layout"], dict):
        data["bar"]["layout"] = {}
    for section in sections:
        entries = data["bar"]["layout"].get(section)
        if not isinstance(entries, list):
            data["bar"]["layout"][section] = []
    data.setdefault("plugins", [])
    if not isinstance(data["plugins"], list):
        data["plugins"] = []
    return data


config = load_config(source)
layout = config["bar"]["layout"]


def find_entries(id_value=None, only_section=""):
    found = []
    for section in sections:
        if only_section and section != only_section:
            continue
        for index, entry in enumerate(layout[section]):
            if id_value is None or (isinstance(entry, dict) and canonical(entry.get("id")) == id_value):
                found.append((section, index, entry))
    return found


def entry_at(section, index):
    if section not in sections:
        die("section must be left, center, or right")
    if index < 0 or index >= len(layout[section]):
        die(f"no widget at {section}[{index}]")
    return layout[section][index]


def parse_index(value):
    if value == "":
        return None
    try:
        parsed = int(value)
    except ValueError:
        die("index must be a non-negative integer")
    if parsed < 0:
        die("index must be a non-negative integer")
    return parsed


def resolve_target(default_section="right"):
    if target_before or target_after:
        target_id = target_before or target_after
        matches = find_entries(target_id, target_section)
        if not matches:
            scope = f" in {target_section}" if target_section else ""
            die(f"could not find target widget {target_id}{scope}")
        section, index, _entry = matches[0]
        if target_after:
            index += 1
        return section, index

    section = target_section or default_section
    if section not in sections:
        die("section must be left, center, or right")
    index = parse_index(target_index)
    if index is None:
        index = len(layout[section])
    if index > len(layout[section]):
        index = len(layout[section])
    return section, index


def source_location():
    if from_index != "":
        if not from_section:
            die("--from-index requires --from-section")
        index = parse_index(from_index)
        entry = entry_at(from_section, index)
        if widget_id and (not isinstance(entry, dict) or canonical(entry.get("id")) != widget_id):
            die(f"widget at {from_section}[{index}] is not {widget_id}")
        return from_section, index, entry

    if not widget_id:
        die("widget id is required")
    matches = find_entries(widget_id, from_section)
    if not matches:
        scope = f" in {from_section}" if from_section else ""
        die(f"could not find widget {widget_id}{scope}")
    return matches[0]


if operation == "add":
    if not widget_id:
        die("widget id is required")
    if not allow_duplicate and find_entries(widget_id):
        die(f"{widget_id} is already in the bar; use 'bar move' or pass --duplicate")
    section, index = resolve_target("right")
    layout[section].insert(index, {"id": widget_id})

elif operation == "move":
    old_section, old_index, entry = source_location()
    entry = deepcopy(entry)
    if isinstance(entry, dict):
        entry["id"] = widget_id
    del layout[old_section][old_index]
    default_section = target_section or old_section
    section, index = resolve_target(default_section)
    layout[section].insert(index, entry)

elif operation == "remove":
    if from_index != "":
        old_section, old_index, _entry = source_location()
        del layout[old_section][old_index]
    elif target_index != "":
        if not target_section:
            die("--index requires --section for bar remove")
        index = parse_index(target_index)
        entry = entry_at(target_section, index)
        if widget_id and (not isinstance(entry, dict) or canonical(entry.get("id")) != widget_id):
            die(f"widget at {target_section}[{index}] is not {widget_id}")
        del layout[target_section][index]
    else:
        if not widget_id:
            die("widget id is required")
        matches = find_entries(widget_id, target_section)
        if not matches:
            scope = f" in {target_section}" if target_section else ""
            die(f"could not find widget {widget_id}{scope}")
        if remove_all:
            for section, index, _entry in reversed(matches):
                del layout[section][index]
        else:
            section, index, _entry = matches[0]
            del layout[section][index]

elif operation == "replace":
    if not widget_id:
        die("source widget id is required")
    if not new_widget_id:
        die("replacement widget id is required")
    section, index, entry = source_location()
    if not isinstance(entry, dict):
        die("widget entry must be an object")
    entry["id"] = new_widget_id
    layout[section][index] = entry

elif operation == "set":
    if not widget_id:
        die("widget id is required")
    if not set_key:
        die("setting key is required")
    if from_index != "":
        section, index, entry = source_location()
    elif target_index != "":
        if not target_section:
            die("--index requires --section for bar set")
        index = parse_index(target_index)
        entry = entry_at(target_section, index)
        if not isinstance(entry, dict) or canonical(entry.get("id")) != widget_id:
            die(f"widget at {target_section}[{index}] is not {widget_id}")
        section = target_section
    else:
        matches = find_entries(widget_id, target_section)
        if not matches:
            scope = f" in {target_section}" if target_section else ""
            die(f"could not find widget {widget_id}{scope}")
        section, index, entry = matches[0]
    if not isinstance(entry, dict):
        die("widget entry must be an object")
    if set_value_is_json:
        try:
            parsed_value = json.loads(set_value)
        except json.JSONDecodeError as error:
            die(f"invalid JSON value: {error}")
    else:
        parsed_value = set_value
    layout[section][index][set_key] = parsed_value

else:
    die(f"unknown operation: {operation}")

json.dump(config, sys.stdout, indent=2)
sys.stdout.write("\n")
PY

  mv "$tmp" "$CONFIG_FILE"
  omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
}

# Canonical ids of every known bar widget, read from manifests on disk
# (first-party + user). This is the same set `omarchy plugin list` reports, but
# read straight from disk so it works with the shell down and never races an
# in-flight rescan.
bar_widget_ids() {
  require_command jq
  first_party_manifest_paths | while IFS= read -r manifest; do
    jq -r 'select((.kinds // []) | index("bar-widget")) | .id' "$manifest" 2>/dev/null
  done
  local user_dir="$HOME/.config/omarchy/plugins"
  if [[ -d $user_dir ]]; then
    find "$user_dir" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) 2>/dev/null | while IFS= read -r dir; do
      [[ -f $dir/manifest.json ]] || continue
      jq -r 'select((.kinds // []) | index("bar-widget")) | .id' "$dir/manifest.json" 2>/dev/null
    done
  fi
}

bar_add() {
  local id="${1:-}"
  [[ -n $id ]] || fail "bar add requires a widget id"
  shift
  id=$(canonical_widget_id "$id")
  # Only let real bar widgets into the layout — a typo'd or non-widget id just
  # renders as a dead entry (this is how a stray "--help" once got in).
  if ! bar_widget_ids | grep -qxF -- "$id"; then
    fail "$id is not a known bar widget; run 'omarchy plugin list' to see valid ids"
  fi
  mutate_bar add --id "$id" "$@"
  echo "Added $id to the bar"
}

bar_move() {
  local id="${1:-}"
  [[ -n $id ]] || fail "bar move requires a widget id"
  shift
  mutate_bar move --id "$id" "$@"
  echo "Moved $id"
}

bar_remove() {
  local id=""
  if (( $# > 0 )) && [[ $1 != --* ]]; then
    id="$1"
    shift
  fi
  mutate_bar remove --id "$id" "$@"
  if [[ -n $id ]]; then
    echo "Removed $id from the bar"
  else
    echo "Removed bar widget"
  fi
}

bar_set() {
  local id="${1:-}"
  local key="${2:-}"
  local value="${3:-}"
  [[ -n $id ]] || fail "bar set requires a widget id"
  [[ -n $key ]] || fail "bar set requires a setting key"
  (( $# >= 3 )) || fail "bar set requires a value"
  shift 3
  mutate_bar set --id "$id" --key "$key" --value "$value" "$@"
  echo "Set $key on $(canonical_widget_id "$id")"
}

slug_id() {
  tr '[:upper:]' '[:lower:]' <<<"$1" | sed -E 's/^omarchy\.//; s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
}

first_party_manifest_paths() {
  require_omarchy_path
  find "$OMARCHY_PATH/shell/plugins" -mindepth 2 -maxdepth 3 -type f \( -name manifest.json -o -name '*.manifest.json' \) 2>/dev/null | sort
}

manifest_source_dir() {
  local manifest="$1"
  if [[ ${manifest##*/} == "manifest.json" ]]; then
    dirname -- "$manifest"
  else
    dirname -- "$manifest"
  fi
}

builtin_widget_info() {
  require_command jq
  local wanted
  wanted=$(canonical_widget_id "$1")

  local manifest id entry source_dir source_path name category multiple
  while IFS= read -r manifest; do
    if ! jq -e --arg id "$wanted" '
      (.id == $id or ((.barWidget.aliases // []) | index($id))) and
      ((.kinds // []) | index("bar-widget"))
    ' "$manifest" >/dev/null 2>&1; then
      continue
    fi

    id=$(jq -r '.id' "$manifest")
    entry=$(jq -r '.entryPoints.barWidget // empty' "$manifest")
    [[ -n $entry ]] || return 1
    source_dir=$(manifest_source_dir "$manifest")
    source_path="$source_dir/$entry"
    name=$(jq -r '.barWidget.displayName // .name // .id' "$manifest")
    category=$(jq -r '.barWidget.category // "Plugin"' "$manifest")
    multiple=$(jq -r 'if .barWidget.allowMultiple == true then "true" else "false" end' "$manifest")
    printf '%s\t%s\t%s\t%s\t%s\n' "$id" "$source_path" "$name" "$category" "$multiple"
    return 0
  done < <(first_party_manifest_paths)

  return 1
}

clone_source_options() {
  local id name
  first_party_manifest_paths | while IFS= read -r manifest; do
    id=$(jq -r '.id // empty' "$manifest" 2>/dev/null || true)
    name=$(jq -r '.barWidget.displayName // .name // .id // empty' "$manifest" 2>/dev/null || true)
    [[ -n $id ]] || continue
    if jq -e '(.kinds // []) | index("bar-widget")' "$manifest" >/dev/null 2>&1; then
      printf '%s\tBuilt-in widget\t%s\n' "$id" "${name:-$id}"
    else
      printf '%s\tBuilt-in plugin\t%s\n' "$id" "${name:-$id}"
    fi
  done

  if [[ -d $HOME/.config/omarchy/plugins ]]; then
    find "$HOME/.config/omarchy/plugins" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) 2>/dev/null | sort | while IFS= read -r dir; do
      [[ -f $dir/manifest.json ]] || continue
      id=$(jq -r '.id // empty' "$dir/manifest.json" 2>/dev/null || true)
      name=$(jq -r '.name // .id // empty' "$dir/manifest.json" 2>/dev/null || true)
      [[ -n $id ]] && printf '%s\tUser plugin\t%s\n' "$id" "${name:-$id}"
    done
  fi
}

choose_value() {
  local prompt="$1"
  shift
  if command -v gum >/dev/null 2>&1; then
    gum choose --header "$prompt" "$@"
  else
    printf '%s\n' "$@" | fzf --prompt "$prompt > "
  fi
}

input_value() {
  local prompt="$1"
  local value="${2:-}"
  if command -v gum >/dev/null 2>&1; then
    gum input --prompt "$prompt " --value "$value"
  else
    read -r -p "$prompt " value
    printf '%s' "$value"
  fi
}

choose_clone_source() {
  local selected
  if command -v gum >/dev/null 2>&1; then
    selected=$(clone_source_options | awk -F '\t' '{ printf "%-32s  %-15s  %s\n", $3, $2, $1 }' | gum filter --header "Clone plugin" --placeholder "Search built-in and user plugins..." --limit 1) || return 1
    awk '{ print $NF }' <<<"$selected"
  elif command -v fzf >/dev/null 2>&1; then
    selected=$(clone_source_options | fzf --delimiter=$'\t' --with-nth=1,2,3 --prompt "Clone plugin > ") || return 1
    cut -f1 <<<"$selected"
  else
    fail "gum or fzf is required for interactive clone selection"
  fi
}

edit_source_options() {
  if [[ -d $HOME/.config/omarchy/plugins ]]; then
    find "$HOME/.config/omarchy/plugins" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) 2>/dev/null | sort | while IFS= read -r dir; do
      [[ -f $dir/manifest.json ]] || continue
      id=$(jq -r '.id // empty' "$dir/manifest.json" 2>/dev/null || true)
      name=$(jq -r '.name // .id // empty' "$dir/manifest.json" 2>/dev/null || true)
      [[ -n $id ]] && printf '%s\t%s\t%s\n' "$id" "${name:-$id}" "$dir"
    done
  fi
}

choose_edit_source() {
  local selected
  if command -v gum >/dev/null 2>&1; then
    selected=$(edit_source_options | awk -F '\t' '{ printf "%-32s  %s\n", $2, $1 }' | gum filter --header "Edit plugin" --placeholder "Search user plugins..." --limit 1) || return 1
    awk '{ print $NF }' <<<"$selected"
  elif command -v fzf >/dev/null 2>&1; then
    selected=$(edit_source_options | fzf --delimiter=$'\t' --with-nth=2,1 --prompt "Edit plugin > ") || return 1
    cut -f1 <<<"$selected"
  else
    fail "gum or fzf is required for interactive plugin selection"
  fi
}

validate_user_plugin_dir() {
  local dir="$1"
  local requested_id="${2:-}"
  local root="$HOME/.config/omarchy/plugins"
  local dir_abs root_abs manifest_id

  require_command jq
  require_command python3
  [[ -d $dir ]] || fail "plugin directory not found: $dir"

  dir_abs=$(python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).expanduser().resolve(strict=False))' "$dir")
  root_abs=$(python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).expanduser().resolve(strict=False))' "$root")
  [[ $dir_abs == $root_abs/* ]] || fail "plugin edit only opens user plugins under $root"
  [[ -f $dir_abs/manifest.json ]] || fail "plugin directory missing manifest.json: $dir_abs"

  manifest_id=$(jq -r '.id // empty' "$dir_abs/manifest.json" 2>/dev/null || true)
  [[ -n $manifest_id ]] || fail "plugin manifest missing id: $dir_abs/manifest.json"
  [[ $manifest_id != omarchy.* && $(canonical_widget_id "$manifest_id") == $manifest_id ]] || fail "$manifest_id is built in; clone it first with: omarchy plugin clone $manifest_id local.$(slug_id "$manifest_id")"
  if [[ -n $requested_id && $manifest_id != $requested_id ]]; then
    fail "plugin id mismatch: requested $requested_id but manifest declares $manifest_id"
  fi

  printf '%s' "$dir_abs"
}

plugin_dir_for_id() {
  local id="$1"

  if [[ $id == */* ]]; then
    validate_user_plugin_dir "$id"
    return
  fi

  id=$(canonical_widget_id "$id")
  if builtin_widget_info "$id" >/dev/null 2>&1 || [[ $id == omarchy.* ]]; then
    fail "$id is built in; clone it first with: omarchy plugin clone $id local.$(slug_id "$id")"
  fi

  validate_user_plugin_dir "$HOME/.config/omarchy/plugins/$id" "$id"
}

open_plugin_dir() {
  local tool="$1"
  local dir="$2"
  local prompt="${3:-}"

  case "$tool" in
  "" | none)
    printf 'Plugin directory: %s\n' "$dir"
    return
    ;;
  ai)
    omarchy-launch-ai --path "$dir" --prompt "$prompt"
    ;;
  editor)
    omarchy-launch-editor "$dir"
    ;;
  *)
    fail "unknown edit target: $tool (use ai, editor, or none)"
    ;;
  esac
}

edit_command() {
  local id=""
  if (( $# > 0 )) && [[ $1 != --* ]]; then
    id="$1"
    shift
  fi

  local open_tool=""
  local prompt=""
  while (( $# > 0 )); do
    case "$1" in
    --with)
      open_tool="${2:-}"
      [[ -n $open_tool ]] || fail "--with requires a value"
      shift 2
      ;;
    --open)
      open_tool="${2:-}"
      [[ -n $open_tool ]] || fail "--open requires a value"
      shift 2
      ;;
    --prompt)
      prompt="${2:-}"
      shift 2
      ;;
    -h | --help)
      usage
      return
      ;;
    *)
      fail "unknown edit option: $1"
      ;;
    esac
  done

  if [[ -z $id ]]; then
    if [[ -t 0 && -t 1 ]]; then
      id=$(choose_edit_source) || fail "edit cancelled"
    else
      fail "plugin id is required"
    fi
  fi

  local dir
  dir=$(plugin_dir_for_id "$id")

  if [[ -z $open_tool ]]; then
    if [[ -t 0 && -t 1 ]]; then
      open_tool=$(choose_value "Edit with" ai editor)
    else
      open_tool="editor"
    fi
  fi

  open_plugin_dir "$open_tool" "$dir" "$prompt"
}

rewrite_cloned_qml() {
  local file="$1"
  local id="$2"
  local source_path="$3"
  python3 - "$file" "$id" "$source_path" <<'PY'
import pathlib
import re
import sys
path, plugin_id, source_path = sys.argv[1], sys.argv[2], pathlib.Path(sys.argv[3])
text = open(path, encoding="utf-8").read()
text = re.sub(r'moduleName:\s*"[^"]+"', f'moduleName: "{plugin_id}"', text, count=1)


def resolve_relative(match):
    rel = match.group(1)
    if rel.startswith(("../", "./")) or (not rel.startswith("/") and "://" not in rel):
        resolved = (source_path.parent / rel).resolve()
        return '"' + resolved.as_uri() + '"'
    return match.group(0)

text = re.sub(r'Qt\.resolvedUrl\("([^"]+)"\)', resolve_relative, text)

# Indicators dynamically loads sibling indicator components via string
# concatenation, so the simple Qt.resolvedUrl("literal") rewrite above cannot
# see it. Point the clone back at Omarchy's bundled indicator directory.
if source_path.name == "Indicators.qml":
    indicators_url = (source_path.parent.parent / "indicators").resolve().as_uri() + "/"
    text = text.replace(
        'Qt.resolvedUrl("../indicators/" + indicatorSlot.indicatorId + ".qml")',
        f'"{indicators_url}" + indicatorSlot.indicatorId + ".qml"',
    )

# Panel-backed clones should not register the same global IPC target as the
# built-in widget. Direct panel clones can disable Panel.manageIpc themselves;
# wrapper widgets such as Weather disable it on the loaded panel instance.
if "ipcTarget:" in text and "manageIpc:" not in text:
    text = text.replace("  id: root\n", "  id: root\n  manageIpc: false\n", 1)
if "function injectPanel()" in text and '"manageIpc" in target' not in text:
    text = text.replace(
        "    if (!target) return\n",
        "    if (!target) return\n    if (\"manageIpc\" in target) target.manageIpc = false\n",
        1,
    )

open(path, "w", encoding="utf-8").write(text)
PY
}

write_clone_manifest() {
  local file="$1"
  local id="$2"
  local name="$3"
  local description="$4"
  local category="$5"
  local multiple="$6"
  local source_id="$7"
  local source_path="$8"
  python3 - "$file" "$id" "$name" "$description" "$category" "$multiple" "$source_id" "$source_path" <<'PY'
import json
import sys
file, plugin_id, name, description, category, multiple, source_id, source_path = sys.argv[1:]
data = {
  "schemaVersion": 1,
  "id": plugin_id,
  "name": name,
  "version": "1.0.0",
  "author": "Local",
  "description": description,
  "kinds": ["bar-widget"],
  "entryPoints": {"barWidget": "Widget.qml"},
  "barWidget": {
    "displayName": name,
    "description": description,
    "category": category,
    "allowMultiple": multiple == "true"
  },
  "omarchy": {
    "clonedFrom": source_id,
    "clonedFromPath": source_path
  }
}
open(file, "w", encoding="utf-8").write(json.dumps(data, indent=2) + "\n")
PY
}

update_cloned_manifest() {
  local file="$1"
  local id="$2"
  local name="$3"
  local source_id="$4"
  local source_path="$5"
  python3 - "$file" "$id" "$name" "$source_id" "$source_path" <<'PY'
import json
import sys
file, plugin_id, name, source_id, source_path = sys.argv[1:]
with open(file, encoding="utf-8") as handle:
    data = json.load(handle)
data["id"] = plugin_id
data["name"] = name
if "barWidget" in data and isinstance(data["barWidget"], dict):
    data["barWidget"]["displayName"] = name
meta = data.get("omarchy") if isinstance(data.get("omarchy"), dict) else {}
meta.update({"clonedFrom": source_id, "clonedFromPath": source_path})
data["omarchy"] = meta
open(file, "w", encoding="utf-8").write(json.dumps(data, indent=2) + "\n")
PY
}

clone_command() {
  require_command jq
  require_command python3
  require_omarchy_path

  local source_id=""
  local new_id=""
  if (( $# > 0 )) && [[ $1 != --* ]]; then
    source_id="$1"
    shift
  fi
  if (( $# > 0 )) && [[ $1 != --* ]]; then
    new_id="$1"
    shift
  fi

  local display_name=""
  local open_tool=""
  local prompt=""
  local replace="false"
  local add="false"
  local use_bar="false"
  local placement=()

  while (( $# > 0 )); do
    case "$1" in
    --name)
      display_name="${2:-}"
      [[ -n $display_name ]] || fail "--name requires a value"
      shift 2
      ;;
    --with)
      open_tool="${2:-}"
      [[ -n $open_tool ]] || fail "--with requires a value"
      shift 2
      ;;
    --open)
      open_tool="${2:-}"
      [[ -n $open_tool ]] || fail "--open requires a value"
      shift 2
      ;;
    --prompt)
      prompt="${2:-}"
      shift 2
      ;;
    --replace)
      replace="true"
      shift
      ;;
    --add)
      add="true"
      shift
      ;;
    --use)
      use_bar="true"
      shift
      ;;
    --section | --index | --before | --after)
      placement+=("$1" "${2:-}")
      shift 2
      ;;
    -h | --help)
      usage
      return
      ;;
    *)
      fail "unknown clone option: $1"
      ;;
    esac
  done

  if [[ -z $source_id ]]; then
    if [[ -t 0 && -t 1 ]]; then
      source_id=$(choose_clone_source) || fail "clone cancelled"
    else
      fail "clone source is required"
    fi
  fi
  source_id=$(canonical_widget_id "$source_id")

  local default_slug
  default_slug=$(slug_id "$source_id")
  if [[ -z $new_id ]]; then
    if [[ -t 0 && -t 1 ]]; then
      new_id=$(input_value "New plugin id:" "local.$default_slug")
    else
      fail "new plugin id is required"
    fi
  fi
  validate_plugin_id "$new_id"

  local target_dir="$HOME/.config/omarchy/plugins/$new_id"
  [[ ! -e $target_dir && ! -L $target_dir ]] || fail "$target_dir already exists"

  local source_path=""
  local source_name=""
  local source_category="Plugin"
  local source_multiple="false"
  local source_type=""

  if IFS=$'\t' read -r _ source_path source_name source_category source_multiple < <(builtin_widget_info "$source_id"); then
    source_type="builtin-widget"
  elif [[ -f $HOME/.config/omarchy/plugins/$source_id/manifest.json ]]; then
    source_type="user-plugin"
    source_path="$HOME/.config/omarchy/plugins/$source_id"
    source_name=$(jq -r '.name // .id' "$source_path/manifest.json")
  else
    source_path=$(find "$OMARCHY_PATH/shell/plugins" -path '*/manifest.json' -print0 | xargs -0 jq -r --arg id "$source_id" 'select(.id == $id) | input_filename' 2>/dev/null | head -1 || true)
    if [[ -n $source_path ]]; then
      source_type="builtin-plugin"
      source_path=$(dirname "$source_path")
      source_name=$(jq -r '.name // .id' "$source_path/manifest.json")
    fi
  fi

  [[ -n $source_type ]] || fail "unknown clone source: $source_id"
  if [[ -z $display_name ]]; then
    if [[ -t 0 && -t 1 ]]; then
      display_name=$(input_value "Display name:" "${source_name:-$new_id}")
    else
      display_name="${source_name:-$new_id}"
    fi
  fi
  [[ -n $display_name ]] || fail "display name is required"

  mkdir -p "$target_dir"
  if [[ $source_type == "builtin-widget" ]]; then
    cp "$source_path" "$target_dir/Widget.qml"
    rewrite_cloned_qml "$target_dir/Widget.qml" "$new_id" "$source_path"
    write_clone_manifest "$target_dir/manifest.json" "$new_id" "$display_name" "Cloned from $source_id" "$source_category" "$source_multiple" "$source_id" "$source_path"
  else
    cp -aL "$source_path/." "$target_dir/"
    update_cloned_manifest "$target_dir/manifest.json" "$new_id" "$display_name" "$source_id" "$source_path"
  fi

  {
    printf '# %s\n\n' "$display_name"
    printf 'Cloned from `%s`.\n\n' "$source_id"
    printf 'Source: `%s`\n\n' "$source_path"
    printf 'The original remains built into Omarchy. Remove this plugin directory or swap\n'
    printf 'your bar entry back to `%s` whenever you want to return to the built-in.\n' "$source_id"
  } >"$target_dir/UPSTREAM.md"

  omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true

  if [[ $replace == "true" ]]; then
    mutate_bar replace --id "$source_id" --new-id "$new_id"
    echo "Cloned $source_id to $new_id and replaced the first bar instance"
  elif [[ $add == "true" ]]; then
    bar_add "$new_id" "${placement[@]}"
    echo "Cloned $source_id to $new_id"
  elif [[ $use_bar == "true" ]]; then
    omarchy-config-shell-bar use "$new_id"
    echo "Cloned $source_id to $new_id and selected it as the active bar option"
  else
    echo "Cloned $source_id to $new_id"
  fi

  if [[ -z $open_tool && -t 0 && -t 1 ]]; then
    open_tool=$(choose_value "Edit with" ai editor)
  fi
  if [[ -n $open_tool ]]; then
    edit_command "$new_id" --with "$open_tool" --prompt "$prompt"
  fi
}

bar_command() {
  local command="${1:-list}"
  [[ $# -gt 0 ]] && shift || true

  case "$command" in
  edit | settings)
    (( $# == 0 )) || fail "bar settings does not take arguments"
    exec omarchy-launch-bar-settings
    ;;
  options | option)
    exec omarchy-config-shell-bar options "$@"
    ;;
  use)
    exec omarchy-config-shell-bar use "$@"
    ;;
  reset)
    exec omarchy-config-shell-bar reset "$@"
    ;;
  list)
    bar_list "$@"
    ;;
  add)
    bar_add "$@"
    ;;
  move)
    bar_move "$@"
    ;;
  remove | rm)
    bar_remove "$@"
    ;;
  set)
    bar_set "$@"
    ;;
  -h | --help | help)
    usage
    ;;
  *)
    fail "unknown bar command: $command"
    ;;
  esac
}

command="${1:-}"
case "$command" in
list | ls)
  shift
  print_plugins "$@"
  ;;
rescan)
  shift
  rescan_plugins "$@"
  ;;
enable)
  shift
  plugin_enabled true "$@"
  ;;
disable)
  shift
  plugin_enabled false "$@"
  ;;
clone)
  shift
  clone_command "$@"
  ;;
edit)
  shift
  edit_command "$@"
  ;;
bar | widget | widgets)
  shift
  bar_command "$@"
  ;;
source | available | add | update | remove | validate)
  # These live in sibling binaries (omarchy-plugin-<command>). The `omarchy`
  # dispatcher normally routes straight to them; delegate here too so invoking
  # this base binary directly matches the commands its --help advertises.
  shift
  sibling="$(dirname -- "${BASH_SOURCE[0]}")/omarchy-plugin-$command"
  [[ -x $sibling ]] || fail "missing helper: omarchy-plugin-$command"
  exec "$sibling" "$@"
  ;;
-h | --help | help | "")
  usage
  ;;
*)
  fail "unknown command: $command"
  ;;
esac
