#!/bin/bash

###############################################################################
# git.sh – Git Worktree API Module
# ---------------------------------------------------------------------------
# This module provides a centralized API for all Git operations used by
# gw.sh, commands.sh, and gw-bridge.sh. It abstracts Git worktree management
# and other Git operations into a clean, consistent interface.
###############################################################################

# --------------------------- Repository Information -------------------------

# Get the Git repository root directory
git_repo_root() {
  git rev-parse --show-toplevel 2>/dev/null || true
}

# Check if current directory is in a Git repository
git_is_repo() {
  git rev-parse --git-dir >/dev/null 2>&1
}

# Get current absolute path (resolved)
git_current_path() {
  pwd -P
}

# Get the primary repository root (shared across all worktrees)
git_shared_root() {
  local common_dir
  common_dir="$(git rev-parse --git-common-dir 2>/dev/null)" || return 1
  if [[ -z "$common_dir" ]]; then
    return 1
  fi
  if [[ "$common_dir" != /* ]]; then
    common_dir="$(pwd -P)/$common_dir"
  fi
  dirname "$common_dir"
}

# Convert branch name to slug (replace / with -)
git_branch_to_slug() {
  echo "$1" | tr '/' '-'
}

# --------------------------- Branch Operations ------------------------------

# Check if a branch exists
git_branch_exists() {
  local branch_name="$1"
  git rev-parse --verify "$branch_name" >/dev/null 2>&1
}

# Get current branch name (or empty if detached HEAD)
git_current_branch() {
  git symbolic-ref --short HEAD 2>/dev/null || true
}

# Get current commit SHA (short format)
git_current_commit_short() {
  git rev-parse --short HEAD 2>/dev/null || true
}

# Delete a local branch (force delete)
git_delete_branch() {
  local branch_name="$1"
  git branch -D "$branch_name" 2>/dev/null || true
}

# --------------------------- Worktree Operations ----------------------------

# Ensure ignore rules for worktrees directory exist in both .gitignore and .git/info/exclude
# Non-destructive and idempotent; creates files if needed
git_ensure_ignore_worktrees() {
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1

  # .git/info/exclude (local-only)
  local exclude_file="$root/.git/info/exclude"
  mkdir -p "$root/.git/info" 2>/dev/null || true
  if [[ -f "$exclude_file" ]]; then
    if ! grep -q -E '^worktrees/(\s*)?$' "$exclude_file" 2>/dev/null; then
      echo 'worktrees/' >> "$exclude_file"
    fi
  else
    echo 'worktrees/' >> "$exclude_file"
  fi

  # Top-level .gitignore (shared)
  local gi_file="$root/.gitignore"
  if [[ -f "$gi_file" ]]; then
    if ! grep -q -E '^worktrees/(\s*)?$' "$gi_file" 2>/dev/null; then
      echo 'worktrees/' >> "$gi_file"
    fi
  else
    echo 'worktrees/' > "$gi_file"
  fi
}

# List all worktrees: returns lines with "path\tbranch\tsha_short"
git_list_worktrees() {
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1

  local current_path=""
  local current_branch=""
  local current_head=""

  while IFS= read -r line || [[ -n "$line" ]]; do
    case "$line" in
      worktree\ *)
        current_path="${line#worktree }"
        ;;
      HEAD\ *)
        current_head="${line#HEAD }"
        ;;
      branch\ *)
        current_branch="${line#branch }"
        if [[ "$current_branch" == refs/heads/* ]]; then
          current_branch="${current_branch#refs/heads/}"
        fi
        ;;
      *)
        ;;
    esac

    if [[ -z "$line" ]]; then
      if [[ -n "$current_path" ]]; then
        printf '%s\t%s\t%s\n' "$current_path" "$current_branch" "${current_head:0:7}"
      fi
      current_path=""
      current_branch=""
      current_head=""
    fi
  done < <(cd "$root" && git worktree list --porcelain)

  if [[ -n "$current_path" ]]; then
    printf '%s\t%s\t%s\n' "$current_path" "$current_branch" "${current_head:0:7}"
  fi
}

# Get branch name for a given worktree path
git_worktree_path_to_branch() {
  local path="$1"
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1

  local current_path=""
  local current_branch=""

  while IFS= read -r line || [[ -n "$line" ]]; do
    case "$line" in
      worktree\ *)
        current_path="${line#worktree }"
        ;;
      branch\ *)
        current_branch="${line#branch }"
        if [[ "$current_branch" == refs/heads/* ]]; then
          current_branch="${current_branch#refs/heads/}"
        fi
        ;;
      *)
        ;;
    esac

    if [[ -z "$line" ]]; then
      if [[ "$current_path" == "$path" ]]; then
        printf '%s\n' "$current_branch"
        return 0
      fi
      current_path=""
      current_branch=""
    fi
  done < <(cd "$root" && git worktree list --porcelain)

  return 1
}

# Get worktree path for a given branch name
git_branch_to_worktree_path() {
  local branch_name="$1"
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1

  local current_path=""
  local current_branch=""

  while IFS= read -r line || [[ -n "$line" ]]; do
    case "$line" in
      worktree\ *)
        current_path="${line#worktree }"
        ;;
      branch\ *)
        current_branch="${line#branch }"
        if [[ "$current_branch" == refs/heads/* ]]; then
          current_branch="${current_branch#refs/heads/}"
        fi
        ;;
      *)
        ;;
    esac

    if [[ -z "$line" ]]; then
      if [[ "$current_branch" == "$branch_name" ]]; then
        printf '%s\n' "$current_path"
        return 0
      fi
      current_path=""
      current_branch=""
    fi
  done < <(cd "$root" && git worktree list --porcelain)

  return 1
}

# Create a new worktree with a new branch
git_create_worktree_with_branch() {
  local branch_name="$1"
  local worktree_path="$2"
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1
  
  local relative_path="${worktree_path#$root/}"
  (cd "$root" && git worktree add "$relative_path" -b "$branch_name" >&2)
}

# Create a new worktree from existing branch
git_create_worktree_from_branch() {
  local branch_name="$1"
  local worktree_path="$2"
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1
  
  local relative_path="${worktree_path#$root/}"
  (cd "$root" && git worktree add "$relative_path" "$branch_name" >&2)
}

git_create_worktree_for_existing_branch() {
  local branch_name="$1"
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1

  git_branch_exists "$branch_name" || { echo "Branch does not exist: $branch_name" >&2; return 1; }

  git_ensure_ignore_worktrees >/dev/null 2>&1 || true

  local worktree_path; worktree_path="$(git_build_worktree_path "$branch_name")"
  mkdir -p "$(dirname "$worktree_path")"

  git_create_worktree_from_branch "$branch_name" "$worktree_path" || return 1

  echo "$worktree_path"
}

git_create_worktree_from_remote_branch() {
  local branch_name="$1"
  local remote_name="${2:-origin}"
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1

  git_ensure_ignore_worktrees >/dev/null 2>&1 || true

  local worktree_path; worktree_path="$(git_build_worktree_path "$branch_name")"
  mkdir -p "$(dirname "$worktree_path")"

  local relative_path="${worktree_path#$root/}"
  (cd "$root" && git worktree add "$relative_path" -b "$branch_name" "$remote_name/$branch_name" >&2) || return 1

  echo "$worktree_path"
}

# Remove a worktree (with force fallback)
git_remove_worktree() {
  local worktree_path="$1"
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1
  
  local relative_path="${worktree_path#$root/}"
  (cd "$root" && {
    git worktree remove "$relative_path" 2>/dev/null || \
    git worktree remove --force "$relative_path" 2>/dev/null || true
  })
}

# Get Git status for current directory
git_status() {
  git status "$@"
}

# Get short Git status for current directory
git_status_short() {
  git status --short
}

# --------------------------- Worktree Path Utilities ------------------------

# Build standard worktree path for a branch
git_build_worktree_path() {
  local branch_name="$1"
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1
  
  local slug; slug="$(git_branch_to_slug "$branch_name")"
  echo "$root/worktrees/$slug"
}

# Check if a worktree exists for a branch
git_worktree_exists_for_branch() {
  local branch_name="$1"
  local path; path="$(git_branch_to_worktree_path "$branch_name")"
  [[ -n "$path" ]]
}

# Get or create worktree path for a branch
git_ensure_worktree_for_branch() {
  local branch_name="$1"
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1
  
  # Check if worktree already exists
  local existing_path; existing_path="$(git_branch_to_worktree_path "$branch_name")"
  if [[ -n "$existing_path" ]]; then
    echo "$existing_path"
    return 0
  fi
  
  # Create worktree if branch exists
  if git_branch_exists "$branch_name"; then
    # Ensure ignores before creating worktree
    git_ensure_ignore_worktrees >/dev/null 2>&1 || true
    local worktree_path; worktree_path="$(git_build_worktree_path "$branch_name")"
    mkdir -p "$(dirname "$worktree_path")"
    git_create_worktree_from_branch "$branch_name" "$worktree_path"
    echo "$worktree_path"
    return 0
  fi
  
  return 1
}

# Create new branch and worktree
git_create_branch_and_worktree() {
  local branch_name="$1"
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && return 1
  
  # Ensure ignores before creating worktree
  git_ensure_ignore_worktrees >/dev/null 2>&1 || true
  local worktree_path; worktree_path="$(git_build_worktree_path "$branch_name")"
  mkdir -p "$(dirname "$worktree_path")"
  git_create_worktree_with_branch "$branch_name" "$worktree_path"
  echo "$worktree_path"
}

# --------------------------- Validation Functions ---------------------------

# Validate that we're in a Git repository
git_require_repo() {
  git_is_repo || { echo "❌ Not a git repository or git command not found." >&2; return 1; }
}

# Get repository root with validation
git_require_repo_root() {
  local root; root="$(git_repo_root)"
  [[ -z "$root" ]] && { echo "❌ Not a git repository" >&2; return 1; }
  echo "$root"
}

# --------------------------- Merge/Rebase Helpers ----------------------------

# Detect the primary branch name (default remote HEAD), fall back to common names
# Echoes the branch name on success; returns non-zero on failure
git_primary_branch() {
  local head_ref branch
  # Try origin/HEAD symbolic ref
  head_ref=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || true)
  if [[ -n "$head_ref" ]]; then
    # refs/remotes/origin/main -> extract last component
    branch="${head_ref##*/}"
    [[ -n "$branch" ]] && { echo "$branch"; return 0; }
  fi
  # Try parsing `git remote show origin`
  branch=$(git remote show origin 2>/dev/null | awk -F': ' '/HEAD branch/ {print $2; exit}')
  if [[ -n "$branch" && "$branch" != "(unknown)" ]]; then
    echo "$branch"; return 0
  fi
  # Fallback candidates
  for cand in main master trunk; do
    if git rev-parse --verify "$cand" >/dev/null 2>&1 || git rev-parse --verify "origin/$cand" >/dev/null 2>&1; then
      echo "$cand"; return 0
    fi
  done
  return 1
}

# Check if a worktree at path is clean (no changes)
# Usage: git_is_worktree_clean <path>
git_is_worktree_clean() {
  local path="$1"
  [[ -z "$path" ]] && return 1
  local out
  out=$(git -C "$path" status --porcelain 2>/dev/null || true)
  [[ -z "$out" ]]
}

# Fetch from origin with prune for a given path
git_fetch_prune() {
  local path="$1"
  git -C "$path" fetch origin --prune 2>&1
}

# Fast-forward-only pull for the currently checked out branch at path
# Returns non-zero if not a fast-forward
# Usage: git_pull_ff_only <path>
git_pull_ff_only() {
  local path="$1"
  git -C "$path" pull --ff-only 2>&1
}

# Merge a ref into the currently checked out branch at target path
# Usage: git_merge_into <target_path> <ref>
git_merge_into() {
  local target="$1"; local ref="$2"
  git -C "$target" merge "$ref" 2>&1
}

# Rebase the current branch at target path onto upstream ref
# Usage: git_rebase_onto <target_path> <upstream_ref>
git_rebase_onto() {
  local target="$1"; local upstream="$2"
  git -C "$target" rebase "$upstream" 2>&1
}

# Squash-merge branch into the primary worktree path, then commit if changes staged
# Usage: git_squash_merge_into <primary_path> <branch>
git_squash_merge_into() {
  local primary_path="$1"; local branch="$2"
  git -C "$primary_path" merge --squash "$branch" 2>&1
}

# Ensure a primary worktree exists; echoes the path
# Usage: git_ensure_primary_worktree
git_ensure_primary_worktree() {
  local primary
  primary=$(git_primary_branch) || return 1
  git_ensure_worktree_for_branch "$primary"
}
