#!/usr/bin/env bash

set -Eeuo pipefail
umask 027

readonly spin_repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
readonly spin_remote="${APP_DEPLOY_REMOTE:-origin}"
readonly spin_branch="${APP_DEPLOY_BRANCH:-main}"
readonly spin_require_signature="${APP_DEPLOY_REQUIRE_SIGNATURE:-0}"
readonly spin_healthcheck_url="${APP_DEPLOY_HEALTHCHECK_URL:-}"
readonly spin_lock_dir="$spin_repo_dir/.git/secure-update.lock"
readonly spin_status_file="$spin_repo_dir/storage/app/private/deployments/status.json"
spin_in_maintenance=0
spin_target_commit=""
spin_preflight_dir=""

write_status() {
    local spin_state="$1"
    local spin_message="$2"
    local spin_commit="${3:-}"

    DEPLOYMENT_STATE="$spin_state" DEPLOYMENT_MESSAGE="$spin_message" DEPLOYMENT_COMMIT="$spin_commit" \
        php -r '
            $path = $argv[1];
            $directory = dirname($path);
            if (! is_dir($directory) && ! mkdir($directory, 0770, true) && ! is_dir($directory)) exit(1);
            $allowed = ["running", "current", "succeeded", "failed"];
            $state = in_array(getenv("DEPLOYMENT_STATE"), $allowed, true) ? getenv("DEPLOYMENT_STATE") : "failed";
            $commit = preg_match("/\A[0-9a-f]{7,64}\z/i", (string) getenv("DEPLOYMENT_COMMIT")) ? strtolower(getenv("DEPLOYMENT_COMMIT")) : null;
            $payload = ["state" => $state, "message" => substr((string) getenv("DEPLOYMENT_MESSAGE"), 0, 240), "commit" => $commit, "updated_at" => gmdate(DATE_ATOM)];
            $temporary = $path.".".bin2hex(random_bytes(6)).".tmp";
            file_put_contents($temporary, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR).PHP_EOL, LOCK_EX);
            chmod($temporary, 0640);
            rename($temporary, $path);
        ' "$spin_status_file" >/dev/null 2>&1 || true
}

fail() {
    write_status "failed" "$*" "$spin_target_commit"
    echo "secure-update: $*" >&2
    exit 1
}

handle_error() {
    local spin_exit_code="$?"
    local spin_line="$1"
    trap - ERR
    write_status "failed" "Update stopped unexpectedly at step $spin_line." "$spin_target_commit"
    exit "$spin_exit_code"
}

cleanup() {
    if [[ "$spin_in_maintenance" == "1" ]]; then
        php artisan up >/dev/null 2>&1 || true
    fi
    [[ -z "$spin_preflight_dir" ]] || rm -rf -- "$spin_preflight_dir"
    rmdir "$spin_lock_dir" 2>/dev/null || true
}

trap 'handle_error $LINENO' ERR

[[ "$(id -u)" -ne 0 ]] || fail "refusing to deploy as root"
[[ "$spin_remote" =~ ^[A-Za-z0-9._-]+$ ]] || fail "invalid remote name"
git check-ref-format --branch "$spin_branch" >/dev/null 2>&1 || fail "invalid branch name"

for spin_command in git composer php bun mktemp; do
    command -v "$spin_command" >/dev/null 2>&1 || fail "$spin_command is required"
done

cd "$spin_repo_dir"
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || fail "not a Git worktree"
git remote get-url "$spin_remote" >/dev/null 2>&1 || fail "remote does not exist"

if [[ "${APP_DEPLOY_ALLOW_NON_PRODUCTION:-0}" != "1" ]]; then
    php -r '
        require "vendor/autoload.php";
        $app = require "bootstrap/app.php";
        $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
        exit(app()->isProduction() && ! config("app.debug") ? 0 : 1);
    ' || fail "APP_ENV must be production and APP_DEBUG must be false"
fi

[[ -z "$(git status --porcelain --untracked-files=normal)" ]] || fail "worktree has local changes"

mkdir "$spin_lock_dir" 2>/dev/null || {
    echo "secure-update: another update is already running"
    exit 0
}
trap cleanup EXIT

readonly spin_current_commit="$(git rev-parse HEAD)"
write_status "running" "Checking the configured Git remote for updates." "$spin_current_commit"
git fetch --prune "$spin_remote" "$spin_branch"
spin_target_commit="$(git rev-parse FETCH_HEAD)"

if [[ "$spin_current_commit" == "$spin_target_commit" ]]; then
    write_status "current" "The application is already on the latest commit." "$spin_current_commit"
    exit 0
fi

git merge-base --is-ancestor "$spin_current_commit" "$spin_target_commit" || fail "remote history is not a fast-forward"

if [[ "$spin_require_signature" == "1" ]]; then
    git verify-commit "$spin_target_commit" || fail "target commit does not have a trusted signature"
fi

spin_preflight_dir="$(mktemp -d "${TMPDIR:-/tmp}/lucky-spin-update.XXXXXX")"
git show "$spin_target_commit:composer.json" > "$spin_preflight_dir/composer.json"
git show "$spin_target_commit:composer.lock" > "$spin_preflight_dir/composer.lock"
git show "$spin_target_commit:package.json" > "$spin_preflight_dir/package.json"
git show "$spin_target_commit:bun.lock" > "$spin_preflight_dir/bun.lock"
composer --working-dir="$spin_preflight_dir" validate --no-check-publish --no-interaction
composer --working-dir="$spin_preflight_dir" audit --locked --no-dev --format=plain
(cd "$spin_preflight_dir" && bun audit)

write_status "running" "Applying the verified fast-forward update." "$spin_target_commit"
php artisan down --retry=60 --no-interaction
spin_in_maintenance=1
git merge --ff-only "$spin_target_commit"
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --no-progress
composer audit --locked --no-dev --format=plain
bun install --frozen-lockfile
bun run build
php artisan migrate --force --no-interaction
php artisan optimize
php artisan queue:restart
php artisan up
spin_in_maintenance=0

if [[ -n "$spin_healthcheck_url" ]]; then
    command -v curl >/dev/null 2>&1 || fail "curl is required for the health check"
    curl --fail --silent --show-error --max-time 15 "$spin_healthcheck_url" >/dev/null || fail "health check failed"
fi

write_status "succeeded" "The application update completed successfully." "$spin_target_commit"
echo "secure-update: deployed $spin_target_commit"
