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

VERSION="0.2.2" # x-release-please-version

# git-harvest のブランドカラー #C0FF39 rgb(192, 255, 57)
BRAND_COLOR='192;255;57'

# カラー出力可否（非 TTY / NO_COLOR では無効）
USE_COLOR=false
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
  USE_COLOR=true
fi

# ---------------------------------------------------------------------------
# 表示ヘルパー
# ---------------------------------------------------------------------------

# ブランドカラー (truecolor)
hi() {
  if [ "$USE_COLOR" = true ]; then
    printf '\033[38;2;%sm%s\033[0m' "$BRAND_COLOR" "$1"
  else
    printf '%s' "$1"
  fi
}

# bold
bold() {
  if [ "$USE_COLOR" = true ]; then
    printf '\033[1m%s\033[0m' "$1"
  else
    printf '%s' "$1"
  fi
}

# dim（補助情報）
dim() {
  if [ "$USE_COLOR" = true ]; then
    printf '\033[2m%s\033[0m' "$1"
  else
    printf '%s' "$1"
  fi
}

# bold + ブランドカラー (wordmark 用)
brand() {
  if [ "$USE_COLOR" = true ]; then
    printf '\033[1;38;2;%sm%s\033[0m' "$BRAND_COLOR" "$1"
  else
    printf '%s' "$1"
  fi
}

# $HOME を ~ に置換してパスを短縮
relpath() {
  local p="$1"
  case "$p" in
    "$HOME") printf '~' ;;
    "$HOME"/*) printf '~%s' "${p#"$HOME"}" ;;
    *) printf '%s' "$p" ;;
  esac
}

# 現在時刻をミリ秒で取得（portable: bash 5+ / python3 / perl / date の順）
now_ms() {
  if [ -n "${EPOCHREALTIME:-}" ]; then
    awk -v t="$EPOCHREALTIME" 'BEGIN{split(t,p,"."); printf "%d", p[1]*1000 + substr(p[2] "000",1,3)}'
  elif command -v python3 >/dev/null 2>&1; then
    python3 -c 'import time; print(int(time.time()*1000))' 2>/dev/null
  elif command -v perl >/dev/null 2>&1; then
    perl -MTime::HiRes -E 'printf("%d", Time::HiRes::time()*1000)' 2>/dev/null
  else
    echo $(($(date +%s) * 1000))
  fi
}

# 経過時間を "X.Ys" 形式で（start_ms を受け取り、現在時刻との差を返す）
elapsed_str() {
  local start_ms=$1 end_ms
  end_ms=$(now_ms)
  local diff=$((end_ms - start_ms))
  local s=$((diff / 1000))
  local ds=$(((diff % 1000) / 100))
  printf '%d.%ds' "$s" "$ds"
}

# ブランドカラー単色のロゴ
print_logo() {
  local lines=(
    ' \|/                     \|/'
    '\\|//  ~~~~~~~~~~~~~~~  \\|//'
    ' \|/        G I T        \|/'
    '  |     H A R V E S T     |'
    ' _|_______________________|_'
  )
  printf '\n'
  for line in "${lines[@]}"; do
    if [ "$USE_COLOR" = true ]; then
      printf '\033[38;2;%sm%s\033[0m\n' "$BRAND_COLOR" "$line"
    else
      printf '%s\n' "$line"
    fi
  done
  printf '\n'
}

# ブランド wordmark を表示
wordmark() {
  printf '\n%s\n' "$(brand 'git harvest')"
}

# ✓ で removed 表示
print_removed() {
  local item="$1"
  printf '  %s  %s\n' "$(hi ✓)" "$item"
}

# → で would-remove 表示
print_will_remove() {
  local item="$1"
  printf '  %s  %s\n' "$(hi →)" "$item"
}

# · で growing 表示（保護理由付き、path と reason の間に最低 2 spaces 確保）
print_growing() {
  local item="$1" reason="$2"
  local pad=$((38 - ${#item}))
  [ $pad -lt 2 ] && pad=2
  if [ "$USE_COLOR" = true ]; then
    printf '  \033[2m·  %s%*s%s\033[0m\n' "$item" "$pad" '' "$reason"
  else
    printf '  ·  %s%*s%s\n' "$item" "$pad" '' "$reason"
  fi
}

# ---------------------------------------------------------------------------
# 引数パース
# ---------------------------------------------------------------------------

DRY_RUN=false
DELETE_ALL=false

while [ $# -gt 0 ]; do
  case "$1" in
  -v | --version)
    echo "git-harvest v$VERSION"
    exit 0
    ;;
  logo)
    print_logo
    exit 0
    ;;
  -n | --dry-run)
    DRY_RUN=true
    ;;
  --all)
    DELETE_ALL=true
    ;;
  -h | --help)
    cat <<'HELP'
git-harvest - Clean up 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
  --all          Delete all branches and worktrees except the default branch
                 (also force-removes locked worktrees; normally locked ones are kept)

Subcommands:
  logo           Show the git-harvest logo
HELP
    exit 0
    ;;
  *)
    echo "Unknown option: $1" >&2
    exit 1
    ;;
  esac
  shift
done

# ---------------------------------------------------------------------------
# git / Claude 連携
# ---------------------------------------------------------------------------

# リモートのデフォルトブランチ名を取得する (例: 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
}

# Claude CLI が走行中セッションを記録するディレクトリ。
# 上書き用 env: GIT_HARVEST_CLAUDE_SESSIONS_DIR (テスト / power user 用)
claude_sessions_dir() {
  echo "${GIT_HARVEST_CLAUDE_SESSIONS_DIR:-$HOME/.claude/sessions}"
}

# パスを canonical (symlink 解決済み) に正規化する。
# git は worktree path を canonical で保持するが、JSON 内の cwd / worktreePath は
# ユーザーが指定したまま記録される可能性があるため、両者を同一表記に揃える必要がある。
# (例: macOS の /var/folders は /private/var/folders への symlink)
canonical_path() {
  local p="$1"
  [ -z "$p" ] && return
  if [ -d "$p" ]; then
    (cd "$p" 2>/dev/null && pwd -P) 2>/dev/null || echo "$p"
  else
    echo "$p"
  fi
}

# 走行中の claude プロセスがこの worktree で動いているか (0=あり, 1=なし)。
# ~/.claude/sessions/<pid>.json の cwd が canonical 一致し、pid が生存していれば true。
has_running_claude_session() {
  local wt="$1"
  local sessions_dir
  sessions_dir=$(claude_sessions_dir)
  [ -d "$sessions_dir" ] || return 1

  local wt_canon
  wt_canon=$(canonical_path "$wt")
  [ -z "$wt_canon" ] && return 1

  local f cwd cwd_canon pid
  for f in "$sessions_dir"/*.json; do
    [ -f "$f" ] || continue
    # JSON は通常 single-line だが、pretty-print された場合にも備えて key/value 間の
    # whitespace を許容する
    cwd=$(sed -nE 's/.*"cwd"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p' "$f" | head -1)
    [ -z "$cwd" ] && continue
    cwd_canon=$(canonical_path "$cwd")
    [ "$cwd_canon" = "$wt_canon" ] || continue
    pid=$(sed -nE 's/.*"pid"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$f" | head -1)
    [ -z "$pid" ] && continue
    if kill -0 "$pid" 2>/dev/null; then
      return 0
    fi
  done
  return 1
}

# worktree が Claude Code 管理下のパス (.claude/worktrees/) 配下にあるか (0=あり, 1=なし)。
# EnterWorktree / 既存 Claude Code CLI が作る worktree はすべてこのパス規約に従う。
# 走行中セッションが無い場合、このパス配下の worktree は積極的な harvest 対象とする。
# `?*` で末尾に最低1文字を要求し、`.claude/worktrees/` 自体や `.claude/worktrees`
# (末尾スラッシュなし) を誤って managed と判定して --force 削除するのを防ぐ。
is_claude_managed_worktree() {
  local wt="$1"
  case "$wt" in
    */.claude/worktrees/?*) return 0 ;;
    *) return 1 ;;
  esac
}

# worktree が `git worktree lock` でロックされているか (0=あり, 1=なし)。
# lock は「このツリーは触るな」というユーザーの明示的な意思表示。通常モードでは最上位の
# 保護シグナルとし、--all のみ `-f -f` で貫通する。
# `git worktree list --porcelain` はロックされたツリーのブロックに `locked`
# (理由付きなら `locked <reason>`) 行を出すので、対象 worktree のブロックを見る。
# パスにスペースが含まれても壊れないよう、`worktree ` 以降を substr で丸ごと取る。
is_locked_worktree() {
  local wt="$1"
  git worktree list --porcelain | awk -v target="$wt" '
    /^worktree / { cur = substr($0, 10) }
    /^locked/    { if (cur == target) found = 1 }
    END          { exit found ? 0 : 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%s\n' "$(bold Worktrees)"
      found=true
    fi

    local rel
    rel=$(relpath "$wt")

    # 判定順 (README の表と一致):
    #   1. --all                       → 強制削除 (-f -f, lock も貫通)
    #   2. locked                      → 保護 "locked" (git worktree lock)
    #   3. running session             → 保護 "session running"
    #   4. .claude/worktrees/ 配下     → --force 削除 (path-regime)
    #   --- 以下は fallback (.claude/worktrees/ 外の従来挙動) ---
    #   5. merged + uncommitted        → 保護 "uncommitted changes"
    #   6. merged + clean              → 削除
    #   7. no unique commits           → 保護 "no unique commits"
    #   8. それ以外                    → 保護 "not merged"
    if [ "$DELETE_ALL" = true ]; then
      # --all: 全保護判定 (lock 含む) をスキップして worktree を削除する。
      # locked worktree は `-f -f` でないと消せないため二重 force を使う。
      # lock を破った場合は痕跡として "(was locked)" を表示する。
      local label="$rel"
      if is_locked_worktree "$wt"; then label="$rel (was locked)"; fi
      if [ "$DRY_RUN" = true ]; then
        print_will_remove "$label"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      else
        git worktree remove --force --force "$wt"
        print_removed "$label"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      fi
    elif is_locked_worktree "$wt"; then
      # git worktree lock による明示的保護。--all 以外では最優先で保護する。
      print_growing "$rel" "locked"
    elif has_running_claude_session "$wt"; then
      # 走行中の Claude CLI セッション (~/.claude/sessions/<pid>.json) で保護
      print_growing "$rel" "session running"
    elif is_claude_managed_worktree "$wt"; then
      # .claude/worktrees/ 配下で active な Claude セッションが無いもの = harvest 対象
      # uncommitted や未マージ commits があっても --force で削除する
      # (commits は branch ref に残り、cleanup_branches で保護される)
      if [ "$DRY_RUN" = true ]; then
        print_will_remove "$rel"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      else
        git worktree remove --force "$wt"
        print_removed "$rel"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      fi
    elif echo "$merged_branches" | grep -qxF "$branch"; then
      # fallback (.claude/worktrees/ 外): マージ済み + 変更なし → 削除、変更あり → 保護
      if has_uncommitted_changes "$wt"; then
        print_growing "$rel" "uncommitted changes"
      elif [ "$DRY_RUN" = true ]; then
        print_will_remove "$rel"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      else
        git worktree remove "$wt"
        print_removed "$rel"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      fi
    elif echo "$no_unique_branches" | grep -qxF "$branch"; then
      print_growing "$rel" "no unique commits"
    else
      print_growing "$rel" "not merged"
    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

  # ブランチ削除では独自コミットなしも削除対象（worktree がなければ残骸）
  local deletable_branches="$merged_branches"
  if [ -n "$no_unique_branches" ]; then
    deletable_branches="${deletable_branches:+$deletable_branches
}$no_unique_branches"
  fi

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

    if [ "$found" = false ]; then
      printf '\n%s\n' "$(bold Branches)"
      found=true
    fi

    if [ "$DELETE_ALL" = true ]; then
      # --all: 全ブランチを削除対象にする（事前チェック済みなのでチェックアウト中は dry-run のみ）
      if [ "$DRY_RUN" = true ]; then
        print_will_remove "$branch"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      else
        git branch -D "$branch" >/dev/null 2>&1
        print_removed "$branch"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      fi
    elif echo "$deletable_branches" | grep -qxF "$branch"; then
      # 削除対象: チェックアウト中かチェック
      if is_current_head "$branch" || is_checked_out_in_worktree "$branch"; then
        print_growing "$branch" "currently checked out"
      elif [ "$DRY_RUN" = true ]; then
        print_will_remove "$branch"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      else
        git branch -D "$branch" >/dev/null 2>&1
        print_removed "$branch"
        DELETED_COUNT=$((DELETED_COUNT + 1))
      fi
    else
      print_growing "$branch" "not merged"
    fi
  done < <(git branch | sed 's/^[*+ ]*//' | grep -v '^(')
  # リモートで削除済みの追跡ブランチを整理
  if [ "$DRY_RUN" = false ]; then git fetch --prune >/dev/null 2>&1 || true; fi
}

# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------

main() {
  local start_ms
  start_ms=$(now_ms)

  local base
  base=$(default_branch)

  # --all の事前チェック: デフォルトブランチ以外をチェックアウト中ならエラー終了
  if [ "$DELETE_ALL" = true ] && [ "$DRY_RUN" = false ]; then
    local current
    current=$(git symbolic-ref --short HEAD 2>/dev/null) || true
    if [ -n "$current" ] && [ "$current" != "$base" ]; then
      printf "\nError: Cannot delete branch '%s' (currently checked out)\n" "$current" >&2
      printf "Run: git checkout %s && git-harvest --all\n\n" "$base" >&2
      exit 1
    fi
  fi

  wordmark

  if [ "$DRY_RUN" = true ]; then
    printf '\n%s\n' "$(dim 'Dry run mode - nothing will be deleted')"
  fi

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

  local merged_branches=""
  local no_unique_branches=""

  # --all の場合はマージ検出をスキップ（全ブランチが削除対象）
  if [ "$DELETE_ALL" = false ]; then
    # デフォルトブランチにマージ済みのローカルブランチ一覧を取得
    # 検出手段: 1) first-parent 上のコミット一致、2) ancestor チェック、
    # 3) 仮想 squash + git cherry、4) patch-id ベースの cherry-pick フォールバック
    local first_parent_commits
    first_parent_commits=$(git rev-list --first-parent "$base" 2>/dev/null) || true

    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 で比較
      # merge-base が見つからない場合（orphaned ブランチ）はスキップして cherry-pick フォールバックへ
      local merge_base
      merge_base=$(git merge-base "$base" "$branch" 2>/dev/null) || true
      if [ -n "$merge_base" ]; then
        local squash_commit
        squash_commit=$(git commit-tree "$branch^{tree}" -p "$merge_base" -m "_" 2>/dev/null) || true
        if [ -n "$squash_commit" ]; then
          local cherry_out
          cherry_out=$(git cherry "$base" "$squash_commit" 2>/dev/null) || true
          if [ -n "$cherry_out" ] && [ "$(echo "$cherry_out" | grep -c '^+')" -eq 0 ]; then
            merged_branches="${merged_branches:+$merged_branches
}$branch"
            continue
          fi
        fi
      fi
      # cherry-pick フォールバック: patch-id ベースで全コミットを比較
      # orphaned ブランチ（履歴書き換えで共通祖先なし）や仮想 squash が一致しないケースをカバー
      local cherry_pick_unique
      cherry_pick_unique=$(git log --cherry-pick --right-only --no-merges --oneline "$base"..."$branch" 2>/dev/null)
      if [ -z "$cherry_pick_unique" ]; then
        merged_branches="${merged_branches:+$merged_branches
}$branch"
      fi
    done <<<"$all_branches"
  fi

  # デフォルトブランチのみ（他にブランチも 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 '\n%s\n\n' "$(dim '· Nothing to harvest. All clean.')"
  else
    # worktree が参照中のブランチは削除できないため、worktree を先に削除する
    DELETED_COUNT=0
    cleanup_worktrees "$base" "$merged_branches" "$no_unique_branches"
    cleanup_branches "$base" "$merged_branches" "$no_unique_branches"

    # サマリー表示
    printf '\n'
    if [ "$DELETED_COUNT" -gt 0 ]; then
      local item_word elapsed
      if [ "$DELETED_COUNT" -eq 1 ]; then item_word="item"; else item_word="items"; fi
      elapsed=$(elapsed_str "$start_ms")
      if [ "$DRY_RUN" = true ]; then
        printf '%s %s  %s\n\n' \
          "$(hi →)" \
          "$(bold "Would harvest $DELETED_COUNT $item_word")" \
          "$(dim "in $elapsed")"
      else
        printf '%s %s  %s\n\n' \
          "$(hi ✓)" \
          "$(bold "Harvested $DELETED_COUNT $item_word")" \
          "$(dim "in $elapsed")"
      fi
    else
      printf '%s\n\n' "$(dim '· Nothing to harvest. All growing.')"
    fi
  fi
}

main
