#!/usr/bin/env bash
# tm.sh - textmarks: terminal companion to tm.html
# Reads/writes the exact same plaintext format:
#   Name > https://example.com #tag1 #tag2
#
# Zero-dependency, POSIX-ish bash. Works on macOS/Linux/NetBSD/BSD.
# Same file a tm.html "Open File" / "Save File" round-trips with tm.sh.

set -u

FILE="${TM_FILE:-bookmarks.txt}"

usage() {
  cat <<'EOF'
[ textmarks - text based bookmarks manager (cli) ]

usage: tm.sh [-f file] <command> [args]

commands:
  add <name> <link> [tags...]   add an entry (tags w/ or w/o leading #)
  list [filter]                 list all entries, or filter by text/tag
  open <n|name>                 open entry n (from list) or first name match
  rm <n|name>                   remove entry n (from list) or first name match
  edit                          open the file in $EDITOR
  new                           start a fresh empty file (overwrites!)
  cat                           print raw file contents

  -f <file>   use this file instead of $TM_FILE / ./bookmarks.txt

format:
  Name > https://example.com #tag1 #tag2

examples:
  tm.sh add GitHub https://github.com dev code repo
  tm.sh list dev
  tm.sh open 2
  tm.sh rm GitHub
EOF
  exit "${1:-0}"
}

# --- portable in-place sed (GNU vs BSD) ---
sed_i() {
  # sed_i 'script' file
  if sed --version >/dev/null 2>&1; then
    sed -i "$1" "$2"          # GNU sed
  else
    sed -i '' "$1" "$2"       # BSD/macOS/NetBSD sed
  fi
}

# --- pick a browser opener for this OS ---
open_url() {
  url="$1"
  if command -v open >/dev/null 2>&1; then
    open "$url"                        # macOS
  elif command -v xdg-open >/dev/null 2>&1; then
    xdg-open "$url" >/dev/null 2>&1    # Linux
  elif command -v sensible-browser >/dev/null 2>&1; then
    sensible-browser "$url"            # Debian fallback
  else
    echo "$url"                        # nothing found, just print it
  fi
}

ensure_file() {
  [ -f "$FILE" ] || : > "$FILE"
}

# normalize a space/comma list of tags into '#tag #tag' form
norm_tags() {
  out=""
  for t in "$@"; do
    t="${t#,}"; t="${t%,}"
    [ -z "$t" ] && continue
    case "$t" in
      \#*) ;;
      *) t="#$t" ;;
    esac
    out="$out $t"
  done
  printf '%s' "${out# }"
}

cmd_add() {
  name="$1"; link="$2"; shift 2 2>/dev/null
  [ -z "${name:-}" ] || [ -z "${link:-}" ] && { echo "usage: tm.sh add <name> <link> [tags...]" >&2; exit 1; }
  tags="$(norm_tags "$@")"
  ensure_file
  if [ -n "$tags" ]; then
    printf '%s > %s %s\n' "$name" "$link" "$tags" >> "$FILE"
  else
    printf '%s > %s\n' "$name" "$link" >> "$FILE"
  fi
  echo "added: $name"
}

# print numbered list, optionally filtered (case-insensitive substring, any field)
cmd_list() {
  ensure_file
  query="${1:-}"
  n=0
  while IFS= read -r line; do
    case "$line" in
      *'>'*) ;;
      *) continue ;;
    esac
    n=$((n+1))
    if [ -n "$query" ]; then
      printf '%s' "$line" | grep -iq -- "$query" || continue
    fi
    printf '%3d  %s\n' "$n" "$line"
  done < "$FILE"
}

# resolve an arg (index or name substring) to a raw line number in FILE
resolve_line() {
  key="$1"
  case "$key" in
    ''|*[!0-9]*)
      # name match: first '>'-line whose name field matches
      grep -n '>' "$FILE" | while IFS=: read -r ln rest; do
        name="${rest%%>*}"
        name="$(printf '%s' "$name" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
        case "$name" in
          *"$key"*) echo "$ln"; break ;;
        esac
      done
      ;;
    *)
      # numeric index into the filtered entry list -> map to real file line
      grep -n '>' "$FILE" | sed -n "${key}p" | cut -d: -f1
      ;;
  esac
}

cmd_open() {
  [ -z "${1:-}" ] && { echo "usage: tm.sh open <n|name>" >&2; exit 1; }
  ensure_file
  ln="$(resolve_line "$1")"
  [ -z "$ln" ] && { echo "not found: $1" >&2; exit 1; }
  line="$(sed -n "${ln}p" "$FILE")"
  rest="${line#*>}"
  link="$(printf '%s' "$rest" | awk '{print $1}')"
  echo "opening: $link"
  open_url "$link"
}

cmd_rm() {
  [ -z "${1:-}" ] && { echo "usage: tm.sh rm <n|name>" >&2; exit 1; }
  ensure_file
  ln="$(resolve_line "$1")"
  [ -z "$ln" ] && { echo "not found: $1" >&2; exit 1; }
  removed="$(sed -n "${ln}p" "$FILE")"
  sed_i "${ln}d" "$FILE"
  echo "removed: $removed"
}

cmd_edit() {
  ensure_file
  "${EDITOR:-vi}" "$FILE"
}

cmd_new() {
  printf '' > "$FILE"
  echo "new file: $FILE"
}

cmd_cat() {
  ensure_file
  cat "$FILE"
}

# --- arg parsing ---
while [ $# -gt 0 ]; do
  case "$1" in
    -f) FILE="$2"; shift 2 ;;
    -h|--help) usage 0 ;;
    *) break ;;
  esac
done

[ $# -eq 0 ] && usage 1

cmd="$1"; shift
case "$cmd" in
  add)  cmd_add "$@" ;;
  list|ls) cmd_list "$@" ;;
  open) cmd_open "$@" ;;
  rm|del) cmd_rm "$@" ;;
  edit) cmd_edit ;;
  new)  cmd_new ;;
  cat)  cmd_cat ;;
  -h|--help) usage 0 ;;
  *) echo "unknown command: $cmd" >&2; usage 1 ;;
esac
