#!/usr/bin/env bash
# Laravel deployer — any Laravel project, VPS or cPanel shared hosting.
# Place this script next to the project zip, then run:  bash deploy.sh
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Directory where the user launched the script (before any cd).
START_CWD="$(pwd -L 2>/dev/null || pwd)"
STARTED_AT="$(date +%s)"

# ── defaults ────────────────────────────────────────────────────────────────
ZIP_PATH=""
APP_DIR=""
IN_PLACE=0
ASSUME_YES=0
SKIP_SEED=0
SKIP_DATA_MIGRATE=0
SKIP_NPM=1
SKIP_COMPOSER=0
SKIP_PUBLIC_LINK=0
WITH_DEV=0
CREATE_DB=1
LINK_PUBLIC=1
LINK_DIR=""
PUBLIC_LINK_PATH=""
WEB_USER=""
WEB_GROUP=""

PHP_BIN=""
PHP_BIN_LOCKED=0
COMPOSER_BIN=""
PHP_MIN="8.1"
IS_CPANEL=0

APP_NAME="Laravel"
APP_ENV="production"
APP_DEBUG="false"
APP_URL=""
APP_TIMEZONE="UTC"
DB_CONNECTION="mysql"
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_DATABASE="laravel"
DB_USERNAME="laravel"
DB_PASSWORD=""

LOG_DIR=""
LOG_FILE=""

# ── tty / colors / glyphs ───────────────────────────────────────────────────
IS_TTY=0
[[ -t 1 ]] && IS_TTY=1

if [[ $IS_TTY -eq 1 ]] && command -v tput >/dev/null 2>&1 && [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then
  C_RESET="$(tput sgr0)"
  C_BOLD="$(tput bold)"
  C_DIM="$(tput dim)"
  C_RED="$(tput setaf 1)"
  C_GREEN="$(tput setaf 2)"
  C_YELLOW="$(tput setaf 3)"
  C_CYAN="$(tput setaf 6)"
else
  C_RESET="" C_BOLD="" C_DIM="" C_RED="" C_GREEN="" C_YELLOW="" C_CYAN=""
fi

UTF8=0
if [[ "$(locale charmap 2>/dev/null || true)" == "UTF-8" ]]; then
  UTF8=1
fi

if [[ $UTF8 -eq 1 ]]; then
  G_OK="✓"
  G_FAIL="✗"
  G_DOT="●"
  G_WARN="!"
  SPINNER_FRAMES=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
else
  G_OK="OK"
  G_FAIL="X"
  G_DOT="*"
  G_WARN="!"
  SPINNER_FRAMES=("|" "/" "-" "\\")
fi

TERM_COLS="$(tput cols 2>/dev/null || echo 80)"
[[ "$TERM_COLS" -lt 50 ]] && TERM_COLS=80
[[ "$TERM_COLS" -gt 100 ]] && TERM_COLS=80

hide_cursor() { [[ $IS_TTY -eq 1 ]] && tput civis 2>/dev/null || true; }
show_cursor() { [[ $IS_TTY -eq 1 ]] && tput cnorm 2>/dev/null || true; }

cleanup_ui() { show_cursor; }
trap cleanup_ui EXIT
trap 'kill $(jobs -p) 2>/dev/null || true; echo; die "Interrupted by user."' INT TERM

# ── output helpers ──────────────────────────────────────────────────────────
hr() {
  printf "${C_DIM}%*s${C_RESET}\n" "$TERM_COLS" "" | tr ' ' '-'
}

blank() { echo; }

info()  { printf "  ${C_CYAN}%s${C_RESET}  %s\n" "$G_DOT" "$*"; }
ok()    { printf "  ${C_GREEN}%s${C_RESET}  %s\n" "$G_OK" "$*"; }
warn()  { printf "  ${C_YELLOW}%s${C_RESET}  %s\n" "$G_WARN" "$*"; }
err()   { printf "  ${C_RED}%s${C_RESET}  %s\n" "$G_FAIL" "$*" >&2; }

init_log() {
  if [[ -n "${HOME:-}" && -d "${HOME}/tmp" && -w "${HOME}/tmp" ]]; then
    LOG_DIR="${HOME}/tmp"
  elif [[ -w /tmp ]]; then
    LOG_DIR="/tmp"
  else
    LOG_DIR="$SCRIPT_DIR"
  fi
  LOG_FILE="${LOG_DIR}/laravel-deploy-$(date +%Y%m%d-%H%M%S).log"
  : >"$LOG_FILE"
  echo "Laravel deploy log $(date '+%Y-%m-%dT%H:%M:%S')" >>"$LOG_FILE"
}

die() {
  show_cursor
  blank
  err "$1"
  if [[ -n "${LOG_FILE:-}" && -f "$LOG_FILE" ]]; then
    printf "  ${C_DIM}Log: %s${C_RESET}\n" "$LOG_FILE" >&2
    if [[ -s "$LOG_FILE" ]]; then
      blank
      printf "  ${C_DIM}Last log lines:${C_RESET}\n" >&2
      tail -n 25 "$LOG_FILE" 2>/dev/null | sed "s/^/    /" >&2 || true
    fi
  fi
  blank
  exit 1
}

usage() {
  cat <<EOF
${C_BOLD}Laravel deployer${C_RESET}

Works with any Laravel zip, on a VPS or cPanel shared hosting.

  ${C_CYAN}bash deploy.sh${C_RESET}

Non-interactive example:

  bash deploy.sh --yes \\
    --zip ./app.zip \\
    --dir \$HOME/myapp \\
    --app-url https://example.com \\
    --db-name account_app --db-user account_app --db-pass 'secret'

Options:
  --zip PATH            Project zip (auto-detected if omitted)
  --dir PATH            Install directory
  --in-place            Configure an existing Laravel directory
  --php PATH            PHP binary (auto-detected, including cPanel ea-php)
  --link-dir PATH       Where to create the public symlink (default: current directory)
  --skip-public-link    Do not create the public symlink
  --yes, -y             Accept defaults; no prompts
  --app-name NAME       APP_NAME
  --app-url URL         APP_URL
  --app-env ENV         local | staging | production  (default: production)
  --db-connection NAME  mysql | sqlite | pgsql  (default: mysql)
  --db-host HOST
  --db-port PORT
  --db-name NAME
  --db-user USER
  --db-pass PASS
  --web-user USER       Owner of writable dirs (VPS / root only)
  --with-dev            Install Composer dev packages
  --with-npm            Build frontend assets (npm)
  --skip-composer       Skip composer install (use vendor inside the zip)
  --skip-seed           Skip php artisan db:seed
  --skip-data-migrate   Skip database/migrations/data if that folder exists
  --no-create-db        Do not CREATE DATABASE
  -h, --help            Show this help
EOF
}

# ── prompts ─────────────────────────────────────────────────────────────────
need_tty_or_yes() {
  if [[ ! -t 0 && $ASSUME_YES -ne 1 ]]; then
    die "No terminal for prompts. Re-run with --yes and the required flags."
  fi
}

ask() {
  local prompt="$1"
  local default="${2:-}"
  local secret="${3:-0}"
  local reply=""

  if [[ $ASSUME_YES -eq 1 ]]; then
    printf '%s\n' "$default"
    return 0
  fi
  need_tty_or_yes

  if [[ -n "$default" ]]; then
    printf "  ${C_BOLD}%s${C_RESET} ${C_DIM}[%s]${C_RESET}: " "$prompt" "$default" >&2
  else
    printf "  ${C_BOLD}%s${C_RESET}: " "$prompt" >&2
  fi

  if [[ "$secret" == "1" ]]; then
    read -r -s reply || true
    echo >&2
  else
    read -r reply || true
  fi

  if [[ -z "$reply" ]]; then
    printf '%s\n' "$default"
  else
    printf '%s\n' "$reply"
  fi
}

confirm() {
  local prompt="$1"
  local default="${2:-y}"
  local reply=""
  local hint="Y/n"

  [[ "$default" == "n" || "$default" == "N" ]] && hint="y/N"

  if [[ $ASSUME_YES -eq 1 ]]; then
    [[ "$default" == "y" || "$default" == "Y" ]]
    return
  fi
  need_tty_or_yes

  printf "  ${C_BOLD}%s${C_RESET} ${C_DIM}[%s]${C_RESET}: " "$prompt" "$hint" >&2
  read -r reply || true
  reply="${reply:-$default}"
  [[ "$reply" == "y" || "$reply" == "Y" || "$reply" == "yes" || "$reply" == "YES" ]]
}

choose() {
  local prompt="$1"
  shift
  local options=("$@")
  local i reply

  if [[ $ASSUME_YES -eq 1 ]]; then
    printf '%s\n' "${options[0]}"
    return 0
  fi
  need_tty_or_yes

  printf "  ${C_BOLD}%s${C_RESET}\n" "$prompt" >&2
  for i in "${!options[@]}"; do
    printf "    ${C_CYAN}%d)${C_RESET}  %s\n" "$((i + 1))" "${options[$i]}" >&2
  done
  printf "  ${C_DIM}Choice [1]:${C_RESET} " >&2
  read -r reply || true
  reply="${reply:-1}"
  if ! [[ "$reply" =~ ^[0-9]+$ ]] || (( reply < 1 || reply > ${#options[@]} )); then
    die "Invalid choice: $reply"
  fi
  printf '%s\n' "${options[$((reply - 1))]}"
}

# ── args ────────────────────────────────────────────────────────────────────
parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -h|--help) usage; exit 0 ;;
      --zip) ZIP_PATH="${2:-}"; shift 2 ;;
      --dir) APP_DIR="${2:-}"; shift 2 ;;
      --in-place) IN_PLACE=1; shift ;;
      --php) PHP_BIN="${2:-}"; PHP_BIN_LOCKED=1; shift 2 ;;
      --link-dir|--public-html) LINK_DIR="${2:-}"; shift 2 ;;
      --skip-public-link|--skip-public-html) SKIP_PUBLIC_LINK=1; LINK_PUBLIC=0; shift ;;
      -y|--yes) ASSUME_YES=1; shift ;;
      --app-name) APP_NAME="${2:-}"; shift 2 ;;
      --app-url) APP_URL="${2:-}"; shift 2 ;;
      --app-env) APP_ENV="${2:-}"; shift 2 ;;
      --db-connection) DB_CONNECTION="${2:-}"; shift 2 ;;
      --db-host) DB_HOST="${2:-}"; shift 2 ;;
      --db-port) DB_PORT="${2:-}"; shift 2 ;;
      --db-name) DB_DATABASE="${2:-}"; shift 2 ;;
      --db-user) DB_USERNAME="${2:-}"; shift 2 ;;
      --db-pass) DB_PASSWORD="${2:-}"; shift 2 ;;
      --web-user) WEB_USER="${2:-}"; shift 2 ;;
      --with-dev) WITH_DEV=1; shift ;;
      --with-npm) SKIP_NPM=0; shift ;;
      --skip-composer) SKIP_COMPOSER=1; shift ;;
      --skip-seed) SKIP_SEED=1; shift ;;
      --skip-data-migrate) SKIP_DATA_MIGRATE=1; shift ;;
      --no-create-db) CREATE_DB=0; shift ;;
      *) die "Unknown option: $1  (try --help)" ;;
    esac
  done
}

# ── steps runner ────────────────────────────────────────────────────────────
STEP_TOTAL=0
STEP_INDEX=0
set_steps() { STEP_TOTAL="$1"; STEP_INDEX=0; }

run_step() {
  local title="$1"
  shift
  STEP_INDEX=$((STEP_INDEX + 1))
  local label
  printf -v label "[%d/%d]" "$STEP_INDEX" "$STEP_TOTAL"

  local start now elapsed spinner_pid=0
  start="$(date +%s)"

  if [[ $IS_TTY -eq 1 ]]; then
    hide_cursor
    (
      local i=0
      while true; do
        printf "\r  ${C_DIM}%s${C_RESET}  %-42s  ${C_CYAN}%s${C_RESET}" \
          "$label" "$title" "${SPINNER_FRAMES[$i]}"
        i=$(( (i + 1) % ${#SPINNER_FRAMES[@]} ))
        sleep 0.08
      done
    ) &
    spinner_pid=$!
  else
    printf "  %s  %s ...\n" "$label" "$title"
  fi

  if "$@" >>"$LOG_FILE" 2>&1; then
    if [[ "$spinner_pid" -ne 0 ]]; then
      kill "$spinner_pid" >/dev/null 2>&1 || true
      wait "$spinner_pid" 2>/dev/null || true
    fi
    now="$(date +%s)"
    elapsed=$((now - start))
    if [[ $IS_TTY -eq 1 ]]; then
      printf "\r\033[2K  ${C_GREEN}%s${C_RESET}  %-42s  ${C_DIM}%ss${C_RESET}\n" \
        "$G_OK" "$title" "$elapsed"
      show_cursor
    else
      printf "  %s  %s (%ss)\n" "$G_OK" "$title" "$elapsed"
    fi
  else
    if [[ "$spinner_pid" -ne 0 ]]; then
      kill "$spinner_pid" >/dev/null 2>&1 || true
      wait "$spinner_pid" 2>/dev/null || true
    fi
    show_cursor
    if [[ $IS_TTY -eq 1 ]]; then
      printf "\r\033[2K  ${C_RED}%s${C_RESET}  %s\n" "$G_FAIL" "$title"
    fi
    die "Step failed: $title"
  fi
}

# ── detect host / php / composer ────────────────────────────────────────────
detect_cpanel() {
  IS_CPANEL=0
  if [[ -d /usr/local/cpanel || -d /var/cpanel || -x /usr/local/cpanel/cpanel ]]; then
    IS_CPANEL=1
  elif [[ -n "${HOME:-}" && -d "${HOME}/public_html" && -d "${HOME}/.cpanel" ]]; then
    IS_CPANEL=1
  elif [[ -n "${HOME:-}" && -d "${HOME}/public_html" && "$(id -u)" -ne 0 ]]; then
    # Common shared layout even without a visible .cpanel dir
    if [[ -d /opt/cpanel || -x /usr/local/bin/ea-php83 || -d /opt/cpanel/ea-php83 ]]; then
      IS_CPANEL=1
    fi
  fi

}

php_version_of() {
  local bin="$1"
  "$bin" -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION.".".PHP_RELEASE_VERSION;' 2>/dev/null || true
}

php_meets_min() {
  local bin="$1"
  local min="$2"
  "$bin" -r 'exit(version_compare(PHP_VERSION, $argv[1], ">=") ? 0 : 1);' -- "$min" 2>/dev/null
}

detect_php() {
  if [[ -n "$PHP_BIN" ]]; then
    [[ -x "$PHP_BIN" ]] || die "PHP binary not executable: $PHP_BIN"
    php_meets_min "$PHP_BIN" "$PHP_MIN" || die "PHP at $PHP_BIN is too old. Need ${PHP_MIN}+."
    return 0
  fi

  local cands=()
  local p
  for p in \
    /opt/cpanel/ea-php84/root/usr/bin/php \
    /opt/cpanel/ea-php83/root/usr/bin/php \
    /opt/cpanel/ea-php82/root/usr/bin/php \
    /opt/cpanel/ea-php81/root/usr/bin/php \
    /usr/local/bin/ea-php84 \
    /usr/local/bin/ea-php83 \
    /usr/local/bin/ea-php82 \
    /usr/local/bin/ea-php81 \
    /usr/bin/php8.4 \
    /usr/bin/php8.3 \
    /usr/bin/php8.2 \
    /usr/bin/php8.1 \
    /usr/local/bin/php \
    /usr/bin/php
  do
    [[ -x "$p" ]] && cands+=("$p")
  done
  if command -v php >/dev/null 2>&1; then
    cands+=("$(command -v php)")
  fi

  local best="" best_ver="0"
  local ver
  for p in "${cands[@]}"; do
    php_meets_min "$p" "$PHP_MIN" || continue
    ver="$(php_version_of "$p")"
    [[ -n "$ver" ]] || continue
    if [[ "$(printf '%s\n%s\n' "$best_ver" "$ver" | sort -V | tail -n1)" == "$ver" && "$ver" != "$best_ver" ]]; then
      best="$p"
      best_ver="$ver"
    elif [[ -z "$best" ]]; then
      best="$p"
      best_ver="$ver"
    fi
  done

  [[ -n "$best" ]] || die "No PHP ${PHP_MIN}+ found. On cPanel open MultiPHP Manager and enable PHP ${PHP_MIN} or newer, or pass --php /path/to/php."
  PHP_BIN="$best"
}

run_php() {
  "$PHP_BIN" -d memory_limit=-1 -d display_errors=0 "$@"
}

php_ext_ok() {
  local ext="$1"
  run_php -m 2>/dev/null | grep -qi "^${ext}$"
}

php_fn_disabled() {
  local fn="$1"
  run_php -r '
    $d = array_map("trim", explode(",", (string) ini_get("disable_functions")));
    $d = array_map("strtolower", $d);
    exit(in_array(strtolower($argv[1]), $d, true) ? 0 : 1);
  ' -- "$fn"
}

detect_web_user() {
  if [[ $IS_CPANEL -eq 1 || "$(id -u)" -ne 0 ]]; then
    WEB_USER="$(id -un)"
    WEB_GROUP="$(id -gn)"
    return 0
  fi
  if [[ -z "$WEB_USER" ]]; then
    if id -u www-data >/dev/null 2>&1; then
      WEB_USER="www-data"
    elif id -u nginx >/dev/null 2>&1; then
      WEB_USER="nginx"
    elif id -u apache >/dev/null 2>&1; then
      WEB_USER="apache"
    else
      WEB_USER="$(id -un)"
    fi
  fi
  WEB_GROUP="$(id -gn "$WEB_USER" 2>/dev/null || echo "$WEB_USER")"
}

ensure_composer() {
  if [[ $SKIP_COMPOSER -eq 1 ]]; then
    return 0
  fi
  if [[ -n "$COMPOSER_BIN" ]]; then
    return 0
  fi

  local c
  for c in \
    "$(command -v composer 2>/dev/null || true)" \
    "$SCRIPT_DIR/composer.phar" \
    "${HOME:-}/bin/composer" \
    "${HOME:-}/composer.phar"
  do
    if [[ -n "$c" && -f "$c" ]]; then
      COMPOSER_BIN="$c"
      return 0
    fi
  done

  if php_fn_disabled proc_open; then
    die "Composer needs PHP proc_open, which is disabled. Enable it in cPanel → MultiPHP INI Editor, or upload vendor/ in the zip and re-run with --skip-composer."
  fi

  local dest="${HOME:-$SCRIPT_DIR}/bin"
  mkdir -p "$dest"
  local installer="${TMPDIR:-/tmp}/composer-setup-$$.php"

  echo "Downloading Composer..." >>"$LOG_FILE"
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL https://getcomposer.org/installer -o "$installer" >>"$LOG_FILE" 2>&1 \
      || die "Could not download Composer. Put composer.phar next to this script, or zip vendor/ and use --skip-composer."
  elif command -v wget >/dev/null 2>&1; then
    wget -qO "$installer" https://getcomposer.org/installer >>"$LOG_FILE" 2>&1 \
      || die "Could not download Composer. Put composer.phar next to this script, or zip vendor/ and use --skip-composer."
  else
    die "Neither Composer, curl, nor wget is available. Upload composer.phar or a vendor/ folder."
  fi

  run_php "$installer" --install-dir="$dest" --filename=composer >>"$LOG_FILE" 2>&1 \
    || die "Composer installer failed. See log."
  rm -f "$installer"
  COMPOSER_BIN="$dest/composer"
  chmod +x "$COMPOSER_BIN" || true
}

run_composer() {
  export COMPOSER_MEMORY_LIMIT="${COMPOSER_MEMORY_LIMIT:--1}"
  export COMPOSER_NO_INTERACTION=1
  export COMPOSER_HOME="${COMPOSER_HOME:-${HOME:-$SCRIPT_DIR}/.composer}"
  mkdir -p "$COMPOSER_HOME"
  # Always invoke Composer with the PHP we selected (cPanel's default `php` is often too old).
  run_php "$COMPOSER_BIN" "$@"
}

run_artisan() {
  (cd "$APP_DIR" && run_php artisan "$@")
}

# ── locate project ──────────────────────────────────────────────────────────
find_zip() {
  if [[ -n "$ZIP_PATH" ]]; then
    [[ -f "$ZIP_PATH" ]] || die "Zip not found: $ZIP_PATH"
    ZIP_PATH="$(cd "$(dirname "$ZIP_PATH")" && pwd)/$(basename "$ZIP_PATH")"
    return 0
  fi

  local zips=()
  local f
  while IFS= read -r -d '' f; do
    zips+=("$f")
  done < <(find "$SCRIPT_DIR" -maxdepth 1 -type f -iname '*.zip' -print0 2>/dev/null | sort -z)

  if [[ ${#zips[@]} -eq 0 ]]; then
    return 1
  elif [[ ${#zips[@]} -eq 1 ]]; then
    ZIP_PATH="${zips[0]}"
  else
    local names=()
    for f in "${zips[@]}"; do
      names+=("$(basename "$f")")
    done
    blank
    local picked
    picked="$(choose "Several zip files found. Which one is the Laravel project?" "${names[@]}")"
    for f in "${zips[@]}"; do
      if [[ "$(basename "$f")" == "$picked" ]]; then
        ZIP_PATH="$f"
        break
      fi
    done
  fi
}

find_laravel_root() {
  local root="$1"
  if [[ -f "$root/artisan" && -f "$root/composer.json" ]]; then
    printf '%s\n' "$root"
    return 0
  fi
  local found
  found="$(find "$root" -maxdepth 3 -type f -name artisan 2>/dev/null | head -n 1 || true)"
  if [[ -n "$found" && -f "$(dirname "$found")/composer.json" ]]; then
    printf '%s\n' "$(dirname "$found")"
    return 0
  fi
  return 1
}

env_get() {
  local file="$1"
  local key="$2"
  local fallback="${3:-}"
  [[ -f "$file" ]] || { printf '%s\n' "$fallback"; return 0; }
  local line
  line="$(grep -E "^${key}=" "$file" 2>/dev/null | tail -n1 || true)"
  if [[ -z "$line" ]]; then
    printf '%s\n' "$fallback"
    return 0
  fi
  local val="${line#*=}"
  val="${val%$'\r'}"
  val="${val#\"}"
  val="${val%\"}"
  val="${val#\'}"
  val="${val%\'}"
  printf '%s\n' "$val"
}

apply_project_defaults() {
  local example="$APP_DIR/.env.example"
  local composer="$APP_DIR/composer.json"

  if [[ -f "$composer" ]]; then
    local constraint
    constraint="$(run_php -r '
      $c = json_decode(file_get_contents($argv[1]), true);
      echo $c["require"]["php"] ?? "";
    ' -- "$composer" || true)"
    if [[ -n "$constraint" ]]; then
      local parsed
      parsed="$(printf '%s' "$constraint" | grep -oE '[0-9]+\.[0-9]+' | head -n1 || true)"
      if [[ -n "$parsed" ]]; then
        PHP_MIN="$parsed"
      fi
    fi
    local name
    name="$(run_php -r '
      $c = json_decode(file_get_contents($argv[1]), true);
      $n = $c["name"] ?? "laravel";
      $n = preg_replace("#^.*/#", "", $n);
      echo $n;
    ' -- "$composer" || true)"
    if [[ -n "$name" && "$APP_NAME" == "Laravel" ]]; then
      APP_NAME="$name"
    fi
  fi

  if [[ -f "$example" ]]; then
    APP_NAME="$(env_get "$example" APP_NAME "$APP_NAME")"
    [[ -z "$APP_URL" ]] && APP_URL="$(env_get "$example" APP_URL "")"
    APP_TIMEZONE="$(env_get "$example" APP_TIMEZONE "$APP_TIMEZONE")"
    DB_CONNECTION="$(env_get "$example" DB_CONNECTION "$DB_CONNECTION")"
    local ex_host ex_db ex_user
    ex_host="$(env_get "$example" DB_HOST "")"
    ex_db="$(env_get "$example" DB_DATABASE "")"
    ex_user="$(env_get "$example" DB_USERNAME "")"
    [[ -n "$ex_host" && "$ex_host" != "db" ]] && DB_HOST="$ex_host"
    [[ -n "$ex_db" ]] && DB_DATABASE="$ex_db"
    [[ -n "$ex_user" && "$ex_user" != "root" ]] && DB_USERNAME="$ex_user"
    DB_PORT="$(env_get "$example" DB_PORT "$DB_PORT")"
  fi

  if [[ $IS_CPANEL -eq 1 ]]; then
    [[ "$DB_HOST" == "127.0.0.1" || "$DB_HOST" == "db" ]] && DB_HOST="localhost"
    local prefix="${USER:-$(id -un)}"
    if [[ "$DB_DATABASE" != "${prefix}_"* ]]; then
      DB_DATABASE="${prefix}_${DB_DATABASE}"
    fi
    if [[ "$DB_USERNAME" != "${prefix}_"* ]]; then
      DB_USERNAME="${prefix}_${DB_USERNAME}"
    fi
    CREATE_DB=0
  fi

  if [[ -z "$APP_URL" ]]; then
    APP_URL="https://$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo localhost)"
  fi
}

# ── .env helpers ────────────────────────────────────────────────────────────
set_env_key() {
  local file="$1"
  local key="$2"
  local value="$3"
  run_php -r '
    $file = $argv[1];
    $key = $argv[2];
    $value = $argv[3];
    if (!is_file($file)) {
      fwrite(STDERR, "Missing file: {$file}\n");
      exit(1);
    }
    $raw = file_get_contents($file);
    $lines = preg_split("/\r\n|\n|\r/", $raw);
    if (end($lines) === "") {
      array_pop($lines);
    }
    $quote = $value === "" || preg_match("/[\s#\"\\\\\$]/", $value);
    $encoded = $quote
      ? "\"" . str_replace(["\\", "\""], ["\\\\", "\\\""], $value) . "\""
      : $value;
    $found = false;
    foreach ($lines as $i => $line) {
      if (preg_match("/^" . preg_quote($key, "/") . "=/", $line)) {
        $lines[$i] = $key . "=" . $encoded;
        $found = true;
        break;
      }
    }
    if (!$found) {
      $lines[] = $key . "=" . $encoded;
    }
    file_put_contents($file, implode("\n", $lines) . "\n");
  ' -- "$file" "$key" "$value"
}

# ── work functions ──────────────────────────────────────────────────────────
do_extract() {
  local dest="$1"
  mkdir -p "$dest"
  if command -v unzip >/dev/null 2>&1; then
    unzip -o -q "$ZIP_PATH" -d "$dest"
  elif command -v python3 >/dev/null 2>&1; then
    python3 - "$ZIP_PATH" "$dest" <<'PY'
import sys, zipfile
zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])
PY
  else
    run_php -r '
      $z = new ZipArchive();
      if ($z->open($argv[1]) !== true) { fwrite(STDERR, "cannot open zip\n"); exit(1); }
      if (!$z->extractTo($argv[2])) { fwrite(STDERR, "extract failed\n"); exit(1); }
      $z->close();
    ' -- "$ZIP_PATH" "$dest"
  fi
}

do_composer_install() {
  cd "$APP_DIR"
  if [[ $SKIP_COMPOSER -eq 1 ]]; then
    [[ -f vendor/autoload.php ]] || { echo "vendor/ missing and --skip-composer was set"; return 1; }
    echo "skipped composer install"
    return 0
  fi
  if [[ -f vendor/autoload.php && ! -f composer.lock ]]; then
    echo "vendor present"
  fi
  local args=(install --no-interaction --prefer-dist --optimize-autoloader)
  if [[ $WITH_DEV -eq 0 ]]; then
    args+=(--no-dev)
  fi
  run_composer "${args[@]}"
}

do_write_env() {
  cd "$APP_DIR"
  if [[ ! -f .env ]]; then
    if [[ -f .env.example ]]; then
      cp .env.example .env
    else
      touch .env
    fi
  fi

  local debug="$APP_DEBUG"
  local log_level="debug"
  if [[ "$APP_ENV" == "production" ]]; then
    debug="false"
    log_level="error"
  fi

  set_env_key .env APP_NAME "$APP_NAME"
  set_env_key .env APP_ENV "$APP_ENV"
  set_env_key .env APP_DEBUG "$debug"
  set_env_key .env APP_URL "$APP_URL"
  set_env_key .env APP_TIMEZONE "$APP_TIMEZONE"

  set_env_key .env DB_CONNECTION "$DB_CONNECTION"
  if [[ "$DB_CONNECTION" == "sqlite" ]]; then
    local sqlite_path="$APP_DIR/database/database.sqlite"
    mkdir -p "$APP_DIR/database"
    [[ -f "$sqlite_path" ]] || : >"$sqlite_path"
    set_env_key .env DB_DATABASE "$sqlite_path"
  else
    set_env_key .env DB_HOST "$DB_HOST"
    set_env_key .env DB_PORT "$DB_PORT"
    set_env_key .env DB_DATABASE "$DB_DATABASE"
    set_env_key .env DB_USERNAME "$DB_USERNAME"
    set_env_key .env DB_PASSWORD "$DB_PASSWORD"
  fi

  set_env_key .env LOG_LEVEL "$log_level"

  if [[ $IS_CPANEL -eq 1 ]]; then
    local cache session queue
    cache="$(env_get .env CACHE_STORE "$(env_get .env CACHE_DRIVER file)")"
    session="$(env_get .env SESSION_DRIVER file)"
    queue="$(env_get .env QUEUE_CONNECTION sync)"
    if [[ "$cache" == "redis" || "$cache" == "memcached" ]]; then
      set_env_key .env CACHE_STORE file
    fi
    if [[ "$session" == "redis" ]]; then
      set_env_key .env SESSION_DRIVER file
    fi
    if [[ "$queue" == "redis" ]]; then
      set_env_key .env QUEUE_CONNECTION database
    fi
  fi
}

do_app_key() {
  cd "$APP_DIR"
  if grep -qE '^APP_KEY=base64:' .env 2>/dev/null; then
    echo "APP_KEY already set"
    return 0
  fi
  run_artisan key:generate --force --ansi
}

do_storage_structure() {
  cd "$APP_DIR"
  mkdir -p \
    storage/app/public \
    storage/framework/cache/data \
    storage/framework/sessions \
    storage/framework/views \
    storage/logs \
    bootstrap/cache \
    database
}

do_permissions() {
  cd "$APP_DIR"
  # Shared hosts run PHP as the account user — 755/644 + writable storage.
  chmod -R u+rwX,go+rX,go-w . 2>/dev/null || true
  chmod -R ug+rwx storage bootstrap/cache
  chmod 755 artisan 2>/dev/null || true

  if [[ "$(id -u)" -eq 0 && $IS_CPANEL -eq 0 ]]; then
    chown -R "${WEB_USER}:${WEB_GROUP}" storage bootstrap/cache
    [[ -d public ]] && chown -R "${WEB_USER}:${WEB_GROUP}" public
  fi
}

do_storage_link() {
  cd "$APP_DIR"
  mkdir -p public storage/app/public
  if run_artisan storage:link --force --ansi; then
    echo "storage linked"
    return 0
  fi
  echo "storage:link failed (symlink often disabled on shared hosting)"
  # Fallback: copy public disk files if the link cannot be created
  if [[ -L public/storage ]]; then
    echo "link exists"
    return 0
  fi
  mkdir -p public/storage
  if command -v rsync >/dev/null 2>&1; then
    rsync -a storage/app/public/ public/storage/ || true
  else
    cp -a storage/app/public/. public/storage/ 2>/dev/null || true
  fi
  echo "copied storage/app/public -> public/storage"
}

do_protect_env() {
  cd "$APP_DIR"
  # If the app was placed where the web root can reach it, deny secrets.
  local deny="$APP_DIR/.htaccess"
  if [[ "$APP_DIR" == *"/public_html"* || "$APP_DIR" == *"/httpdocs"* || "$APP_DIR" == *"/www"* ]]; then
    if [[ ! -f "$deny" ]]; then
      cat >"$deny" <<'HT'
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule ^\.env - [F,L]
    RewriteRule ^composer\.(json|lock)$ - [F,L]
    RewriteRule ^artisan$ - [F,L]
    RewriteRule ^$ public/ [L]
    RewriteRule ^(.*)$ public/$1 [L]
</IfModule>
HT
      echo "wrote deny/rewrite .htaccess at app root"
    fi
  fi
}

relative_path() {
  run_php -r '
    $from = realpath($argv[1]);
    $to = realpath($argv[2]);
    if ($from === false || $to === false) { fwrite(STDERR, "path not found\n"); exit(1); }
    $from = explode(DIRECTORY_SEPARATOR, rtrim($from, DIRECTORY_SEPARATOR));
    $to = explode(DIRECTORY_SEPARATOR, rtrim($to, DIRECTORY_SEPARATOR));
    while (count($from) && count($to) && $from[0] === $to[0]) {
      array_shift($from); array_shift($to);
    }
    $rel = str_repeat("..".DIRECTORY_SEPARATOR, count($from)) . implode(DIRECTORY_SEPARATOR, $to);
    echo str_replace("\\", "/", $rel);
  ' -- "$1" "$2"
}

do_link_public() {
  [[ $LINK_PUBLIC -eq 1 ]] || return 0

  local dir="${LINK_DIR:-$START_CWD}"
  mkdir -p "$dir" "$APP_DIR/public"
  dir="$(cd "$dir" && pwd)"
  local target
  target="$(cd "$APP_DIR/public" && pwd)"
  local link="$dir/public"

  if [[ "$dir" == "$target" ]]; then
    echo "launch directory is already Laravel public"
    PUBLIC_LINK_PATH="$dir"
    return 0
  fi

  if [[ -e "$link" || -L "$link" ]]; then
    if [[ -L "$link" ]]; then
      rm -f "$link"
    elif [[ -d "$link" ]]; then
      local existing
      existing="$(cd "$link" && pwd)"
      if [[ "$existing" == "$target" ]]; then
        echo "public already is the app public folder"
        PUBLIC_LINK_PATH="$link"
        return 0
      fi
      mv "$link" "${link}.bak-$(date +%Y%m%d%H%M%S)"
      echo "moved existing public directory aside"
    else
      rm -f "$link"
    fi
  fi

  local rel=""
  rel="$(relative_path "$dir" "$target" 2>/dev/null || true)"
  if [[ -n "$rel" ]]; then
    if ln -sfn "$rel" "$link"; then
      echo "symlink $link -> $rel"
      PUBLIC_LINK_PATH="$link"
      return 0
    fi
  fi
  if ln -sfn "$target" "$link"; then
    echo "symlink $link -> $target"
    PUBLIC_LINK_PATH="$link"
    return 0
  fi
  echo "Could not create symlink (often disabled on the host). Enable symlink() or point the domain document root to: $target" >&2
  return 1
}

db_create_if_needed() {
  [[ "$DB_CONNECTION" == "sqlite" ]] && return 0
  run_php -r '
    $host = $argv[1]; $port = $argv[2]; $db = $argv[3]; $user = $argv[4]; $pass = $argv[5];
    try {
      $pdo = new PDO("mysql:host={$host};port={$port};charset=utf8mb4", $user, $pass, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      ]);
    } catch (Throwable $e) {
      fwrite(STDERR, $e->getMessage() . PHP_EOL);
      exit(2);
    }
    try {
      $name = str_replace("`", "``", $db);
      $pdo->exec("CREATE DATABASE IF NOT EXISTS `{$name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
      echo "database ready: {$db}\n";
    } catch (Throwable $e) {
      fwrite(STDERR, "Cannot CREATE DATABASE (normal on cPanel). Create it in MySQL Databases, then re-run.\n");
      fwrite(STDERR, $e->getMessage() . PHP_EOL);
      exit(1);
    }
  ' -- "$DB_HOST" "$DB_PORT" "$DB_DATABASE" "$DB_USERNAME" "$DB_PASSWORD"
}

db_ping() {
  if [[ "$DB_CONNECTION" == "sqlite" ]]; then
    [[ -f "$APP_DIR/database/database.sqlite" ]] || : >"$APP_DIR/database/database.sqlite"
    echo "sqlite ok"
    return 0
  fi
  local driver_dsn="mysql"
  [[ "$DB_CONNECTION" == "pgsql" ]] && driver_dsn="pgsql"
  run_php -r '
    $conn = $argv[1]; $host = $argv[2]; $port = $argv[3]; $db = $argv[4]; $user = $argv[5]; $pass = $argv[6];
    $dsn = $conn === "pgsql"
      ? "pgsql:host={$host};port={$port};dbname={$db}"
      : "mysql:host={$host};port={$port};dbname={$db};charset=utf8mb4";
    try {
      $pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
      $pdo->query("SELECT 1");
      echo "ok\n";
    } catch (Throwable $e) {
      fwrite(STDERR, $e->getMessage() . PHP_EOL);
      fwrite(STDERR, PHP_EOL . "On cPanel: MySQL Databases → create DB + user (account_name prefix) → add user to DB with ALL PRIVILEGES." . PHP_EOL);
      exit(1);
    }
  ' -- "$driver_dsn" "$DB_HOST" "$DB_PORT" "$DB_DATABASE" "$DB_USERNAME" "$DB_PASSWORD"
}

do_migrate() {
  run_artisan migrate --force --ansi
}

do_data_migrate() {
  if [[ -d "$APP_DIR/database/migrations/data" ]]; then
    run_artisan migrate --path=database/migrations/data --force --ansi
  fi
}

do_seed() {
  run_artisan db:seed --force --ansi
}

do_optimize() {
  run_artisan optimize:clear --ansi || true
  if [[ "$APP_ENV" == "production" ]]; then
    run_artisan config:cache --ansi
    run_artisan event:cache --ansi || true
    if ! run_artisan route:cache --ansi; then
      echo "route:cache skipped (closure routes are common); config/view still cached"
    fi
    run_artisan view:cache --ansi || true
  fi
}

do_npm() {
  cd "$APP_DIR"
  if [[ ! -f package.json ]]; then
    echo "no package.json"
    return 0
  fi
  if ! command -v npm >/dev/null 2>&1; then
    echo "npm not installed; skipped (typical on shared hosting — build assets locally and include public/build in the zip)"
    return 0
  fi
  if [[ -f package-lock.json ]]; then
    npm ci --ignore-scripts
  else
    npm install --ignore-scripts
  fi
  npm run build
}

# ── screens ─────────────────────────────────────────────────────────────────
banner() {
  blank
  hr
  printf "  ${C_BOLD}${C_CYAN}Laravel${C_RESET}  ${C_DIM}·${C_RESET}  installer\n"
  hr
  blank
  printf "  Prepares any Laravel app to run on this server:\n"
  printf "    ${C_DIM}1.${C_RESET}  Extract the zip (unless already unpacked)\n"
  printf "    ${C_DIM}2.${C_RESET}  Write ${C_BOLD}.env${C_RESET} (URL + database)\n"
  printf "    ${C_DIM}3.${C_RESET}  Install PHP packages, migrate, seed\n"
  printf "    ${C_DIM}4.${C_RESET}  Fix permissions, caches, and symlink ${C_BOLD}public${C_RESET} here\n"
  blank
}

show_summary() {
  local zip_label="(in-place, no zip)"
  [[ -n "$ZIP_PATH" && $IN_PLACE -eq 0 ]] && zip_label="$(basename "$ZIP_PATH")"
  local host_label="VPS / dedicated"
  [[ $IS_CPANEL -eq 1 ]] && host_label="cPanel shared hosting"
  local public_label="skip"
  if [[ $LINK_PUBLIC -eq 1 ]]; then
    public_label="${LINK_DIR:-$START_CWD}/public  →  ${APP_DIR}/public"
  fi

  blank
  printf "  ${C_BOLD}Review before we start${C_RESET}\n"
  blank
  printf "  ${C_DIM}%-16s${C_RESET} %s\n" "Host" "$host_label"
  printf "  ${C_DIM}%-16s${C_RESET} %s\n" "PHP" "$PHP_BIN ($(php_version_of "$PHP_BIN"))"
  printf "  ${C_DIM}%-16s${C_RESET} %s\n" "Archive" "$zip_label"
  printf "  ${C_DIM}%-16s${C_RESET} %s\n" "App path" "$APP_DIR"
  printf "  ${C_DIM}%-16s${C_RESET} %s\n" "Web" "$public_label"
  printf "  ${C_DIM}%-16s${C_RESET} %s\n" "App URL" "$APP_URL"
  printf "  ${C_DIM}%-16s${C_RESET} %s  ${C_DIM}(debug %s)${C_RESET}\n" "Environment" "$APP_ENV" "$APP_DEBUG"
  if [[ "$DB_CONNECTION" == "sqlite" ]]; then
    printf "  ${C_DIM}%-16s${C_RESET} sqlite\n" "Database"
  else
    printf "  ${C_DIM}%-16s${C_RESET} %s @ %s:%s\n" "Database" "$DB_DATABASE" "$DB_HOST" "$DB_PORT"
    printf "  ${C_DIM}%-16s${C_RESET} %s\n" "DB user" "$DB_USERNAME"
  fi
  printf "  ${C_DIM}%-16s${C_RESET} %s\n" "Seed" "$([[ $SKIP_SEED -eq 0 ]] && echo yes || echo skip)"
  printf "  ${C_DIM}%-16s${C_RESET} %s\n" "Composer" "$([[ $SKIP_COMPOSER -eq 1 ]] && echo skip || ([[ $WITH_DEV -eq 1 ]] && echo 'with dev' || echo 'production (--no-dev)'))"
  blank
}

success_screen() {
  local elapsed=$(( $(date +%s) - STARTED_AT ))
  local health="${APP_URL%/}/up"
  local cron_php="$PHP_BIN"

  blank
  hr
  printf "  ${C_GREEN}${C_BOLD}%s  Deploy complete${C_RESET}  ${C_DIM}(%ss)${C_RESET}\n" "$G_OK" "$elapsed"
  hr
  blank
  printf "  ${C_DIM}%-16s${C_RESET} %s\n" "App path" "$APP_DIR"
  if [[ $LINK_PUBLIC -eq 1 ]]; then
    printf "  ${C_DIM}%-16s${C_RESET} %s\n" "Public link" "${PUBLIC_LINK_PATH:-${LINK_DIR:-$START_CWD}/public}"
  else
    printf "  ${C_DIM}%-16s${C_RESET} %s\n" "Public folder" "$APP_DIR/public"
  fi
  printf "  ${C_DIM}%-16s${C_RESET} %s\n" "Health check" "$health"
  printf "  ${C_DIM}%-16s${C_RESET} %s\n" "Log" "$LOG_FILE"
  blank

  if [[ $IS_CPANEL -eq 1 ]]; then
    printf "  ${C_BOLD}cPanel checklist${C_RESET}\n"
    printf "    ${C_DIM}1.${C_RESET}  Software → MultiPHP Manager → set this domain to PHP %s+\n" "$PHP_MIN"
    if [[ $LINK_PUBLIC -eq 1 ]]; then
      printf "    ${C_DIM}2.${C_RESET}  Domains → document root should be the public symlink:\n"
      printf "         %s\n" "${PUBLIC_LINK_PATH:-${LINK_DIR:-$START_CWD}/public}"
    else
      printf "    ${C_DIM}2.${C_RESET}  Domains → the domain's document root must be:\n"
      printf "         %s\n" "$APP_DIR/public"
    fi
    printf "    ${C_DIM}3.${C_RESET}  Cron Jobs (every minute):\n"
    printf "         %s %s/artisan schedule:run >> /dev/null 2>&1\n" "$cron_php" "$APP_DIR"
    printf "         %s %s/artisan queue:work --stop-when-empty --tries=3\n" "$cron_php" "$APP_DIR"
    blank
    warn "Shared hosts cannot keep a long-running queue worker. The --stop-when-empty cron is the usual workaround."
    blank
  else
    printf "  ${C_BOLD}Point the web server at the public folder${C_RESET}\n"
    if [[ $LINK_PUBLIC -eq 1 ]]; then
      printf "    %s\n" "${PUBLIC_LINK_PATH:-$LINK_DIR/public}"
    else
      printf "    %s\n" "$APP_DIR/public"
    fi
    blank
    printf "  ${C_BOLD}Keep the app running${C_RESET}\n"
    printf "    ${C_CYAN}# queue worker (Supervisor / systemd)${C_RESET}\n"
    printf "    cd %s && %s artisan queue:work --sleep=3 --tries=3\n" "$APP_DIR" "$cron_php"
    printf "    ${C_CYAN}# scheduler crontab${C_RESET}\n"
    printf "    * * * * * cd %s && %s artisan schedule:run >> /dev/null 2>&1\n" "$APP_DIR" "$cron_php"
    blank
    if [[ "$(id -u)" -ne 0 ]]; then
      warn "Not run as root. If the web server cannot write logs/cache:"
      printf "    sudo chown -R %s:%s %s/storage %s/bootstrap/cache\n" \
        "$WEB_USER" "$WEB_GROUP" "$APP_DIR" "$APP_DIR"
      blank
    fi
  fi
}

# ── main ────────────────────────────────────────────────────────────────────
main() {
  parse_args "$@"
  [[ -n "$LINK_DIR" ]] || LINK_DIR="$START_CWD"
  [[ $SKIP_PUBLIC_LINK -eq 1 ]] && LINK_PUBLIC=0
  init_log
  detect_cpanel
  banner

  printf "  ${C_BOLD}Checking your system${C_RESET}\n"
  blank

  if [[ $IS_CPANEL -eq 1 ]]; then
    ok "cPanel-style hosting detected"
  else
    info "Standard server (VPS / dedicated)"
  fi

  detect_php
  ok "PHP $(php_version_of "$PHP_BIN")  ${C_DIM}${PHP_BIN}${C_RESET}"

  local missing_ext=()
  local ext
  for ext in openssl mbstring tokenizer xml ctype json fileinfo; do
    php_ext_ok "$ext" || missing_ext+=("$ext")
  done
  if [[ "$DB_CONNECTION" != "sqlite" ]]; then
    php_ext_ok pdo_mysql || php_ext_ok pdo_pgsql || missing_ext+=("pdo_mysql")
  fi
  if [[ ${#missing_ext[@]} -gt 0 ]]; then
    die "Missing PHP extensions: ${missing_ext[*]}. On cPanel enable them in MultiPHP INI / Extensions."
  fi
  local rec=()
  for ext in bcmath curl intl zip gd; do
    php_ext_ok "$ext" || rec+=("$ext")
  done
  ok "Required PHP extensions"
  if [[ ${#rec[@]} -gt 0 ]]; then
    warn "Recommended extensions missing: ${rec[*]}"
  fi

  if [[ $SKIP_COMPOSER -eq 0 ]]; then
    ensure_composer
    ok "Composer  ${C_DIM}${COMPOSER_BIN}${C_RESET}"
  else
    warn "Composer install will be skipped"
  fi

  if ! command -v unzip >/dev/null 2>&1 && ! command -v python3 >/dev/null 2>&1; then
    run_php -r 'exit(class_exists("ZipArchive")?0:1);' \
      || die "Need unzip, python3, or PHP zip to extract the archive."
  fi
  ok "Zip extraction available"

  detect_web_user
  ok "File owner  ${WEB_USER}"
  ok "Launch dir  ${START_CWD}"

  blank
  printf "  ${C_BOLD}Project${C_RESET}\n"
  blank

  local has_zip=0
  if find_zip; then
    has_zip=1
    ok "Archive  $(basename "$ZIP_PATH")  ${C_DIM}$(du -h "$ZIP_PATH" 2>/dev/null | awk '{print $1}')${C_RESET}"
  fi

  local existing=""
  if [[ -n "$APP_DIR" && -d "$APP_DIR" ]]; then
    existing="$(find_laravel_root "$APP_DIR" || true)"
  fi
  if [[ -z "$existing" ]]; then
    existing="$(find_laravel_root "$PWD" 2>/dev/null || true)"
  fi
  if [[ -z "$existing" ]]; then
    existing="$(find_laravel_root "$SCRIPT_DIR" 2>/dev/null || true)"
  fi

  if [[ $IN_PLACE -eq 1 ]]; then
    has_zip=0
  fi

  if [[ $has_zip -eq 1 && -n "$existing" && $IN_PLACE -eq 0 && $ASSUME_YES -eq 0 ]]; then
    if confirm "A Laravel app is already on disk. Use it instead of extracting the zip?" n; then
      has_zip=0
      IN_PLACE=1
      APP_DIR="$existing"
    fi
  fi

  if [[ $has_zip -eq 1 ]]; then
    local default_dir
    if [[ $IS_CPANEL -eq 1 && -n "${HOME:-}" ]]; then
      default_dir="${APP_DIR:-$HOME/$(basename "$ZIP_PATH" .zip)}"
    else
      default_dir="${APP_DIR:-$SCRIPT_DIR/$(basename "$ZIP_PATH" .zip)}"
    fi
    APP_DIR="$(ask "Install directory  (keep this outside public_html)" "$default_dir")"
    [[ -n "$APP_DIR" ]] || die "Install directory is required."
    mkdir -p "$APP_DIR"
    APP_DIR="$(cd "$APP_DIR" && pwd)"
    if [[ "$APP_DIR" == *"/public_html" || "$APP_DIR" == *"/public_html/"* ]]; then
      warn "Installing inside public_html exposes .env unless Apache rewrite is working."
      confirm "Continue anyway?" n || die "Choose a folder outside public_html, e.g. $HOME/myapp"
    fi
    if [[ -n "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]]; then
      if [[ $ASSUME_YES -eq 1 ]]; then
        warn "Directory is not empty; zip files may overwrite existing ones"
      else
        confirm "Install directory is not empty. Extract into it anyway?" n \
          || die "Install directory is not empty: $APP_DIR"
      fi
    fi
  elif [[ -n "$existing" ]]; then
    IN_PLACE=1
    APP_DIR="$(ask "Laravel directory" "${APP_DIR:-$existing}")"
    APP_DIR="$(cd "$APP_DIR" && pwd)"
    find_laravel_root "$APP_DIR" >/dev/null || die "No Laravel app (artisan) in $APP_DIR"
    APP_DIR="$(find_laravel_root "$APP_DIR")"
    ok "Using existing project"
  else
    die "No zip next to this script, and no Laravel app found. Pass --zip PATH or --dir PATH --in-place"
  fi

  if [[ $has_zip -eq 1 ]]; then
    blank
    printf "  ${C_BOLD}Extracting${C_RESET}\n"
    blank
    set_steps 1
    run_step "Extract archive" do_extract "$APP_DIR"
    local resolved
    resolved="$(find_laravel_root "$APP_DIR" || true)"
    [[ -n "$resolved" ]] || die "Zip extracted, but no Laravel artisan file was found under $APP_DIR"
    APP_DIR="$resolved"
    ok "Laravel root  $APP_DIR"
  fi

  apply_project_defaults
  if ! php_meets_min "$PHP_BIN" "$PHP_MIN"; then
    if [[ $PHP_BIN_LOCKED -eq 1 ]]; then
      die "This project needs PHP ${PHP_MIN}+. Current: $(php_version_of "$PHP_BIN") ($PHP_BIN)."
    fi
    PHP_BIN=""
    detect_php
    ok "Switched to PHP $(php_version_of "$PHP_BIN")  ${C_DIM}${PHP_BIN}${C_RESET}"
  fi

  if [[ -f "$APP_DIR/vendor/autoload.php" && $SKIP_COMPOSER -eq 0 && $ASSUME_YES -eq 0 ]]; then
    if confirm "vendor/ is already in the project. Skip Composer install?" n; then
      SKIP_COMPOSER=1
    fi
  fi

  blank
  printf "  ${C_BOLD}Application${C_RESET}\n"
  blank
  APP_NAME="$(ask "App name" "$APP_NAME")"
  APP_URL="$(ask "App URL" "$APP_URL")"
  APP_ENV="$(ask "Environment  (local / staging / production)" "$APP_ENV")"
  case "$APP_ENV" in
    local|staging|production) ;;
    *) warn "Unusual APP_ENV=$APP_ENV — continuing anyway" ;;
  esac
  if [[ "$APP_ENV" == "production" ]]; then
    APP_DEBUG="false"
  else
    APP_DEBUG="true"
    WITH_DEV=1
  fi
  APP_TIMEZONE="$(ask "Timezone" "$APP_TIMEZONE")"

  blank
  printf "  ${C_BOLD}Database${C_RESET}\n"
  blank
  if [[ $IS_CPANEL -eq 1 ]]; then
    info "cPanel names look like ${C_BOLD}${USER:-user}_app${C_RESET}. Create DB + user in MySQL Databases first."
    blank
  fi
  DB_CONNECTION="$(ask "DB connection  (mysql / sqlite / pgsql)" "$DB_CONNECTION")"
  if [[ "$DB_CONNECTION" != "sqlite" ]]; then
    DB_HOST="$(ask "DB host" "$DB_HOST")"
    DB_PORT="$(ask "DB port" "$DB_PORT")"
    DB_DATABASE="$(ask "DB name" "$DB_DATABASE")"
    DB_USERNAME="$(ask "DB user" "$DB_USERNAME")"
    if [[ $ASSUME_YES -eq 1 && -z "$DB_PASSWORD" ]]; then
      die "Database password required in non-interactive mode: --db-pass"
    fi
    if [[ -z "$DB_PASSWORD" ]]; then
      DB_PASSWORD="$(ask "DB password" "" 1)"
    fi
    [[ -n "$DB_PASSWORD" ]] || die "Database password cannot be empty."
  fi

  local has_data_migrations=0
  [[ -d "$APP_DIR/database/migrations/data" ]] && has_data_migrations=1

  if [[ $ASSUME_YES -eq 0 ]]; then
    blank
    if [[ $SKIP_PUBLIC_LINK -eq 0 ]]; then
      local default_link_dir="${LINK_DIR:-$START_CWD}"
      if confirm "Create a public symlink here?  (${default_link_dir}/public → app/public)" y; then
        LINK_PUBLIC=1
        LINK_DIR="$(ask "Directory for the public symlink" "$default_link_dir")"
      else
        LINK_PUBLIC=0
      fi
    else
      LINK_PUBLIC=0
    fi
    if confirm "Run database seeders?" y; then
      SKIP_SEED=0
    else
      SKIP_SEED=1
    fi
    if [[ $has_data_migrations -eq 1 ]]; then
      if confirm "Run extra data migrations  (database/migrations/data)?" y; then
        SKIP_DATA_MIGRATE=0
      else
        SKIP_DATA_MIGRATE=1
      fi
    else
      SKIP_DATA_MIGRATE=1
    fi
    if [[ "$DB_CONNECTION" != "sqlite" && $IS_CPANEL -eq 0 ]]; then
      if confirm "Create the database if it does not exist?" y; then
        CREATE_DB=1
      else
        CREATE_DB=0
      fi
    fi
    if [[ -f "$APP_DIR/package.json" ]]; then
      if confirm "Build frontend assets with npm?" n; then
        SKIP_NPM=0
      else
        SKIP_NPM=1
      fi
    fi
  else
    [[ $SKIP_PUBLIC_LINK -eq 1 ]] && LINK_PUBLIC=0
    [[ -n "$LINK_DIR" ]] || LINK_DIR="$START_CWD"
    [[ $has_data_migrations -eq 1 ]] || SKIP_DATA_MIGRATE=1
  fi

  show_summary
  confirm "Proceed with install?" y || die "Aborted."

  local steps=0
  steps=$((steps + 1)) # env
  [[ $SKIP_COMPOSER -eq 1 && -f "$APP_DIR/vendor/autoload.php" ]] || steps=$((steps + 1))
  steps=$((steps + 4)) # key, dirs, perms, storage link
  [[ $CREATE_DB -eq 1 && "$DB_CONNECTION" != "sqlite" ]] && steps=$((steps + 1))
  steps=$((steps + 1)) # db ping
  steps=$((steps + 1)) # migrate
  [[ $SKIP_DATA_MIGRATE -eq 0 ]] && steps=$((steps + 1))
  [[ $SKIP_SEED -eq 0 ]] && steps=$((steps + 1))
  steps=$((steps + 1)) # optimize
  [[ $SKIP_NPM -eq 0 ]] && steps=$((steps + 1))
  [[ $LINK_PUBLIC -eq 1 ]] && steps=$((steps + 1))
  steps=$((steps + 1)) # protect env
  set_steps "$steps"

  blank
  printf "  ${C_BOLD}Installing${C_RESET}\n"
  blank

  run_step "Write .env" do_write_env
  if [[ $SKIP_COMPOSER -eq 1 && -f "$APP_DIR/vendor/autoload.php" ]]; then
    info "Using vendor/ from the archive"
  else
    run_step "Install Composer packages" do_composer_install
  fi
  run_step "Generate application key" do_app_key
  run_step "Create storage directories" do_storage_structure
  run_step "Fix folder permissions" do_permissions
  run_step "Link public storage" do_storage_link
  run_step "Protect app files" do_protect_env

  if [[ $CREATE_DB -eq 1 && "$DB_CONNECTION" != "sqlite" ]]; then
    run_step "Create database if needed" db_create_if_needed
  fi
  run_step "Test database connection" db_ping
  run_step "Run migrations" do_migrate
  if [[ $SKIP_DATA_MIGRATE -eq 0 ]]; then
    run_step "Run data migrations" do_data_migrate
  fi
  if [[ $SKIP_SEED -eq 0 ]]; then
    run_step "Seed database" do_seed
  fi
  run_step "Optimize application" do_optimize
  if [[ $SKIP_NPM -eq 0 ]]; then
    run_step "Build frontend assets" do_npm
  fi
  if [[ $LINK_PUBLIC -eq 1 ]]; then
    run_step "Symlink public from launch dir" do_link_public
  fi

  success_screen
}

main "$@"
