#!/bin/bash
set -euo pipefail

script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
root_dir="$(cd -- "$script_dir/.." && pwd)"

local_folder="${LOCAL_FOLDER:-$root_dir}"
remote_folder="${REMOTE_FOLDER:-/opt/www/virtual/SkinbaseNova}"
remote_server="${REMOTE_SERVER:-klevze@server3.klevze.si}"
# Keep release/shared defaults derived from the already-resolved remote_folder so staging/custom targets do not share production state by accident.
remote_release_root="${REMOTE_RELEASE_ROOT:-${remote_folder}.releases}"
remote_shared_root="${REMOTE_SHARED_ROOT:-${remote_release_root}/shared}"
php_bin="${PHP_BIN:-php}"
composer_bin="${COMPOSER_BIN:-composer}"
ssh_bin="${SSH_BIN:-ssh}"
rsync_bin="${RSYNC_BIN:-rsync}"
local_build_command="${LOCAL_BUILD_COMMAND:-}"
local_test_command="${LOCAL_TEST_COMMAND:-$php_bin artisan test}"
allow_deploy_from_dot_deploy="${ALLOW_DEPLOY_FROM_DOT_DEPLOY:-0}"

run_local_build=1
run_local_tests=0
run_remote_migrations=1
run_db_sync=0
run_meilisearch_setup=0
auto_detect_meilisearch=0
dry_run=0
deploy_mode="normal"
skip_ssr_restart=0
db_sync_source=""
legacy_db_sync_mode=0
force_db_sync=0
skip_maintenance=0
full_upgrade_pre_hook="${FULL_UPGRADE_PRE_HOOK:-}"
full_upgrade_post_hook="${FULL_UPGRADE_POST_HOOK:-}"
db_sync_confirm_target="${DB_SYNC_CONFIRM_TARGET:-}"
db_sync_confirm_phrase="${DB_SYNC_CONFIRM_PHRASE:-}"
release_retention="${RELEASE_RETENTION:-5}"
release_id="${RELEASE_ID:-}"
shared_storage_excludes="${REMOTE_SHARED_STORAGE_EXCLUDES:-}"
meilisearch_models_csv=""
readonly all_meilisearch_models_csv='App\Models\Artwork,App\Models\User,App\Models\Group,App\Models\Post,App\Models\Message'
healthcheck_url="${HEALTHCHECK_URL:-}"
deploy_rollback="${DEPLOY_ROLLBACK:-1}"
reload_php_fpm="${RELOAD_PHP_FPM:-0}"
php_fpm_service="${PHP_FPM_SERVICE:-php8.4-fpm}"
ssr_supervisor_program="${SSR_SUPERVISOR_PROGRAM:-skinbase-ssr}"
require_clean_git="${REQUIRE_CLEAN_GIT:-0}"
required_git_branch="${REQUIRED_GIT_BRANCH:-}"
db_sync_remote_maintenance=0

declare -a rsync_args=()

usage() {
  cat <<'EOF_USAGE'
Usage: bash sync.sh [options]

Options:
  --mode=normal|full-upgrade
                        Choose the deploy mode. Default: normal.
  --full-upgrade        Alias for --mode=full-upgrade.
  --skip-build          Skip local npm build before rsync.
  --with-tests          Run the local test command before build/sync. Default command: php artisan test.
  --skip-migrate        Skip php artisan migrate on the server.
  --dry-run             Print the planned rsync/deploy actions without changing the remote server.
  --release-id ID       Override the generated release version label used for the remote release directory.
  --keep-releases N     Keep the latest N remote releases ready for server-side switching. Default: 5.
  --shared-storage-exclude PATHS
                        Comma-separated paths under storage/ to omit from shared storage adoption/copy.
                        Existing shared storage is NOT deleted by this option.
  --with-db-from=local  Replace the production database with a dump from the local database.
  --confirm-db-sync-target HOST
                        Must match the remote server name when running non-interactively.
  --confirm-db-sync-phrase TEXT
                        Must equal 'replace production db from local' when running non-interactively.
  --with-db             Legacy alias for --with-db-from=local.
  --force-db-sync       Legacy extra confirmation flag for --with-db.
  --with-meilisearch    Run Meilisearch settings sync and reimport searchable models.
  --auto-meilisearch    Refresh only searchable models affected by changed searchable model/config files.
  --skip-meilisearch    Explicitly skip Meilisearch refresh (default behavior).
  --upgrade-pre-hook CMD
                        Run a remote shell command before Composer/migrations in full-upgrade mode.
  --upgrade-post-hook CMD
                        Run a remote shell command after the deploy completes in full-upgrade mode.
  --no-maintenance      Skip php artisan down/up during deploy.
  --skip-ssr-restart    Skip restarting the Inertia SSR Node.js process (supervisor: skinbase-ssr).
  --healthcheck-url URL Run an HTTP health check after the release switch and before marking deploy successful.
  --no-rollback         Disable automatic rollback to the previous release when the switched release fails before health/safe point.
  --reload-php-fpm      Try to reload PHP-FPM after release switch. Uses PHP_FPM_SERVICE, default php8.4-fpm.
  --no-php-fpm-reload   Explicitly skip PHP-FPM reload.
  --require-clean-git   Refuse deploy when the local Git working tree has uncommitted changes.
  --required-branch BRANCH
                        Refuse deploy unless the local Git branch matches BRANCH.
  --help                Show this help.

Environment overrides:
  LOCAL_FOLDER, REMOTE_FOLDER, REMOTE_SERVER, REMOTE_RELEASE_ROOT, REMOTE_SHARED_ROOT,
  PHP_BIN, COMPOSER_BIN, SSH_BIN, RSYNC_BIN, LOCAL_BUILD_COMMAND, LOCAL_TEST_COMMAND,
  DB_SYNC_CONFIRM_TARGET, DB_SYNC_CONFIRM_PHRASE, RELEASE_RETENTION, RELEASE_ID,
  REMOTE_SHARED_STORAGE_EXCLUDES, HEALTHCHECK_URL, DEPLOY_ROLLBACK,
  SSR_SUPERVISOR_PROGRAM,
  RELOAD_PHP_FPM, PHP_FPM_SERVICE, REQUIRE_CLEAN_GIT, REQUIRED_GIT_BRANCH,
  ALLOW_DEPLOY_FROM_DOT_DEPLOY, FULL_UPGRADE_PRE_HOOK, FULL_UPGRADE_POST_HOOK
EOF_USAGE
}

log_step() {
  printf '\n==> %s\n' "$1"
}

log_info() {
  printf '  -> %s\n' "$1"
}

log_warn() {
  printf 'WARN: %s\n' "$1" >&2
}

die() {
  printf 'ERROR: %s\n' "$1" >&2
  exit 1
}

require_command() {
  local command_name="$1"
  local description="$2"

  command -v "$command_name" >/dev/null 2>&1 || die "$description is required but was not found in PATH ($command_name)."
}

validate_positive_integer() {
  local value="$1"
  local label="$2"

  [[ "$value" =~ ^[1-9][0-9]*$ ]] || die "$label must be a positive integer. Received: $value"
}

validate_boolean_flag() {
  local value="$1"
  local label="$2"

  [[ "$value" == "0" || "$value" == "1" ]] || die "$label must be 0 or 1. Received: $value"
}

sanitize_release_fragment() {
  local value="$1"

  value="${value//[^A-Za-z0-9._-]/-}"
  value="${value#-}"
  value="${value%-}"

  printf '%s' "$value"
}

validate_release_id() {
  local value="$1"

  [[ -n "$value" ]] || die "Release id cannot be empty."
  [[ "$value" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || die "Invalid release id: $value"
  [[ "$value" != "." && "$value" != ".." ]] || die "Invalid release id: $value"
}

determine_release_id() {
  local timestamp
  local vcs_fragment="manual"

  [[ -n "$release_id" ]] && return 0

  timestamp="$(date -u +%Y%m%d-%H%M%S)"

  if command -v git >/dev/null 2>&1 && git -C "$local_folder" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    vcs_fragment="$(git -C "$local_folder" describe --always --dirty --tags 2>/dev/null || git -C "$local_folder" rev-parse --short HEAD 2>/dev/null || printf 'manual')"
  fi

  vcs_fragment="$(sanitize_release_fragment "$vcs_fragment")"
  [[ -n "$vcs_fragment" ]] || vcs_fragment="manual"
  release_id="${timestamp}-${vcs_fragment}"
}

remote_release_path() {
  printf '%s' "${remote_release_root}/releases/${release_id}"
}

guard_local_folder() {
  local normalized_local_folder

  normalized_local_folder="$(cd -- "$local_folder" && pwd)"
  local_folder="$normalized_local_folder"

  echo "Deploy source: $local_folder"
  echo "Deploy target: $remote_server:$remote_folder"

  if [[ ! -d "$local_folder" ]]; then
    die "Deploy source folder does not exist: $local_folder"
  fi

  if [[ "$allow_deploy_from_dot_deploy" != "1" && "$local_folder" == *"/.deploy/"* ]]; then
    log_warn "Refusing to deploy from a .deploy snapshot folder: $local_folder"
    log_warn "This usually means LOCAL_FOLDER is pointing at a stale release snapshot instead of the repo root."
    log_warn "Unset LOCAL_FOLDER or set it to the repository root before running sync.sh."
    die "If you intentionally want to deploy from that folder, set ALLOW_DEPLOY_FROM_DOT_DEPLOY=1."
  fi
}

guard_git_state() {
  local current_branch=""

  if ! command -v git >/dev/null 2>&1 || ! git -C "$local_folder" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    return 0
  fi

  if [[ -n "$required_git_branch" ]]; then
    current_branch="$(git -C "$local_folder" branch --show-current 2>/dev/null || printf '')"
    [[ "$current_branch" == "$required_git_branch" ]] || die "Refusing deploy from Git branch '${current_branch:-unknown}'. Required branch: $required_git_branch"
  fi

  if [[ "$require_clean_git" == "1" ]]; then
    git -C "$local_folder" diff --quiet || die "Working tree has uncommitted changes. Commit/stash them or disable REQUIRE_CLEAN_GIT."
    git -C "$local_folder" diff --cached --quiet || die "Git index has staged but uncommitted changes. Commit/stash them or disable REQUIRE_CLEAN_GIT."
  fi
}

run_preflight_checks() {
  log_step "Running local preflight checks"

  require_command "$php_bin" "PHP"
  require_command "$composer_bin" "Composer"
  require_command "$ssh_bin" "SSH"
  require_command "$rsync_bin" "rsync"

  [[ -f "$local_folder/artisan" ]] || die "Expected Laravel artisan entrypoint at $local_folder/artisan."
  [[ -f "$local_folder/composer.json" ]] || die "Expected composer.json at $local_folder/composer.json."
  validate_positive_integer "$release_retention" "Release retention"
  validate_boolean_flag "$deploy_rollback" "Deploy rollback"
  validate_boolean_flag "$reload_php_fpm" "PHP-FPM reload"
  validate_boolean_flag "$require_clean_git" "Require clean Git"
  [[ -n "$ssr_supervisor_program" ]] || die "SSR_SUPERVISOR_PROGRAM cannot be empty."
  determine_release_id
  validate_release_id "$release_id"
  guard_git_state
  log_info "Release version: $release_id"
  log_info "Remote release root: $remote_release_root"
  log_info "Remote shared root: $remote_shared_root"

  if [[ "$run_local_build" -eq 1 && -z "$local_build_command" ]]; then
    if is_wsl && command -v wslpath >/dev/null 2>&1 && command -v powershell.exe >/dev/null 2>&1; then
      log_info "WSL frontend build will use Windows npm.cmd via powershell.exe"
    else
      require_command npm "npm"
    fi
  fi

  log_info "Required local deploy tools are available"
}

is_wsl() {
  [[ -n "${WSL_DISTRO_NAME:-}" || -n "${WSL_INTEROP:-}" ]]
}

run_local_tests_if_requested() {
  [[ "$run_local_tests" -eq 1 ]] || return 0

  log_step "Running local tests"
  (
    cd "$local_folder"
    eval "$local_test_command"
  )
}

run_frontend_build() {
  if [[ -n "$local_build_command" ]]; then
    (
      cd "$local_folder"
      eval "$local_build_command"
    )
    return
  fi

  if is_wsl && command -v wslpath >/dev/null 2>&1 && command -v powershell.exe >/dev/null 2>&1; then
    local windows_local_folder
    windows_local_folder="$(wslpath -w "$local_folder")"

    echo "Detected WSL checkout; running frontend build with Windows npm.cmd to match local node_modules..."
    powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \
      "Set-Location -LiteralPath '$windows_local_folder'; npm.cmd run build; npm.cmd run build:ssr"
    return
  fi

  (
    cd "$local_folder"
    npm run build
    npm run build:ssr
  )
}

validate_local_build_artifacts() {
  [[ "$run_local_build" -eq 1 ]] || return 0

  if [[ ! -f "$local_folder/public/build/manifest.json" ]]; then
    log_warn "Vite manifest was not found at public/build/manifest.json after build. Continuing because some projects use a custom build path."
  fi
}

build_rsync_args() {
  rsync_args=(
    -rlvz
    --no-perms
    --no-times
    --omit-dir-times
    --delay-updates
    --delete
    --delete-delay
    --exclude ".phpintel/"
    --exclude "bootstrap/cache/"
    --exclude ".env"
    --exclude "public/hot"
    --exclude "public/sitemap.xml"
    --exclude "public/sitemaps/"
    --exclude "node_modules"
    --exclude "public/files/"
    --exclude "public/storage"
    --exclude "resources/lang/"
    --exclude "storage/"
    --exclude ".git/"
    --exclude ".deploy/"
    --exclude ".cursor/"
    --exclude ".venv/"
    --exclude "/var/php-tmp"
    --exclude "/var/php-sessions"
    --exclude "/oldSite"
    --exclude "/vendor"
    -e "$ssh_bin"
  )
}

collect_sync_changed_files() {
  local itemized

  if ! itemized="$($rsync_bin "${rsync_args[@]}" --dry-run --itemize-changes "$local_folder/" "$remote_server:$remote_folder/" 2>/dev/null)"; then
    return 1
  fi

  printf '%s\n' "$itemized" | awk '
    /^deleting / {
      sub(/^deleting /, "", $0)
      if ($0 !~ /\/$/) print
      next
    }
    /^[<>ch.*][^ ]* / {
      path = $0
      sub(/^[^ ]+ /, "", path)
      if (path !~ /\/$/) print path
    }
  ' | sed '/^$/d' | sort -u
}

detect_meilisearch_models_from_sync() {
  local changed_files
  local file
  local force_full=0
  local -a models=()

  if ! changed_files="$(collect_sync_changed_files)"; then
    return 1
  fi

  [[ -n "$changed_files" ]] || return 1

  while IFS= read -r file; do
    case "$file" in
      config/scout.php|app/Console/Commands/ConfigureMeilisearchIndex.php)
        force_full=1
        ;;
      app/Models/Artwork.php)
        models+=("App\\Models\\Artwork")
        ;;
      app/Models/User.php)
        models+=("App\\Models\\User")
        ;;
      app/Models/Group.php)
        models+=("App\\Models\\Group")
        ;;
      app/Models/Post.php)
        models+=("App\\Models\\Post")
        ;;
      app/Models/Message.php)
        models+=("App\\Models\\Message")
        ;;
    esac
  done <<< "$changed_files"

  if [[ "$force_full" -eq 1 ]]; then
    printf '%s\n' "$all_meilisearch_models_csv"
    return 0
  fi

  [[ ${#models[@]} -gt 0 ]] || return 1

  printf '%s\n' "$(printf '%s\n' "${models[@]}" | awk '!seen[$0]++' | paste -sd, -)"
}

confirm_database_replacement() {
  local expected_phrase="replace production db from local"
  local typed_target=""
  local typed_phrase=""

  if [[ "$run_db_sync" -ne 1 || "$db_sync_source" != "local" ]]; then
    return
  fi

  echo "WARNING: this will overwrite the production database on $remote_server using your local database dump."

  if [[ -n "$db_sync_confirm_target" || -n "$db_sync_confirm_phrase" ]]; then
    if [[ "$db_sync_confirm_target" != "$remote_server" ]]; then
      die "Refusing DB sync: --confirm-db-sync-target must exactly match $remote_server."
    fi

    if [[ "$db_sync_confirm_phrase" != "$expected_phrase" ]]; then
      die "Refusing DB sync: --confirm-db-sync-phrase must exactly equal '$expected_phrase'."
    fi

    return
  fi

  if [[ ! -t 0 ]]; then
    die "Refusing DB sync in non-interactive mode without --confirm-db-sync-target '$remote_server' and --confirm-db-sync-phrase '$expected_phrase'."
  fi

  read -r -p "Type the remote server to confirm DB replacement [$remote_server]: " typed_target
  [[ "$typed_target" == "$remote_server" ]] || die "Refusing DB sync: remote server confirmation did not match."

  read -r -p "Type '$expected_phrase' to continue: " typed_phrase
  [[ "$typed_phrase" == "$expected_phrase" ]] || die "Refusing DB sync: confirmation phrase did not match."
}

enable_remote_maintenance_for_db_sync() {
  [[ "$skip_maintenance" -eq 0 ]] || return 0

  log_step "Enabling remote maintenance mode before database replacement"
  "$ssh_bin" "$remote_server" \
    REMOTE_FOLDER="$(printf '%q' "$remote_folder")" \
    PHP_BIN="$(printf '%q' "$php_bin")" \
    'bash -s' <<'EOF_REMOTE_MAINTENANCE'
set -euo pipefail

if [[ -f "$REMOTE_FOLDER/artisan" ]]; then
  "$PHP_BIN" "$REMOTE_FOLDER/artisan" down --retry=60 || true
fi
EOF_REMOTE_MAINTENANCE
  db_sync_remote_maintenance=1
}

bring_remote_app_up_from_local_trap() {
  local exit_code="${1:-$?}"

  if [[ "${db_sync_remote_maintenance:-0}" -eq 1 && "$skip_maintenance" -eq 0 ]]; then
    log_warn "Deploy exited after DB maintenance was enabled; attempting to bring the remote app back online."
    "$ssh_bin" "$remote_server" \
      REMOTE_FOLDER="$(printf '%q' "$remote_folder")" \
      PHP_BIN="$(printf '%q' "$php_bin")" \
      'bash -s' <<'EOF_REMOTE_UP' || true
set -euo pipefail

if [[ -f "$REMOTE_FOLDER/artisan" ]]; then
  "$PHP_BIN" "$REMOTE_FOLDER/artisan" up >/dev/null 2>&1 || true
fi
EOF_REMOTE_UP
  fi

  exit "$exit_code"
}

prepare_remote_release_layout() {
  log_step "Preparing remote release layout"

  "$ssh_bin" "$remote_server" \
    REMOTE_FOLDER="$(printf '%q' "$remote_folder")" \
    REMOTE_RELEASE_ROOT="$(printf '%q' "$remote_release_root")" \
    REMOTE_SHARED_ROOT="$(printf '%q' "$remote_shared_root")" \
    RELEASE_ID="$(printf '%q' "$release_id")" \
    REMOTE_SHARED_STORAGE_EXCLUDES="$(printf '%q' "$shared_storage_excludes")" \
    'bash -s' <<'EOF_PREPARE_REMOTE'
set -euo pipefail

release_path="${REMOTE_RELEASE_ROOT}/releases/${RELEASE_ID}"
current_link="${REMOTE_RELEASE_ROOT}/current"
legacy_release_id="legacy-$(date -u +%Y%m%d-%H%M%S)"
legacy_release_path="${REMOTE_RELEASE_ROOT}/releases/${legacy_release_id}"
declare -a storage_exclude_args=()

log_info() {
  printf '  -> %s\n' "$1"
}

die() {
  printf 'ERROR: %s\n' "$1" >&2
  exit 1
}

ensure_dir() {
  local d
  for d in "$@"; do
    mkdir -p "$d"
  done
}

ensure_laravel_shared_storage_layout() {
  ensure_dir \
    "${REMOTE_SHARED_ROOT}/storage/app/public" \
    "${REMOTE_SHARED_ROOT}/storage/framework/cache/data" \
    "${REMOTE_SHARED_ROOT}/storage/framework/sessions" \
    "${REMOTE_SHARED_ROOT}/storage/framework/views" \
    "${REMOTE_SHARED_ROOT}/storage/logs"
}

build_storage_exclude_args() {
  local detected_excludes=()
  local normalized=""

  mapfile -t detected_excludes < <(find_auto_storage_excludes "${REMOTE_SHARED_ROOT}/storage")

  for normalized in "${detected_excludes[@]}"; do
    storage_exclude_args+=(--exclude "$normalized")
  done

  while IFS= read -r normalized; do
    normalized="${normalized#storage/}"
    normalized="${normalized#/}"
    [[ -n "$normalized" ]] || continue
    storage_exclude_args+=(--exclude "$normalized")
  done < <(printf '%s' "${REMOTE_SHARED_STORAGE_EXCLUDES:-}" | tr ',' '\n')
}

find_auto_storage_excludes() {
  local storage_root="$1"

  [[ -d "${storage_root}/app" ]] || return 0

  find "${storage_root}/app" \
    \( -type d -o -type f \) \
    \( -iname '*backup*' -o -iname '*.bak' -o -iname '*.bak.*' \) \
    -printf '%P\n' \
    | sed 's#^#app/#' \
    | awk '!seen[$0]++'
}

sync_dir_into_shared() {
  local source_path="$1"
  local shared_path="$2"
  local exclude_scope="${3:-}"

  ensure_dir "$shared_path"

  if [[ -d "$source_path" ]]; then
    if [[ "$exclude_scope" == "storage" && ${#storage_exclude_args[@]} -gt 0 ]]; then
      rsync -a "${storage_exclude_args[@]}" "$source_path/" "$shared_path/"
    else
      rsync -a "$source_path/" "$shared_path/"
    fi
  fi
}

adopt_dir_into_shared() {
  local source_path="$1"
  local shared_path="$2"
  local exclude_scope="${3:-}"

  [[ -d "$source_path" ]] || return 0

  ensure_dir "$(dirname "$shared_path")"

  if [[ ! -d "$shared_path" ]] || [[ -z "$(find "$shared_path" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]]; then
    rm -rf "$shared_path"
    mv "$source_path" "$shared_path"
    return 0
  fi

  sync_dir_into_shared "$source_path" "$shared_path" "$exclude_scope"
}

adopt_file_into_shared() {
  local source_path="$1"
  local shared_path="$2"

  [[ -f "$source_path" ]] || return 0

  ensure_dir "$(dirname "$shared_path")"

  if [[ ! -e "$shared_path" ]]; then
    mv "$source_path" "$shared_path"
    return 0
  fi

  cp -a "$source_path" "$shared_path"
}

link_shared_paths() {
  local target_release="$1"

  ensure_dir "$target_release/bootstrap/cache" "$target_release/public" "$target_release/var"

  rm -f "$target_release/.env"
  ln -sfn "${REMOTE_SHARED_ROOT}/.env" "$target_release/.env"

  rm -rf "$target_release/storage"
  ln -sfn "${REMOTE_SHARED_ROOT}/storage" "$target_release/storage"

  rm -rf "$target_release/public/files"
  ln -sfn "${REMOTE_SHARED_ROOT}/public/files" "$target_release/public/files"

  rm -f "$target_release/public/sitemap.xml"
  ln -sfn "${REMOTE_SHARED_ROOT}/public/sitemaps/sitemap.xml" "$target_release/public/sitemap.xml"

  rm -rf "$target_release/public/sitemaps"
  ln -sfn "${REMOTE_SHARED_ROOT}/public/sitemaps" "$target_release/public/sitemaps"

  rm -rf "$target_release/var/php-tmp"
  ln -sfn "${REMOTE_SHARED_ROOT}/var/php-tmp" "$target_release/var/php-tmp"

  rm -rf "$target_release/var/php-sessions"
  ln -sfn "${REMOTE_SHARED_ROOT}/var/php-sessions" "$target_release/var/php-sessions"

  rm -rf "$target_release/public/storage"
  ln -sfn "${REMOTE_SHARED_ROOT}/storage/app/public" "$target_release/public/storage"
}

ensure_dir "${REMOTE_RELEASE_ROOT}/releases" "${REMOTE_RELEASE_ROOT}/deployments" "$REMOTE_SHARED_ROOT"
ensure_dir "${REMOTE_SHARED_ROOT}/storage" "${REMOTE_SHARED_ROOT}/public/files" "${REMOTE_SHARED_ROOT}/public/sitemaps" "${REMOTE_SHARED_ROOT}/var/php-tmp" "${REMOTE_SHARED_ROOT}/var/php-sessions"
ensure_laravel_shared_storage_layout
build_storage_exclude_args

if [[ -e "$REMOTE_FOLDER" && ! -L "$REMOTE_FOLDER" ]]; then
  [[ -d "$REMOTE_FOLDER" ]] || die "Remote app path exists but is not a directory: ${REMOTE_FOLDER}"

  log_info "Adopting existing live folder into release layout"
  mv "$REMOTE_FOLDER" "$legacy_release_path"

  if [[ -f "${legacy_release_path}/.env" && ! -f "${REMOTE_SHARED_ROOT}/.env" ]]; then
    cp -a "${legacy_release_path}/.env" "${REMOTE_SHARED_ROOT}/.env"
  fi

  adopt_dir_into_shared "${legacy_release_path}/storage" "${REMOTE_SHARED_ROOT}/storage" "storage"
  adopt_dir_into_shared "${legacy_release_path}/public/files" "${REMOTE_SHARED_ROOT}/public/files"
  adopt_dir_into_shared "${legacy_release_path}/public/sitemaps" "${REMOTE_SHARED_ROOT}/public/sitemaps"
  adopt_file_into_shared "${legacy_release_path}/public/sitemap.xml" "${REMOTE_SHARED_ROOT}/public/sitemaps/sitemap.xml"
  adopt_dir_into_shared "${legacy_release_path}/var/php-tmp" "${REMOTE_SHARED_ROOT}/var/php-tmp"
  adopt_dir_into_shared "${legacy_release_path}/var/php-sessions" "${REMOTE_SHARED_ROOT}/var/php-sessions"

  link_shared_paths "$legacy_release_path"
  ln -sfn "$legacy_release_path" "$current_link"
fi

if [[ -L "$REMOTE_FOLDER" ]]; then
  ln -sfn "$current_link" "$REMOTE_FOLDER"
elif [[ ! -e "$REMOTE_FOLDER" ]]; then
  ln -sfn "$current_link" "$REMOTE_FOLDER"
fi

rm -rf "$release_path"
mkdir -p "$release_path"

log_info "Release staging path ready at ${release_path}"
EOF_PREPARE_REMOTE
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --mode)
      shift
      case "${1:?Missing value for --mode}" in
        normal|full-upgrade)
          deploy_mode="$1"
          ;;
        *)
          die "Unsupported deploy mode: $1"
          ;;
      esac
      ;;
    --mode=*)
      case "${1#*=}" in
        normal|full-upgrade)
          deploy_mode="${1#*=}"
          ;;
        *)
          die "Unsupported deploy mode: ${1#*=}"
          ;;
      esac
      ;;
    --full-upgrade)
      deploy_mode="full-upgrade"
      ;;
    --skip-build)
      run_local_build=0
      ;;
    --with-tests)
      run_local_tests=1
      ;;
    --skip-migrate)
      run_remote_migrations=0
      ;;
    --dry-run)
      dry_run=1
      ;;
    --release-id)
      shift
      release_id="$(sanitize_release_fragment "${1:?Missing value for --release-id}")"
      ;;
    --release-id=*)
      release_id="$(sanitize_release_fragment "${1#*=}")"
      ;;
    --keep-releases)
      shift
      release_retention="${1:?Missing value for --keep-releases}"
      ;;
    --keep-releases=*)
      release_retention="${1#*=}"
      ;;
    --shared-storage-exclude)
      shift
      shared_storage_excludes="${1:?Missing value for --shared-storage-exclude}"
      ;;
    --shared-storage-exclude=*)
      shared_storage_excludes="${1#*=}"
      ;;
    --with-db-from=local)
      run_db_sync=1
      db_sync_source="local"
      ;;
    --with-db-from=*)
      die "Unsupported DB sync source in option: $1"
      ;;
    --with-db)
      run_db_sync=1
      db_sync_source="local"
      legacy_db_sync_mode=1
      ;;
    --force-db-sync)
      force_db_sync=1
      ;;
    --confirm-db-sync-target)
      shift
      db_sync_confirm_target="${1:?Missing value for --confirm-db-sync-target}"
      ;;
    --confirm-db-sync-target=*)
      db_sync_confirm_target="${1#*=}"
      ;;
    --confirm-db-sync-phrase)
      shift
      db_sync_confirm_phrase="${1:?Missing value for --confirm-db-sync-phrase}"
      ;;
    --confirm-db-sync-phrase=*)
      db_sync_confirm_phrase="${1#*=}"
      ;;
    --with-meilisearch)
      run_meilisearch_setup=1
      auto_detect_meilisearch=0
      meilisearch_models_csv="$all_meilisearch_models_csv"
      ;;
    --auto-meilisearch)
      run_meilisearch_setup=0
      auto_detect_meilisearch=1
      meilisearch_models_csv=""
      ;;
    --skip-meilisearch)
      run_meilisearch_setup=0
      auto_detect_meilisearch=0
      meilisearch_models_csv=""
      ;;
    --upgrade-pre-hook)
      shift
      full_upgrade_pre_hook="${1:?Missing value for --upgrade-pre-hook}"
      ;;
    --upgrade-pre-hook=*)
      full_upgrade_pre_hook="${1#*=}"
      ;;
    --upgrade-post-hook)
      shift
      full_upgrade_post_hook="${1:?Missing value for --upgrade-post-hook}"
      ;;
    --upgrade-post-hook=*)
      full_upgrade_post_hook="${1#*=}"
      ;;
    --no-maintenance)
      skip_maintenance=1
      ;;
    --skip-ssr-restart)
      skip_ssr_restart=1
      ;;
    --healthcheck-url)
      shift
      healthcheck_url="${1:?Missing value for --healthcheck-url}"
      ;;
    --healthcheck-url=*)
      healthcheck_url="${1#*=}"
      ;;
    --no-rollback)
      deploy_rollback=0
      ;;
    --reload-php-fpm)
      reload_php_fpm=1
      ;;
    --no-php-fpm-reload)
      reload_php_fpm=0
      ;;
    --require-clean-git)
      require_clean_git=1
      ;;
    --required-branch)
      shift
      required_git_branch="${1:?Missing value for --required-branch}"
      ;;
    --required-branch=*)
      required_git_branch="${1#*=}"
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    *)
      die "Unknown option: $1"
      ;;
  esac
  shift
done

if [[ -n "$full_upgrade_pre_hook" || -n "$full_upgrade_post_hook" ]] && [[ "$deploy_mode" != "full-upgrade" ]]; then
  die "Upgrade hooks can only be used with --mode=full-upgrade."
fi

guard_local_folder
run_preflight_checks

if [[ "$run_db_sync" -eq 1 && "$db_sync_source" != "local" ]]; then
  die "Refusing DB sync without an explicit source. Use --with-db-from=local."
fi

if [[ "$run_db_sync" -eq 1 ]] && { [[ -z "$db_sync_confirm_target" && -n "$db_sync_confirm_phrase" ]] || [[ -n "$db_sync_confirm_target" && -z "$db_sync_confirm_phrase" ]]; }; then
  die "Refusing DB sync: both --confirm-db-sync-target and --confirm-db-sync-phrase are required together when provided."
fi

if [[ "$legacy_db_sync_mode" -eq 1 && "$force_db_sync" -ne 1 ]]; then
  die "Refusing legacy --with-db without --force-db-sync. Prefer --with-db-from=local instead."
fi

if [[ "$run_db_sync" -eq 1 ]]; then
  confirm_database_replacement
fi

if [[ "$deploy_mode" == "full-upgrade" && "$auto_detect_meilisearch" -eq 1 && "$run_meilisearch_setup" -eq 0 ]]; then
  run_meilisearch_setup=1
  auto_detect_meilisearch=0
  meilisearch_models_csv="$all_meilisearch_models_csv"
fi

run_local_tests_if_requested

if [[ "$run_local_build" -eq 1 ]]; then
  log_step "Building frontend assets locally"
  run_frontend_build
  validate_local_build_artifacts
fi

build_rsync_args

if [[ "$dry_run" -eq 1 ]]; then
  log_step "Dry-run deployment preview"
  log_info "Skipping remote changes because --dry-run was requested"
  log_info "Release version: $release_id"
  log_info "Release staging path: $(remote_release_path)"
  log_info "Remote app path will be switched on the server by updating ${remote_release_root}/current"
  log_info "Planned rsync command: $rsync_bin ${rsync_args[*]} --dry-run --itemize-changes $local_folder/ $remote_server:$(remote_release_path)/"
  "$rsync_bin" "${rsync_args[@]}" --dry-run --itemize-changes "$local_folder/" "$remote_server:$(remote_release_path)/"

  if [[ "$run_db_sync" -eq 1 ]]; then
    log_warn "Dry-run requested with database sync enabled. The database replacement step was not executed."
  fi

  if [[ "$auto_detect_meilisearch" -eq 1 ]]; then
    if meilisearch_models_csv="$(detect_meilisearch_models_from_sync)"; then
      log_info "Meilisearch refresh would run for detected models: $meilisearch_models_csv"
    else
      log_info "Meilisearch auto-detection found no model/index changes."
    fi
  elif [[ "$run_meilisearch_setup" -eq 1 ]]; then
    log_info "Meilisearch refresh would run for: ${meilisearch_models_csv:-$all_meilisearch_models_csv}"
  fi

  log_step "Dry-run complete"
  exit 0
fi

prepare_remote_release_layout

if [[ "$run_meilisearch_setup" -eq 0 && "$auto_detect_meilisearch" -eq 1 ]]; then
  if meilisearch_models_csv="$(detect_meilisearch_models_from_sync)"; then
    run_meilisearch_setup=1
    log_info "Detected Meilisearch-relevant changes in this deployment; will refresh indexes for: $meilisearch_models_csv"
  fi
fi

log_step "Syncing release ${release_id} to $remote_server"
"$rsync_bin" "${rsync_args[@]}" "$local_folder/" "$remote_server:$(remote_release_path)/"

if [[ "$run_db_sync" -eq 1 ]]; then
  trap 'rc=$?; bring_remote_app_up_from_local_trap "$rc"' EXIT
  enable_remote_maintenance_for_db_sync

  log_step "Replacing the production database from the local dump"
  db_push_args=(
    --force
    --remote-server "$remote_server"
    --remote-folder "$remote_folder"
  )

  if [[ "$run_remote_migrations" -eq 0 ]]; then
    db_push_args+=(--skip-migrate)
  fi

  "$script_dir/push-db-to-prod.sh" "${db_push_args[@]}"
fi

log_step "Running remote Composer and release switch steps"
"$ssh_bin" "$remote_server" \
  REMOTE_FOLDER="$(printf '%q' "$remote_folder")" \
  REMOTE_RELEASE_ROOT="$(printf '%q' "$remote_release_root")" \
  REMOTE_SHARED_ROOT="$(printf '%q' "$remote_shared_root")" \
  RELEASE_ID="$(printf '%q' "$release_id")" \
  RELEASE_RETENTION="$(printf '%q' "$release_retention")" \
  REMOTE_SHARED_STORAGE_EXCLUDES="$(printf '%q' "$shared_storage_excludes")" \
  PHP_BIN="$(printf '%q' "$php_bin")" \
  COMPOSER_BIN="$(printf '%q' "$composer_bin")" \
  RUN_REMOTE_MIGRATIONS="$run_remote_migrations" \
  SKIP_MAINTENANCE="$skip_maintenance" \
  SKIP_SSR_RESTART="$skip_ssr_restart" \
  SSR_SUPERVISOR_PROGRAM="$(printf '%q' "$ssr_supervisor_program")" \
  DEPLOY_MODE="$(printf '%q' "$deploy_mode")" \
  RUN_MEILISEARCH_SETUP="$run_meilisearch_setup" \
  FULL_UPGRADE_PRE_HOOK="$(printf '%q' "$full_upgrade_pre_hook")" \
  FULL_UPGRADE_POST_HOOK="$(printf '%q' "$full_upgrade_post_hook")" \
  MEILISEARCH_MODELS_CSV="$(printf '%q' "$meilisearch_models_csv")" \
  HEALTHCHECK_URL="$(printf '%q' "$healthcheck_url")" \
  DEPLOY_ROLLBACK="$deploy_rollback" \
  RELOAD_PHP_FPM="$reload_php_fpm" \
  PHP_FPM_SERVICE="$(printf '%q' "$php_fpm_service")" \
  'bash -s' <<'EOF_REMOTE_DEPLOY'
set -euo pipefail

release_path="${REMOTE_RELEASE_ROOT}/releases/${RELEASE_ID}"
current_link="${REMOTE_RELEASE_ROOT}/current"
current_app_path="$REMOTE_FOLDER"
previous_release_id=""
release_switched=0
deploy_switch_safe=0
declare -a storage_exclude_args=()

log_step() {
  printf '\n==> %s\n' "$1"
}

log_warn() {
  printf 'WARN: %s\n' "$1" >&2
}

die() {
  printf 'ERROR: %s\n' "$1" >&2
  exit 1
}

ensure_dir() {
  local d
  for d in "$@"; do
    mkdir -p "$d"
  done
}

ensure_laravel_shared_storage_layout() {
  ensure_dir \
    "${REMOTE_SHARED_ROOT}/storage/app/public" \
    "${REMOTE_SHARED_ROOT}/storage/framework/cache/data" \
    "${REMOTE_SHARED_ROOT}/storage/framework/sessions" \
    "${REMOTE_SHARED_ROOT}/storage/framework/views" \
    "${REMOTE_SHARED_ROOT}/storage/logs"
}

acquire_deploy_lock() {
  if command -v flock >/dev/null 2>&1; then
    exec 9>"${REMOTE_RELEASE_ROOT}/deploy.lock"
    flock -n 9 || die "Another deployment is already running for ${REMOTE_RELEASE_ROOT}."
  else
    log_warn "flock is not available on the remote server; continuing without a deploy lock."
  fi
}

atomic_symlink() {
  local target="$1"
  local link_path="$2"
  local tmp_link="${link_path}.tmp-${RELEASE_ID}-$$"

  ln -sfn "$target" "$tmp_link"
  mv -Tf "$tmp_link" "$link_path"
}

build_storage_exclude_args() {
  local detected_excludes=()
  local normalized=""

  mapfile -t detected_excludes < <(find_auto_storage_excludes "${REMOTE_SHARED_ROOT}/storage")

  for normalized in "${detected_excludes[@]}"; do
    storage_exclude_args+=(--exclude "$normalized")
  done

  while IFS= read -r normalized; do
    normalized="${normalized#storage/}"
    normalized="${normalized#/}"
    [[ -n "$normalized" ]] || continue
    storage_exclude_args+=(--exclude "$normalized")
  done < <(printf '%s' "${REMOTE_SHARED_STORAGE_EXCLUDES:-}" | tr ',' '\n')
}

find_auto_storage_excludes() {
  local storage_root="$1"

  [[ -d "${storage_root}/app" ]] || return 0

  find "${storage_root}/app" \
    \( -type d -o -type f \) \
    \( -iname '*backup*' -o -iname '*.bak' -o -iname '*.bak.*' \) \
    -printf '%P\n' \
    | sed 's#^#app/#' \
    | awk '!seen[$0]++'
}

ensure_php_runtime_dir() {
  local target_dir="$1"
  local -a privileged_cmd=()

  if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
    privileged_cmd=(sudo -n)
  elif [[ "$(id -u)" -eq 0 ]]; then
    privileged_cmd=()
  fi

  if [[ ! -d "$target_dir" ]]; then
    if [[ ${#privileged_cmd[@]} -gt 0 ]]; then
      "${privileged_cmd[@]}" mkdir -p "$target_dir"
    else
      mkdir -p "$target_dir"
    fi
  fi

  if [[ ${#privileged_cmd[@]} -gt 0 || "$(id -u)" -eq 0 ]]; then
    "${privileged_cmd[@]}" chown -R skinbase:skinbase "$target_dir"
    "${privileged_cmd[@]}" chmod 770 "$target_dir"
    return
  fi

  chmod 770 "$target_dir" >/dev/null 2>&1 || true
}

ensure_shared_env_readable() {
  local env_path="${REMOTE_SHARED_ROOT}/.env"
  local current_user
  local env_details="details=unavailable"

  [[ -f "$env_path" ]] || die "Shared production .env is missing at ${env_path}"
  [[ -r "$env_path" ]] && return 0

  current_user="$(id -un)"

  if command -v stat >/dev/null 2>&1; then
    env_details="$(stat -c 'owner=%U group=%G mode=%a path=%n' "$env_path" 2>/dev/null || printf 'details=unavailable path=%s' "$env_path")"
  else
    env_details="$(ls -ld "$env_path" 2>/dev/null || printf 'details=unavailable path=%s' "$env_path")"
  fi

  if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then
    sudo -n setfacl -m "u:${current_user}:r" "$env_path" >/dev/null 2>&1 || true
  fi

  [[ -r "$env_path" ]] && return 0

  die "Shared production .env exists but is not readable by ${current_user}. ${env_details}. Fix permissions or ACLs on ${env_path} before deploy."
}

link_shared_paths() {
  local target_release="$1"

  ensure_dir "$target_release/bootstrap/cache" "$target_release/public" "$target_release/var"

  rm -f "$target_release/.env"
  ln -sfn "${REMOTE_SHARED_ROOT}/.env" "$target_release/.env"

  rm -rf "$target_release/storage"
  ln -sfn "${REMOTE_SHARED_ROOT}/storage" "$target_release/storage"

  rm -rf "$target_release/public/files"
  ln -sfn "${REMOTE_SHARED_ROOT}/public/files" "$target_release/public/files"

  rm -f "$target_release/public/sitemap.xml"
  ln -sfn "${REMOTE_SHARED_ROOT}/public/sitemaps/sitemap.xml" "$target_release/public/sitemap.xml"

  rm -rf "$target_release/public/sitemaps"
  ln -sfn "${REMOTE_SHARED_ROOT}/public/sitemaps" "$target_release/public/sitemaps"

  rm -rf "$target_release/var/php-tmp"
  ln -sfn "${REMOTE_SHARED_ROOT}/var/php-tmp" "$target_release/var/php-tmp"

  rm -rf "$target_release/var/php-sessions"
  ln -sfn "${REMOTE_SHARED_ROOT}/var/php-sessions" "$target_release/var/php-sessions"

  rm -rf "$target_release/public/storage"
  ln -sfn "${REMOTE_SHARED_ROOT}/storage/app/public" "$target_release/public/storage"
}

current_release_id() {
  if [[ -L "$current_link" ]]; then
    basename "$(readlink "$current_link")"
    return 0
  fi

  printf '%s\n' ''
}

bring_app_up() {
  if [[ "$SKIP_MAINTENANCE" -eq 0 && -f "$current_app_path/artisan" ]]; then
    "$PHP_BIN" "$current_app_path/artisan" up >/dev/null 2>&1 || true
  fi
}

rollback_to_previous_release() {
  local previous_release_path=""

  [[ "${DEPLOY_ROLLBACK:-1}" -eq 1 ]] || return 0
  [[ -n "$previous_release_id" ]] || return 0

  previous_release_path="${REMOTE_RELEASE_ROOT}/releases/${previous_release_id}"

  if [[ -d "$previous_release_path" ]]; then
    log_warn "Deploy failed before safe point. Rolling back current release to ${previous_release_id}."
    atomic_symlink "$previous_release_path" "$current_link" || true
    atomic_symlink "$current_link" "$current_app_path" || true
    bring_app_up
  else
    log_warn "Deploy failed, but previous release path is missing: ${previous_release_path}"
    bring_app_up
  fi
}

on_exit() {
  local exit_code="$1"

  if [[ "$exit_code" -ne 0 ]]; then
    if [[ "${release_switched:-0}" -eq 1 && "${deploy_switch_safe:-0}" -eq 0 ]]; then
      rollback_to_previous_release
    else
      bring_app_up
    fi
  fi
}

run_remote_hook() {
  local hook_name="$1"
  local hook_command="$2"

  [[ -n "$hook_command" ]] || return 0

  log_step "Running ${hook_name}"
  bash -lc "$hook_command"
}

reload_php_fpm_if_requested() {
  [[ "${RELOAD_PHP_FPM:-0}" -eq 1 ]] || return 0

  log_step "Reloading PHP-FPM"
  if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
    sudo -n systemctl reload "$PHP_FPM_SERVICE" || log_warn "PHP-FPM reload failed for service ${PHP_FPM_SERVICE}."
  elif [[ "$(id -u)" -eq 0 ]]; then
    systemctl reload "$PHP_FPM_SERVICE" || log_warn "PHP-FPM reload failed for service ${PHP_FPM_SERVICE}."
  else
    log_warn "Cannot reload PHP-FPM without root or passwordless sudo. Continuing."
  fi
}

run_health_check() {
  [[ -n "${HEALTHCHECK_URL:-}" ]] || return 0

  log_step "Running HTTP health check"
  command -v curl >/dev/null 2>&1 || die "curl is required on the remote server for --healthcheck-url."

  curl -fsS --max-time 15 --retry 3 --retry-delay 2 "$HEALTHCHECK_URL" >/dev/null
}

repair_release_permissions_for_deletion() {
  local target_path="$1"
  local current_user
  local current_group

  [[ -e "$target_path" ]] || return 0

  current_user="$(id -un)"
  current_group="$(id -gn)"

  find "$target_path" -mindepth 0 -user "$current_user" -exec chmod u+rwX {} + 2>/dev/null || true
  find "$target_path" -mindepth 0 -group "$current_group" -exec chmod g+rwX {} + 2>/dev/null || true
  chmod u+rwx "$target_path" >/dev/null 2>&1 || true
  chmod g+rwx "$target_path" >/dev/null 2>&1 || true
}

find_release_delete_blocked_path() {
  local target_path="$1"
  local delete_output="$2"
  local blocked_path=""

  blocked_path="$(printf '%s\n' "$delete_output" | sed -n "s/^rm: cannot [^']*'\([^']*\)': Permission denied$/\1/p" | head -n 1)"

  if [[ -n "$blocked_path" && -e "$blocked_path" ]]; then
    printf '%s' "$blocked_path"
    return 0
  fi

  blocked_path="$(find "$target_path" -mindepth 0 \( ! -writable -o ! -executable \) -print 2>/dev/null | head -n 1)"
  printf '%s' "$blocked_path"
}

remove_release_with_retry() {
  local target_path="$1"
  local delete_output=""

  if delete_output="$(rm -rf -- "$target_path" 2>&1)"; then
    RELEASE_DELETE_ERROR=""
    return 0
  fi

  repair_release_permissions_for_deletion "$target_path"

  if delete_output="$(rm -rf -- "$target_path" 2>&1)"; then
    RELEASE_DELETE_ERROR=""
    return 0
  fi

  if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
    if delete_output="$(sudo -n rm -rf -- "$target_path" 2>&1)"; then
      RELEASE_DELETE_ERROR=""
      return 0
    fi
  fi

  RELEASE_DELETE_ERROR="$delete_output"
  return 1
}

resolve_ssr_supervisor_target() {
  local preferred_program="${SSR_SUPERVISOR_PROGRAM:-skinbase-ssr}"
  local detected_program=""

  if supervisorctl status "$preferred_program" >/dev/null 2>&1; then
    printf '%s' "$preferred_program"
    return 0
  fi

  detected_program="$(supervisorctl status 2>/dev/null | awk 'tolower($1) ~ /(ssr|inertia)/ { print $1; exit }')"

  if [[ -n "$detected_program" ]]; then
    printf '%s' "$detected_program"
    return 0
  fi

  return 1
}

prune_old_releases() {
  local -a releases=()
  local current_release
  local prune_count
  local release_name
  local blocked_path=""
  local blocked_details="details=unavailable"
  local delete_output=""

  mapfile -t releases < <(find "${REMOTE_RELEASE_ROOT}/releases" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\n' | sort -n | awk '{print $2}')
  current_release="$(current_release_id)"

  if (( ${#releases[@]} <= RELEASE_RETENTION )); then
    return
  fi

  prune_count=$(( ${#releases[@]} - RELEASE_RETENTION ))
  for (( i=0; i<${#releases[@]} && prune_count>0; i++ )); do
    release_name="$(basename "${releases[$i]}")"

    if [[ "$release_name" == "$current_release" ]]; then
      continue
    fi

    if remove_release_with_retry "${releases[$i]}"; then
      rm -f "${REMOTE_RELEASE_ROOT}/deployments/${release_name}.json"
    else
      delete_output="${RELEASE_DELETE_ERROR:-}"
      printf '%s\n' "$delete_output" | grep -v 'Permission denied' || true
      blocked_path="$(find_release_delete_blocked_path "${releases[$i]}" "$delete_output")"

      if [[ -n "$blocked_path" ]]; then
        if command -v stat >/dev/null 2>&1; then
          blocked_details="$(stat -c 'owner=%U group=%G mode=%a path=%n' "$blocked_path" 2>/dev/null || printf 'details=unavailable path=%s' "$blocked_path")"
        else
          blocked_details="$(ls -ld "$blocked_path" 2>/dev/null || printf 'details=unavailable path=%s' "$blocked_path")"
        fi
      fi

      echo "WARNING: Could not fully remove old release ${releases[$i]} after retry. ${blocked_details}. Manual cleanup may be needed." >&2
    fi
    prune_count=$(( prune_count - 1 ))
  done
}

trap 'rc=$?; on_exit "$rc"; exit "$rc"' EXIT

[[ -d "$release_path" ]] || die "Release path does not exist: ${release_path}"
ensure_dir "$REMOTE_RELEASE_ROOT"
acquire_deploy_lock
ensure_dir "$REMOTE_SHARED_ROOT" "${REMOTE_RELEASE_ROOT}/deployments"
ensure_laravel_shared_storage_layout
build_storage_exclude_args
ensure_php_runtime_dir "${REMOTE_SHARED_ROOT}/var/php-tmp"
ensure_php_runtime_dir "${REMOTE_SHARED_ROOT}/var/php-sessions"
link_shared_paths "$release_path"
ensure_shared_env_readable

previous_release_id="$(current_release_id)"

if [[ "$DEPLOY_MODE" == "full-upgrade" ]]; then
  run_remote_hook "full-upgrade pre-hook" "${FULL_UPGRADE_PRE_HOOK:-}"
fi

log_step "Installing Composer dependencies in staged release"
cd "$release_path"
"$COMPOSER_BIN" install --no-dev --prefer-dist --optimize-autoloader --no-interaction

if [[ "$SKIP_MAINTENANCE" -eq 0 && -f "$current_app_path/artisan" ]]; then
  log_step "Enabling maintenance mode"
  "$PHP_BIN" "$current_app_path/artisan" down --retry=60 || true
fi

log_step "Switching current release to ${RELEASE_ID}"
atomic_symlink "$release_path" "$current_link"
atomic_symlink "$current_link" "$current_app_path"
release_switched=1

cd "$current_app_path"

if [[ "$RUN_REMOTE_MIGRATIONS" -eq 1 ]]; then
  log_step "Running database migrations"
  "$PHP_BIN" artisan migrate --force
fi

log_step "Refreshing caches"
"$PHP_BIN" artisan view:clear
"$PHP_BIN" artisan optimize:clear
"$PHP_BIN" artisan optimize
"$PHP_BIN" artisan view:cache

log_step "Ensuring public/storage symlink"
"$PHP_BIN" artisan storage:link --force || true

if [[ "$SKIP_MAINTENANCE" -eq 0 ]]; then
  log_step "Bringing application back online"
  "$PHP_BIN" artisan up
fi

reload_php_fpm_if_requested
run_health_check
deploy_switch_safe=1
trap - EXIT

if ! "$PHP_BIN" artisan homepage:warm-guest-cache; then
  log_warn "Homepage guest cache warm failed during deploy."
fi

if ! "$PHP_BIN" artisan posts:warm-trending; then
  log_warn "Post trending cache warm failed during deploy."
fi

log_step "Restarting queue workers"
"$PHP_BIN" artisan queue:restart || true

log_step "Restarting Horizon workers"
"$PHP_BIN" artisan horizon:terminate >/dev/null 2>&1 || true

if [[ "${SKIP_SSR_RESTART:-0}" -eq 0 ]]; then
  log_step "Restarting Inertia SSR server"
  restart_ssr_with_artisan() {
    "$PHP_BIN" artisan inertia:stop-ssr >/dev/null 2>&1 || true
    nohup "$PHP_BIN" artisan inertia:start-ssr >/tmp/skinbase-inertia-ssr.log 2>&1 < /dev/null &
  }

  if command -v supervisorctl >/dev/null 2>&1; then
    ssr_supervisor_target="$(resolve_ssr_supervisor_target || true)"

    if [[ -n "$ssr_supervisor_target" ]]; then
      if [[ "$ssr_supervisor_target" != "${SSR_SUPERVISOR_PROGRAM:-skinbase-ssr}" ]]; then
        printf '  -> Using detected Supervisor program: %s\n' "$ssr_supervisor_target"
      fi

      supervisorctl restart "$ssr_supervisor_target" || {
        log_warn "supervisorctl restart ${ssr_supervisor_target} failed — falling back to artisan SSR restart."
        restart_ssr_with_artisan || log_warn "artisan SSR restart failed — SSR server may need a manual restart."
      }
    else
      log_warn "Supervisor program '${SSR_SUPERVISOR_PROGRAM:-skinbase-ssr}' not found — falling back to artisan SSR restart. Set SSR_SUPERVISOR_PROGRAM if the server uses a different name."
      restart_ssr_with_artisan || log_warn "artisan SSR restart failed — add deploy/supervisor/skinbase-ssr.conf to /etc/supervisor/conf.d/ and run 'supervisorctl reread && supervisorctl update'."
    fi
  else
    log_warn "supervisorctl not available — falling back to artisan SSR restart."
    restart_ssr_with_artisan || log_warn "artisan SSR restart failed — SSR server may need a manual restart."
  fi
fi

if [[ "$RUN_MEILISEARCH_SETUP" -eq 1 ]]; then
  if [[ -z "${MEILISEARCH_MODELS_CSV:-}" ]]; then
    MEILISEARCH_MODELS_CSV='App\Models\Artwork,App\Models\User,App\Models\Group,App\Models\Post,App\Models\Message'
  fi

  IFS=',' read -r -a meilisearch_models <<< "$MEILISEARCH_MODELS_CSV"

  log_step "Importing searchable models into Meilisearch (auto-creates indexes)"
  for model in "${meilisearch_models[@]}"; do
    [[ -n "$model" ]] || continue
    printf '  -> %s\n' "$model"
    "$PHP_BIN" artisan scout:import "$model"
  done
  log_step "Syncing Meilisearch index settings"
  "$PHP_BIN" artisan scout:sync-index-settings
  log_step "Meilisearch setup complete"
fi

if [[ "$DEPLOY_MODE" == "full-upgrade" ]]; then
  run_remote_hook "full-upgrade post-hook" "${FULL_UPGRADE_POST_HOOK:-}"
fi

cat > "${REMOTE_RELEASE_ROOT}/deployments/${RELEASE_ID}.json" <<JSON
{
  "release_id": "${RELEASE_ID}",
  "deployed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "remote_folder": "${REMOTE_FOLDER}",
  "deployment_mode": "${DEPLOY_MODE}",
  "release_path": "${release_path}",
  "previous_release_id": "${previous_release_id}",
  "meilisearch_refreshed": ${RUN_MEILISEARCH_SETUP},
  "healthcheck_url": "${HEALTHCHECK_URL}"
}
JSON

cat > "${REMOTE_RELEASE_ROOT}/current-release.json" <<JSON
{
  "release_id": "${RELEASE_ID}",
  "deployed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "remote_folder": "${REMOTE_FOLDER}",
  "release_path": "${release_path}",
  "previous_release_id": "${previous_release_id}",
  "deployment_mode": "${DEPLOY_MODE}",
  "meilisearch_refreshed": ${RUN_MEILISEARCH_SETUP},
  "healthcheck_url": "${HEALTHCHECK_URL}"
}
JSON

printf '%s\n' "${RELEASE_ID}" > "${REMOTE_RELEASE_ROOT}/current-release.txt"
prune_old_releases
EOF_REMOTE_DEPLOY

# Remote deploy completed successfully; no need for the local DB-maintenance safety trap anymore.
db_sync_remote_maintenance=0
trap - EXIT

log_step "Deployment complete"
