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

# secrets — encrypted env file sync between machines
# Uses age key-file encryption + a private git repo.

SECRETS_DIR="${SECRETS_DIR:-$HOME/.secrets}"
KEY_FILE="$SECRETS_DIR/key.txt"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# ─── Helpers ───────────────────────────────────────────────────────────

die() { echo "ERROR: $*" >&2; exit 1; }
info() { echo "==> $*"; }

check_cmd() {
  command -v "$1" >/dev/null 2>&1 || die "'$1' is not installed. Run: brew install $1"
}

check_initialized() {
  [ -d "$SECRETS_DIR/.git" ] || die "Not initialized. Run: secrets init"
}

check_key() {
  [ -f "$KEY_FILE" ] || die "Key file not found at $KEY_FILE. Run: secrets init"
}

get_pubkey() {
  age-keygen -y "$KEY_FILE" 2>/dev/null || die "Failed to derive public key from $KEY_FILE"
}

derive_project_name() {
  local explicit="${1:-}"
  if [ -n "$explicit" ]; then
    echo "$explicit"
    return
  fi
  # Try git remote basename
  local remote
  remote=$(git config --get remote.origin.url 2>/dev/null || true)
  if [ -n "$remote" ]; then
    basename "$remote" .git
    return
  fi
  # Fall back to current directory name
  basename "$PWD"
}

install_hook() {
  local hook_src="$SCRIPT_DIR/hooks/pre-commit"
  local hook_dst="$SECRETS_DIR/.git/hooks/pre-commit"
  if [ -f "$hook_src" ]; then
    cp "$hook_src" "$hook_dst"
    chmod +x "$hook_dst"
  else
    # Inline hook if template not found (e.g. secrets installed standalone)
    cat > "$hook_dst" << 'HOOKEOF'
#!/usr/bin/env bash
BLOCKED=$(git diff --cached --name-only | grep -E '\.env' | grep -v '\.age$' || true)
if [ -n "$BLOCKED" ]; then
  echo "ERROR: Plaintext env files staged for commit:"
  echo "$BLOCKED"
  echo "Only .age (encrypted) files should be committed."
  exit 1
fi
HOOKEOF
    chmod +x "$hook_dst"
  fi
}

# ─── Subcommands ───────────────────────────────────────────────────────

cmd_init() {
  check_cmd age
  check_cmd git

  if [ -d "$SECRETS_DIR/.git" ]; then
    die "Already initialized at $SECRETS_DIR. Key file preserved."
  fi

  info "Initializing secrets repo at $SECRETS_DIR"
  mkdir -p "$SECRETS_DIR"
  git init "$SECRETS_DIR" >/dev/null

  # Generate age key pair
  info "Generating age key pair"
  age-keygen -o "$KEY_FILE" 2>&1

  # Write .gitignore
  cat > "$SECRETS_DIR/.gitignore" << 'EOF'
# Never commit the private key
key.txt

# Block plaintext env files
**/.env
**/.env.*

# Allow encrypted env files
!**/.env.age
!**/.env.*.age
EOF

  # Install pre-commit hook
  mkdir -p "$SECRETS_DIR/.git/hooks"
  install_hook

  local pubkey
  pubkey=$(get_pubkey)

  info "Done! Your public key is:"
  echo "  $pubkey"
  echo ""
  echo "Next steps:"
  echo "  1. Add a remote:  cd $SECRETS_DIR && git remote add origin <url>"
  echo "  2. Copy $KEY_FILE to your other machine (AirDrop, scp, USB)"
  echo "  3. Run 'secrets push <project>' from a project directory"
}

cmd_push() {
  check_cmd age
  check_cmd git
  check_initialized
  check_key

  local project
  project=$(derive_project_name "${1:-}")
  info "Pushing secrets for project: $project"

  # Glob .env and .env.* (not .envrc, .environment-*, etc.)
  local files=()
  for f in "$PWD"/.env "$PWD"/.env.*; do
    [ -f "$f" ] || continue
    local basename_f
    basename_f=$(basename "$f")
    # Skip patterns that aren't actual .env files
    case "$basename_f" in
      .envrc|.environment*) continue ;;
    esac
    files+=("$f")
  done

  if [ ${#files[@]} -eq 0 ]; then
    die "No .env or .env.* files found in $PWD"
  fi

  info "Files to encrypt:"
  for f in "${files[@]}"; do
    echo "  $(basename "$f")"
  done

  local pubkey
  pubkey=$(get_pubkey)

  # Encrypt each file
  mkdir -p "$SECRETS_DIR/$project"
  for f in "${files[@]}"; do
    local name
    name=$(basename "$f")
    age -r "$pubkey" -o "$SECRETS_DIR/$project/${name}.age" "$f"
  done

  # Pull before push (ff-only)
  if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
    if ! git -C "$SECRETS_DIR" pull --ff-only 2>/dev/null; then
      die "Fast-forward pull failed. Run 'secrets pull $project' first, then retry push."
    fi
  fi

  # Commit and push
  git -C "$SECRETS_DIR" add "$project/"
  if git -C "$SECRETS_DIR" diff --cached --quiet 2>/dev/null; then
    info "No changes to push (secrets unchanged)"
    return
  fi
  git -C "$SECRETS_DIR" commit -m "update $project" >/dev/null
  if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
    git -C "$SECRETS_DIR" push >/dev/null 2>&1
    info "Pushed $project to remote"
  else
    info "Committed $project locally (no remote configured)"
  fi
}

cmd_pull() {
  check_cmd age
  check_cmd git
  check_initialized
  check_key

  local project
  project=$(derive_project_name "${1:-}")
  local target_dir="$PWD"
  info "Pulling secrets for project: $project"

  # Pull latest
  if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
    git -C "$SECRETS_DIR" pull >/dev/null 2>&1
  fi

  # Check project exists
  if [ ! -d "$SECRETS_DIR/$project" ]; then
    die "Project '$project' not found. Run: secrets list"
  fi

  # Decrypt each .age file into target dir (including dotfiles)
  local count=0
  for f in "$SECRETS_DIR/$project"/*.age "$SECRETS_DIR/$project"/.*.age; do
    [ -f "$f" ] || continue
    local name
    name=$(basename "$f" .age)
    local outfile="$target_dir/$name"
    age -d -i "$KEY_FILE" -o "$outfile" "$f"
    # Integrity check: verify non-empty
    if [ ! -s "$outfile" ]; then
      echo "WARNING: Decrypted file '$name' is empty (possibly truncated .age blob)"
    fi
    count=$((count + 1))
  done

  info "Decrypted $count file(s) into $target_dir"

  # Reinstall hook if missing
  if [ ! -x "$SECRETS_DIR/.git/hooks/pre-commit" ]; then
    install_hook
    info "Reinstalled pre-commit hook"
  fi
}

cmd_list() {
  check_initialized

  local found=0
  for dir in "$SECRETS_DIR"/*/; do
    [ -d "$dir" ] || continue
    local project
    project=$(basename "$dir")
    # Skip hidden dirs
    [[ "$project" == .* ]] && continue
    echo "$project:"
    for f in "$dir"*.age "$dir".*.age; do
      [ -f "$f" ] || continue
      echo "  $(basename "$f" .age)"
      found=1
    done
  done

  if [ "$found" -eq 0 ]; then
    echo "No projects found. Run 'secrets push <project>' to add one."
  fi
}

cmd_rm() {
  check_cmd git
  check_initialized

  local project="${1:-}"
  [ -n "$project" ] || die "Usage: secrets rm <project>"

  if [ ! -d "$SECRETS_DIR/$project" ]; then
    die "Project '$project' not found. Run: secrets list"
  fi

  info "Removing project: $project"
  git -C "$SECRETS_DIR" rm -r "$project/" >/dev/null
  git -C "$SECRETS_DIR" commit -m "remove $project" >/dev/null
  if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
    git -C "$SECRETS_DIR" push >/dev/null 2>&1
    info "Removed $project from remote"
  else
    info "Removed $project locally (no remote configured)"
  fi
}

cmd_rekey() {
  check_cmd age
  check_cmd git
  check_initialized
  check_key

  # Create temp dir with cleanup trap
  local tmpdir
  tmpdir=$(mktemp -d)
  trap 'rm -rf "$tmpdir"' EXIT INT TERM

  info "Decrypting all files with current key..."

  # Decrypt all .age files into temp dir
  local file_count=0
  for dir in "$SECRETS_DIR"/*/; do
    [ -d "$dir" ] || continue
    local project
    project=$(basename "$dir")
    [[ "$project" == .* ]] && continue
    mkdir -p "$tmpdir/$project"
    for f in "$dir"*.age "$dir".*.age; do
      [ -f "$f" ] || continue
      local name
      name=$(basename "$f" .age)
      if ! age -d -i "$KEY_FILE" -o "$tmpdir/$project/$name" "$f"; then
        die "Decryption failed for $project/$name. Rekey aborted. Old key preserved."
      fi
      file_count=$((file_count + 1))
    done
  done

  if [ "$file_count" -eq 0 ]; then
    die "No encrypted files found. Nothing to rekey."
  fi

  info "Decrypted $file_count file(s). Generating new key pair..."

  # Generate new key (overwrites old)
  age-keygen -o "$KEY_FILE" 2>&1
  local pubkey
  pubkey=$(get_pubkey)

  info "Re-encrypting all files with new key..."

  # Re-encrypt all files
  for dir in "$tmpdir"/*/; do
    [ -d "$dir" ] || continue
    local project
    project=$(basename "$dir")
    mkdir -p "$SECRETS_DIR/$project"
    for f in "$dir"*; do
      [ -f "$f" ] || continue
      local name
      name=$(basename "$f")
      age -r "$pubkey" -o "$SECRETS_DIR/$project/${name}.age" "$f"
    done
  done

  # Commit and push
  git -C "$SECRETS_DIR" add -A
  git -C "$SECRETS_DIR" commit -m "rekey all secrets" >/dev/null
  if git -C "$SECRETS_DIR" remote get-url origin >/dev/null 2>&1; then
    git -C "$SECRETS_DIR" push >/dev/null 2>&1
    info "Pushed rekeyed secrets to remote"
  else
    info "Committed rekeyed secrets locally (no remote configured)"
  fi

  info "Rekey complete!"
  echo ""
  echo "IMPORTANT: Copy new key to your other machine:"
  echo "  scp $KEY_FILE <other-machine>:$KEY_FILE"
  echo ""
  echo "WARNING: Old ciphertext remains in git history."
  echo "For full rotation, create a fresh repo."
}

cmd_help() {
  cat << 'EOF'
secrets — encrypted env file sync between machines

Usage:
  secrets init              Initialize the secrets repo and generate an age key
  secrets push [project]    Encrypt .env* files and push to the secrets repo
  secrets pull [project]    Pull and decrypt .env* files into current directory
  secrets list              List all projects and their secret files
  secrets rm <project>      Remove a project's secrets from the repo
  secrets rekey             Re-encrypt all secrets with a new key

If [project] is omitted, it is derived from the current directory's
git remote (if available) or the directory name.

Environment:
  SECRETS_DIR    Path to secrets repo (default: ~/.secrets)
EOF
}

# ─── Main ──────────────────────────────────────────────────────────────

case "${1:-help}" in
  init)   cmd_init ;;
  push)   cmd_push "${2:-}" ;;
  pull)   cmd_pull "${2:-}" ;;
  list)   cmd_list ;;
  rm)     cmd_rm "${2:-}" ;;
  rekey)  cmd_rekey ;;
  help|--help|-h) cmd_help ;;
  *)      die "Unknown command: $1. Run 'secrets help' for usage." ;;
esac
