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

VERSION="0.1.13" # x-release-please-version
REPO="nozomiishii/git-harvest"
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/git-harvest"
CACHE_FILE="$CACHE_DIR/latest-version"
CHECK_INTERVAL=86400 # 24時間（秒）

# アニメーション中の中断でカーソルが非表示のままにならないよう、EXIT で復元
# 非TTY環境では何もしない（stdout を汚さない）
trap '[ -t 1 ] && printf "\033[?25h" 2>/dev/null || true' EXIT

# カラー出力の設定（非TTY / NO_COLOR 環境では無効化）
USE_COLOR=false
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
  USE_COLOR=true
  BOLD='\033[1m'
  GREEN='\033[32m'
  DIM='\033[2m'
  RESET='\033[0m'
else
  BOLD='' GREEN='' DIM='' RESET=''
fi

# HSL (hue 0-360) から ANSI truecolor エスケープを生成
# 彩度・明度は固定 (S=80%, L=65%) でレインボー向けに最適化
hsl_fg() {
  local h=$1
  # HSL to RGB (簡易版: 6セクターに分けて線形補間)
  local s=80 l=65
  local c=$(((100 - (2 * l - 100 > 0 ? 2 * l - 100 : 100 - 2 * l)) * s / 100))
  local h_section=$((h / 60))
  local h_remainder=$((h % 60))
  local x=$((c * (60 - (h_remainder > 30 ? h_remainder * 2 - 60 : 60 - h_remainder * 2)) / 60))
  local m=$(((l * 255 / 100) - (c * 255 / 200)))
  local r g b
  case $h_section in
  0) r=$((c * 255 / 100 + m)) g=$((x * 255 / 100 + m)) b=$m ;;
  1) r=$((x * 255 / 100 + m)) g=$((c * 255 / 100 + m)) b=$m ;;
  2) r=$m g=$((c * 255 / 100 + m)) b=$((x * 255 / 100 + m)) ;;
  3) r=$m g=$((x * 255 / 100 + m)) b=$((c * 255 / 100 + m)) ;;
  4) r=$((x * 255 / 100 + m)) g=$m b=$((c * 255 / 100 + m)) ;;
  *) r=$((c * 255 / 100 + m)) g=$m b=$((x * 255 / 100 + m)) ;;
  esac
  # clamp 0-255
  if [ "$r" -lt 0 ]; then r=0; elif [ "$r" -gt 255 ]; then r=255; fi
  if [ "$g" -lt 0 ]; then g=0; elif [ "$g" -gt 255 ]; then g=255; fi
  if [ "$b" -lt 0 ]; then b=0; elif [ "$b" -gt 255 ]; then b=255; fi
  printf '\033[38;2;%d;%d;%dm' "$r" "$g" "$b"
}

# テキストをレインボーアニメーションで表示 (TTY時のみ)
rainbow_animate() {
  local text="$1"
  local len=${#text}
  if [ "$USE_COLOR" = false ] || [ "$len" -eq 0 ]; then
    printf '  %s' "$text"
    return
  fi
  # カーソルを非表示
  printf '\033[?25l'
  local frames=20
  local frame=0
  while [ "$frame" -lt "$frames" ]; do
    printf '\r  '
    local i=0
    local offset=$((frame * 18))
    while [ "$i" -lt "$len" ]; do
      local char="${text:$i:1}"
      if [ "$char" = ' ' ]; then
        printf ' '
      else
        local hue=$(((i * 360 / len - offset + 3600) % 360))
        printf '%s%s' "$(hsl_fg "$hue")" "$char"
      fi
      i=$((i + 1))
    done
    printf '\033[0m'
    sleep 0.04
    frame=$((frame + 1))
  done
  # カーソルを再表示
  printf '\033[?25h'
}

# テキストを緑ベースでキラキラ光らせるアニメーション (TTY時のみ)
sparkle_animate() {
  local text="$1"
  local len=${#text}
  if [ "$USE_COLOR" = false ] || [ "$len" -eq 0 ]; then
    printf '  %s' "$text"
    return
  fi
  printf '\033[?25l'
  local frames=25
  local frame=0
  while [ "$frame" -lt "$frames" ]; do
    printf '\r  '
    local i=0
    while [ "$i" -lt "$len" ]; do
      local char="${text:$i:1}"
      if [ "$char" = ' ' ]; then
        printf ' '
      else
        # ランダムに明度を変える (40-100%)、一部の文字を白に近づけて「キラッ」
        local rand=$((RANDOM % 100))
        local lightness
        if [ "$rand" -lt 10 ]; then
          # 10% の確率で白に近い光 (明度 85-95%)
          lightness=$((85 + RANDOM % 11))
        elif [ "$rand" -lt 25 ]; then
          # 15% の確率で明るい緑 (明度 60-75%)
          lightness=$((60 + RANDOM % 16))
        else
          # 通常の緑 (明度 35-50%)
          lightness=$((35 + RANDOM % 16))
        fi
        # 緑 (hue=120) 固定、彩度80%、明度可変
        local s=80 l=$lightness
        local c=$(((100 - (2 * l - 100 > 0 ? 2 * l - 100 : 100 - 2 * l)) * s / 100))
        local x=0
        local m=$(((l * 255 / 100) - (c * 255 / 200)))
        local r=$m g=$((c * 255 / 100 + m)) b=$m
        if [ "$r" -lt 0 ]; then r=0; elif [ "$r" -gt 255 ]; then r=255; fi
        if [ "$g" -lt 0 ]; then g=0; elif [ "$g" -gt 255 ]; then g=255; fi
        if [ "$b" -lt 0 ]; then b=0; elif [ "$b" -gt 255 ]; then b=255; fi
        printf '\033[38;2;%d;%d;%dm%s' "$r" "$g" "$b" "$char"
      fi
      i=$((i + 1))
    done
    printf '\033[0m'
    sleep 0.05
    frame=$((frame + 1))
  done
  printf '\033[?25h'
}

# ---------------------------------------------------------------------------
# バージョン更新チェック
# ---------------------------------------------------------------------------

# インストール経路を判定し、自動更新チェックの対象かどうかを返す
# Homebrew / npm 経由のインストールはパッケージマネージャーに任せる
should_check_update() {
  local resolved
  resolved=$(realpath "$0" 2>/dev/null || echo "$0")
  case "$resolved" in
    */Cellar/*|*/homebrew/*) return 1 ;;
    */node_modules/*)        return 1 ;;
  esac
  return 0
}

# バックグラウンドで GitHub API から最新バージョンを取得しキャッシュに保存
check_update_async() {
  should_check_update || return 0

  # TTL チェック: キャッシュが新しければスキップ
  if [ -f "$CACHE_FILE" ]; then
    local last_check now
    last_check=$(stat -c %Y "$CACHE_FILE" 2>/dev/null \
              || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0)
    now=$(date +%s)
    if (( now - last_check < CHECK_INTERVAL )); then
      return 0
    fi
  fi

  mkdir -p "$CACHE_DIR"
  # バックグラウンドで取得（メイン処理をブロックしない）
  (
    local tag
    tag=$(curl -fsSL --max-time 5 \
      "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null \
      | grep -o '"tag_name":"[^"]*"' | head -1 \
      | sed 's/"tag_name":"v\{0,1\}\([^"]*\)"/\1/')
    if [ -n "$tag" ]; then
      echo "$tag" > "$CACHE_FILE"
    fi
  ) &
  UPDATE_CHECK_PID=$!
}

# メイン処理完了後に呼び出し、新しいバージョンがあれば stderr に通知
show_update_notification() {
  should_check_update || return 0
  [ -f "$CACHE_FILE" ] || return 0

  local latest
  latest=$(cat "$CACHE_FILE" 2>/dev/null)
  [ -z "$latest" ] && return 0
  [ "$latest" = "$VERSION" ] && return 0

  cat >&2 <<EOF

  Update available: v${VERSION} -> v${latest}
  Run: git-harvest --update

EOF
}

# 自分自身を最新版に置き換える
self_update() {
  local install_path
  install_path=$(realpath "$0" 2>/dev/null || echo "$0")

  # Homebrew / npm 経由の場合はパッケージマネージャーを案内
  case "$install_path" in
    */Cellar/*|*/homebrew/*)
      echo "Installed via Homebrew. Please update with:" >&2
      echo "  brew upgrade git-harvest" >&2
      exit 1
      ;;
    */node_modules/*)
      echo "Installed via npm. Please update with:" >&2
      echo "  npm update -g git-harvest" >&2
      exit 1
      ;;
  esac

  # 書き込み権限チェック
  if ! [ -w "$install_path" ]; then
    echo "Error: No write permission for $install_path" >&2
    echo "  Retry with: sudo git-harvest --update" >&2
    exit 1
  fi

  echo "Updating git-harvest..."
  local tmp
  tmp=$(mktemp)
  # 最新リリースをダウンロードして置き換え
  if curl -fsSL --max-time 30 \
    "https://github.com/${REPO}/releases/latest/download/git-harvest" \
    -o "$tmp" 2>/dev/null; then
    chmod +x "$tmp"
    mv "$tmp" "$install_path"
    rm -f "$CACHE_FILE"
    local new_version
    new_version=$("$install_path" --version)
    echo "Updated to ${new_version}"
  else
    rm -f "$tmp"
    echo "Error: Download failed" >&2
    exit 1
  fi
  exit 0
}

DRY_RUN=false

case "${1:-}" in
-v | --version)
  echo "git-harvest v$VERSION"
  exit 0
  ;;
--update)
  self_update
  ;;
-n | --dry-run)
  DRY_RUN=true
  ;;
-h | --help)
  cat <<'HELP'
git-harvest - Clean up merged branches and worktrees

Usage: git-harvest [options]

Options:
  -h, --help     Show this help
  -v, --version  Show version
  -n, --dry-run  Show what would be deleted without actually deleting
  --update       Update to the latest version
HELP
  exit 0
  ;;
esac

# リモートのデフォルトブランチ名を取得する (例: main, master)
# 未設定の場合はリモートから自動取得を試みる
default_branch() {
  local branch
  branch=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')

  # 未設定の場合、リモートに問い合わせて自動設定
  if [ -z "$branch" ]; then
    git remote set-head origin --auto >/dev/null 2>&1
    branch=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')
  fi

  if [ -z "$branch" ]; then
    echo "Could not determine default branch" >&2
    exit 1
  fi

  echo "$branch"
}

# worktree に未コミットの変更があるかチェックする (0=あり, 1=なし)
has_uncommitted_changes() {
  local wt="$1"
  if ! git -C "$wt" diff --quiet HEAD 2>/dev/null; then return 0; fi
  if ! git -C "$wt" diff --quiet --cached 2>/dev/null; then return 0; fi
  if [ -n "$(git -C "$wt" ls-files --others --exclude-standard 2>/dev/null)" ]; then return 0; fi
  return 1
}

# worktree の状態を表示・削除する (DELETED_COUNT をインクリメント)
cleanup_worktrees() {
  local base="$1"
  local merged_branches="$2"
  local no_unique_branches="$3"
  local found=false

  # メインワーキングツリーのパスを取得
  local main_wt
  main_wt=$(git worktree list --porcelain | grep --color=never -m1 '^worktree ' | sed 's/^worktree //')

  # 全 worktree のパスを取得してループ
  while read -r wt; do
    # メインワーキングツリーは表示しない
    [ "$wt" = "$main_wt" ] && continue
    # その worktree が指しているブランチ名を取得
    local branch
    branch=$(git -C "$wt" rev-parse --abbrev-ref HEAD 2>/dev/null) || continue
    # デフォルトブランチの worktree は表示しない
    [ "$branch" = "$base" ] && continue

    if [ "$found" = false ]; then
      printf "\n  Worktrees\n"
      found=true
    fi

    if echo "$merged_branches" | grep -qxF "$branch"; then
      # マージ済み: 未コミット変更があるかチェック
      if has_uncommitted_changes "$wt"; then
        printf "    [GROWING]      %s (uncommitted changes)\n" "$wt"
      elif [ "$DRY_RUN" = true ]; then
        printf "    [WILL DELETE]  %s\n" "$wt"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      else
        git worktree remove "$wt"
        printf "    [DELETED]      %s\n" "$wt"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      fi
    elif echo "$no_unique_branches" | grep -qxF "$branch"; then
      printf "    [GROWING]      %s (no unique commits)\n" "$wt"
    else
      printf "    [GROWING]      %s (not merged)\n" "$wt"
    fi
  done < <(git worktree list --porcelain | grep --color=never '^worktree ' | sed 's/^worktree //')
  # 既に存在しない worktree の管理情報を削除
  if [ "$DRY_RUN" = false ]; then git worktree prune; fi
}

# ブランチが worktree にチェックアウトされているかチェックする
is_checked_out_in_worktree() {
  local branch="$1"
  git worktree list --porcelain | grep -q "^branch refs/heads/${branch}$"
}

# ブランチが現在の HEAD かチェックする
is_current_head() {
  local branch="$1"
  local current
  current=$(git symbolic-ref --short HEAD 2>/dev/null) || return 1
  [ "$branch" = "$current" ]
}

# ブランチの状態を表示・削除する (DELETED_COUNT をインクリメント)
cleanup_branches() {
  local base="$1"
  local merged_branches="$2"
  local no_unique_branches="$3"
  local found=false

  while read -r branch; do
    [ -z "$branch" ] && continue
    [ "$branch" = "$base" ] && continue

    if [ "$found" = false ]; then
      printf "\n  Branches\n"
      found=true
    fi

    if echo "$merged_branches" | grep -qxF "$branch"; then
      # マージ済み: チェックアウト中かチェック
      if is_current_head "$branch" || is_checked_out_in_worktree "$branch"; then
        printf "    [GROWING]      %s (currently checked out)\n" "$branch"
      elif [ "$DRY_RUN" = true ]; then
        printf "    [WILL DELETE]  %s\n" "$branch"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      else
        git branch -D "$branch" >/dev/null 2>&1
        printf "    [DELETED]      %s\n" "$branch"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      fi
    elif echo "$no_unique_branches" | grep -qxF "$branch"; then
      printf "    [GROWING]      %s (no unique commits)\n" "$branch"
    else
      printf "    [GROWING]      %s (not merged)\n" "$branch"
    fi
  done < <(git branch | sed 's/^[*+ ]*//')
  # リモートで削除済みの追跡ブランチを整理
  if [ "$DRY_RUN" = false ]; then git fetch --prune >/dev/null 2>&1 || true; fi
}

main() {
  # バックグラウンドでバージョンチェックを開始
  UPDATE_CHECK_PID=""
  check_update_async

  local base
  base=$(default_branch)

  if [ "$DRY_RUN" = true ]; then
    printf "\n  Dry run mode - nothing will be deleted\n"
  fi
  printf '\n'
  sparkle_animate "git harvest"
  printf '\n'

  # デフォルトブランチにマージ済みのローカルブランチ一覧を取得
  # git branch --merged は squash merge を検出できないため、
  # commit-tree で仮想 squash コミットを作成し git cherry で比較する
  # base の first-parent 上のコミット一覧（独自コミットなしブランチの判定に使用）
  local first_parent_commits
  first_parent_commits=$(git rev-list --first-parent "$base" 2>/dev/null) || true

  local all_branches
  all_branches=$(git branch | sed 's/^[*+ ]*//')

  local merged_branches=""
  local no_unique_branches=""

  while read -r branch; do
    [ "$branch" = "$base" ] && continue
    # 独自コミットがないブランチ（作成直後でまだ作業していないブランチ）
    # ブランチ HEAD が base の first-parent 上にある場合、独自の作業がないと判断
    local branch_head
    branch_head=$(git rev-parse "$branch" 2>/dev/null) || continue
    if echo "$first_parent_commits" | grep -q "^${branch_head}$"; then
      no_unique_branches="${no_unique_branches:+$no_unique_branches
}$branch"
      continue
    fi
    # 通常マージ: ブランチが base の祖先であれば完全マージ済み
    if git merge-base --is-ancestor "$branch" "$base" 2>/dev/null; then
      merged_branches="${merged_branches:+$merged_branches
}$branch"
      continue
    fi
    # squash マージ: commit-tree で仮想 squash コミットを作成し git cherry で比較
    local merge_base
    merge_base=$(git merge-base "$base" "$branch" 2>/dev/null) || continue
    local squash_commit
    squash_commit=$(git commit-tree "$branch^{tree}" -p "$merge_base" -m "_" 2>/dev/null) || continue
    local cherry_out
    cherry_out=$(git cherry "$base" "$squash_commit" 2>/dev/null) || continue
    if [ -n "$cherry_out" ] && [ "$(echo "$cherry_out" | grep -c '^+')" -eq 0 ]; then
      merged_branches="${merged_branches:+$merged_branches
}$branch"
    fi
  done <<<"$all_branches"

  # デフォルトブランチのみ（他にブランチも linked worktree もない）場合
  local linked_wt_count
  linked_wt_count=$(git worktree list --porcelain | grep -c '^worktree ' || true)
  linked_wt_count=$((linked_wt_count - 1)) # メインワーキングツリーを除外

  local other_branch_count=0
  while read -r b; do
    [ -n "$b" ] && [ "$b" != "$base" ] && other_branch_count=$((other_branch_count + 1))
  done <<<"$all_branches"

  if [ "$other_branch_count" -eq 0 ] && [ "$linked_wt_count" -eq 0 ]; then
    printf "  Nothing to harvest. All clean!\n\n"
  else
    # worktree が参照中のブランチは削除できないため、worktree を先に削除する
    DELETED_COUNT=0
    cleanup_worktrees "$base" "$merged_branches" "$no_unique_branches"
    cleanup_branches "$base" "$merged_branches" "$no_unique_branches"

    # サマリー表示
    printf "\n\n"
    if [ "$DELETED_COUNT" -gt 0 ]; then
      rainbow_animate "Harvested!"
    else
      printf "  Nothing to harvest. All growing!"
    fi
    printf '\n\n\n'
  fi

  # バックグラウンドのバージョンチェックを待機して通知表示
  if [ -n "$UPDATE_CHECK_PID" ]; then
    wait "$UPDATE_CHECK_PID" 2>/dev/null || true
  fi
  show_update_notification
}

main
