#!/usr/bin/env bash
# DELTA_FLOW_TOWER_INTEGRATION_VERSION=1.0.2
#
# git difftool --dir-diff launcher for the Delta Flow VS Code extension.
#
# git invokes this once with the two temp trees:  delta-flow "$LOCAL" "$REMOTE"
# We open a VS Code window whose folder settings carry the two paths, then
# block (via --wait) until the user closes it, so git keeps the temp trees alive.
#
# Set $DELTA_FLOW_CODE to override the VS Code CLI location.

set -euo pipefail

if [[ $# -lt 2 ]]; then
  echo "usage: delta-flow <LEFT_DIR> <RIGHT_DIR>" >&2
  exit 2
fi

# Tower and other GUI clients run with a minimal PATH, so probe known locations.
find_code() {
  local candidate
  for candidate in "${DELTA_FLOW_CODE:-}" code /usr/local/bin/code \
    "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"; do
    if [[ -n "$candidate" ]] && command -v "$candidate" >/dev/null 2>&1; then
      echo "$candidate"
      return 0
    fi
  done
  echo "error: could not find the VS Code CLI; set DELTA_FLOW_CODE" >&2
  return 1
}

# The VS Code .app bundle behind the CLI, so `open -a` can reuse the running
# instance instead of `code` spawning a second one (a duplicate Dock icon).
find_code_app() {
  local cli path dir
  cli="$(command -v "$(find_code 2>/dev/null)" 2>/dev/null)" || return 1
  path="$cli"
  while [[ -h "$path" ]]; do
    dir="$(dirname "$path")"
    path="$(readlink "$path")"
    [[ "$path" != /* ]] && path="$dir/$path"
  done
  case "$path" in
    *.app/*) printf '%s' "${path%%.app/*}.app" ;;
    *) return 1 ;;
  esac
}

revision_label() {
  local repo="$1" revision="$2" ref
  for ref in $(git -C "$repo" for-each-ref --points-at="$revision" \
    --format='%(refname)' refs/heads refs/tags refs/remotes 2>/dev/null); do
    case "$ref" in
      refs/heads/*) echo "${ref#refs/heads/}"; return 0 ;;
    esac
  done
  for ref in $(git -C "$repo" for-each-ref --points-at="$revision" \
    --format='%(refname)' refs/tags refs/remotes 2>/dev/null); do
    case "$ref" in
      refs/tags/*) echo "${ref#refs/tags/}"; return 0 ;;
      refs/remotes/*/HEAD) ;;
      refs/remotes/*) echo "${ref#refs/remotes/}"; return 0 ;;
    esac
  done
  git -C "$repo" rev-parse --short=8 "$revision" 2>/dev/null
}

add_revision() {
  local repo="$1" candidate="$2" resolved
  candidate="${candidate#\'}"
  candidate="${candidate#\"}"
  candidate="${candidate%\'}"
  candidate="${candidate%\"}"
  resolved="$(git -C "$repo" rev-parse --verify --quiet "${candidate}^{commit}" 2>/dev/null || true)"
  if [[ -n "$resolved" && "$resolved" != "$inferred_left" && "$resolved" != "$inferred_right" ]]; then
    if [[ -z "$inferred_left" ]]; then
      inferred_left="$resolved"
    elif [[ -z "$inferred_right" ]]; then
      inferred_right="$resolved"
    fi
  fi
}

# Tower's public diff-tool contract only supplies two directories. Git's parent
# process still contains the compared revisions, so resolve those to friendly
# branch/tag names when available. This is best-effort and falls back cleanly.
infer_workspace_name() {
  local repo pid count command token range_left range_right root
  repo="${GIT_WORK_TREE:-$PWD}"
  if ! git -C "$repo" rev-parse --git-dir >/dev/null 2>&1; then
    echo "session"
    return
  fi

  inferred_left=""
  inferred_right=""
  pid="$PPID"
  count=0
  while [[ "$pid" =~ ^[0-9]+$ && "$pid" -gt 1 && "$count" -lt 8 && -z "$inferred_right" ]]; do
    command="$(ps -ww -o command= -p "$pid" 2>/dev/null || true)"
    if [[ "$command" == *difftool* && "$command" != *difftool--helper* ]]; then
      for token in $command; do
        if [[ "$token" == *...* ]]; then
          range_left="${token%%...*}"
          range_right="${token#*...}"
          add_revision "$repo" "$range_left"
          add_revision "$repo" "$range_right"
        elif [[ "$token" == *..* ]]; then
          range_left="${token%%..*}"
          range_right="${token#*..}"
          add_revision "$repo" "$range_left"
          add_revision "$repo" "$range_right"
        else
          add_revision "$repo" "$token"
        fi
        [[ -n "$inferred_right" ]] && break
      done
    fi
    pid="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ' || true)"
    count=$((count + 1))
  done

  if [[ -n "$inferred_left" && -n "$inferred_right" ]]; then
    echo "$(revision_label "$repo" "$inferred_left") ↔ $(revision_label "$repo" "$inferred_right")"
    return
  fi
  root="$(git -C "$repo" rev-parse --show-toplevel 2>/dev/null || true)"
  if [[ -n "$root" ]]; then
    echo "Delta Flow - ${root##*/}"
  else
    echo "session"
  fi
}

dir_owner() {
  stat -f '%u' "$1" 2>/dev/null || stat -c '%u' "$1" 2>/dev/null
}

# Create $1 as a private directory we own, or fail if anything else holds the
# path. The sessions root lives at a predictable location, so we refuse a
# pre-existing symlink or another user's directory as defence in depth.
ensure_owned_dir() {
  local dir="$1"
  if [[ -L "$dir" ]]; then
    echo "error: $dir is a symlink; refusing to use it" >&2
    return 1
  elif [[ ! -e "$dir" ]]; then
    mkdir -m 700 "$dir"
  elif [[ ! -d "$dir" ]]; then
    echo "error: $dir exists but is not a directory" >&2
    return 1
  elif [[ "$(dir_owner "$dir")" != "$(id -u)" ]]; then
    echo "error: $dir is not owned by you; refusing to use it" >&2
    return 1
  fi
}

# The per-user cache directory, so sessions live at a readable, conventional path
# (rather than an opaque temp path) that the user can recognise when trusting it.
cache_base() {
  case "$(uname -s)" in
    Darwin) echo "$HOME/Library/Caches" ;;
    *) echo "${XDG_CACHE_HOME:-$HOME/.cache}" ;;
  esac
}

# A stable per-user root the user can trust once; every session opens beneath
# it, so VS Code's inherited trust suppresses the Restricted Mode banner. Only
# the sessions/ child is meant to be trusted, keeping any siblings out of scope.
sessions_root() {
  local base root sessions
  base="$(cache_base)"
  mkdir -p "$base"
  root="$base/delta-flow"
  sessions="$root/sessions"
  ensure_owned_dir "$root" || return 1
  ensure_owned_dir "$sessions" || return 1
  echo "$sessions"
}

# Remove session anchors left behind by crashed or force-quit windows. The
# 3-day floor mirrors the OS temp reaping we used to rely on and stays clear of
# any window a user has legitimately left open.
sweep_stale_sessions() {
  find "$1" -mindepth 1 -maxdepth 1 -type d -mtime +3 -exec rm -rf {} + 2>/dev/null || true
}

# Without --dir-diff, git invokes us once per file. Fall back to a plain diff so
# this tool is safe to set as the global diff.tool.
open_dir_diff() {
  local left right sessions session workdir settings workspace_name
  left="$(cd "$1" && pwd)"
  right="$(cd "$2" && pwd)"

  sessions="$(sessions_root)"
  sweep_stale_sessions "$sessions"
  session="$(mktemp -d "$sessions/XXXXXX")"

  # Snapshot mode: the extension renders the comparison inside its own window,
  # so it only needs the two trees to outlive git's temporaries (freed the
  # moment this launcher returns). Copy them in, keep the session — the
  # extension removes it when the comparison closes — and report the folder.
  # No VS Code window is launched.
  if [[ -n "${DELTA_FLOW_SNAPSHOT_ONLY:-}" ]]; then
    cp -R "$left" "$session/left"
    cp -R "$right" "$session/right"
    printf 'DELTA_FLOW_SESSION_DIR=%s\n' "$session"
    return
  fi

  # A named empty folder anchors the window. Using a folder rather than a
  # .code-workspace file avoids VS Code's automatic "(Workspace)" suffix. It
  # lives under the stable, per-user sessions root so trust is inherited.
  workspace_name="${DELTA_FLOW_WORKSPACE_NAME:-$(infer_workspace_name)}"
  # The extension supplies a filesystem-safe name; also neutralise path
  # separators in case this launcher is called directly.
  workspace_name="${workspace_name//\//-}"
  workspace_name="${workspace_name//\\/-}"
  workdir="$session/$workspace_name"
  settings="$workdir/.vscode/settings.json"
  mkdir -p "$(dirname "$settings")"

  # On macOS, `open -a` reuses the running VS Code (one Dock icon) but cannot
  # wait, so copy the trees in and let the extension delete the session when the
  # window closes. Elsewhere, `code --wait` holds git's temporary trees open.
  local app=""
  if [[ "$(uname -s)" == Darwin ]]; then
    app="$(find_code_app || true)"
  fi
  if [[ -n "$app" ]]; then
    cp -R "$left" "$session/left"
    cp -R "$right" "$session/right"
    left="$session/left"
    right="$session/right"
  else
    # Expand $session now so cleanup works even though it is function-local.
    trap "rm -rf '$session'" EXIT
  fi

  # Folder-scoped settings carry the trees into the extension.
  cat > "$settings" <<EOF
{
  "deltaFlow.session": { "left": "$left", "right": "$right" },
  "workbench.startupEditor": "none",
  "explorer.openEditors.visible": 0,
  "workbench.editor.enablePreview": true,
  "files.exclude": { ".vscode": true, ".delta-flow": true }
}
EOF

  # Marker that scopes the extension's activationEvents to Delta Flow windows only.
  touch "$workdir/.delta-flow"

  if [[ -n "$app" ]] && open -a "$app" "$workdir"; then
    return
  fi
  code_bin="$(find_code)"
  "$code_bin" --new-window --wait "$workdir"
}

if [[ -d "$1" && -d "$2" ]]; then
  open_dir_diff "$1" "$2"
else
  code_bin="$(find_code)"
  "$code_bin" --diff --wait "$1" "$2"
fi
