#!/usr/bin/env bash
# Reset a Laravel deployment: code to users_owner:www-data, runtime to
# www-data:www-data (Ubuntu / GNU tools).
# Run during maintenance, without concurrent deploys or filesystem changes.
set -Eeuo pipefail
export LC_ALL=C
unset POSIXLY_CORRECT
umask 077

usage() {
    cat <<'HELP'
Usage: sudo bash ./bash_script/set_ownership.sh OWNER [--dry-run]
OWNER must be ubuntu, master or adminkview and must already exist.
The application root is the parent of this script's bash_script directory.

Code entries: OWNER:www-data (symlink targets are never followed).
storage + bootstrap/cache entries: www-data:www-data, so PHP owns its runtime
files (required e.g. for Blade's touch() on compiled views).
Code: directories 0755, files 0644. Shell scripts/artisan: 0750.
storage + bootstrap/cache: directories 2775 (setgid), files 0664.
.env*, keys/certificates and cached config.php: 0640.
.git metadata: directories 0700, files 0600, existing executables 0700.
Existing executable dependency files in vendor/node_modules use 0750.
Standard Unix permissions only; no ACL tools or automatic permission backup.
OWNER must belong to www-data. Writers need a compatible umask (usually 0002).
Does not remove pre-existing extended/default ACLs or change accounts/services.
HELP
}
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
users_owner=''
dry_run=0
for arg in "$@"; do
    case "$arg" in
        --dry-run) dry_run=1 ;;
        -h|--help) usage; exit 0 ;;
        ubuntu|master|adminkview)
            [[ -z "$users_owner" ]] || die 'Specify exactly one owner.'
            users_owner="$arg" ;;
        *) die "Unknown argument: $arg (use --help)." ;;
    esac
done
[[ -n "$users_owner" ]] || die 'Specify ubuntu, master or adminkview explicitly.'
[[ "$(uname -s)" == Linux ]] || die 'Run this script on Ubuntu/Linux, not Windows.'
for tool in realpath find findmnt getent id chown chmod mktemp; do
    command -v "$tool" >/dev/null || die "Missing tool: $tool"
done
id "$users_owner" >/dev/null 2>&1 || die "Unknown account: $users_owner"
[[ "$(id -u "$users_owner")" != 0 ]] || die 'The deployment owner must not be root.'
id www-data >/dev/null 2>&1 || die 'User www-data does not exist.'
[[ "$(id -u www-data)" != 0 ]] || die 'User www-data must not be root.'
group_record="$(getent group www-data)" || die 'Group www-data does not exist.'
IFS=: read -r group_name group_password group_gid group_members <<< "$group_record"
owner_groups="$(id -G "$users_owner")"
[[ " $owner_groups " == *" $group_gid "* ]] || die "$users_owner must belong to www-data; configure membership and refresh login/services first."
(( dry_run )) || [[ "$(id -u)" == 0 ]] || die 'Use sudo to apply ownership and permission changes.'

script_path="$(realpath -e -- "${BASH_SOURCE[0]}")"
script_dir="${script_path%/*}"
[[ "${script_dir##*/}" == bash_script ]] || die 'Install this file in APP_ROOT/bash_script/.'
app_root="$(realpath -e -- "$script_dir/..")"
case "$app_root" in
    /|/var|/var/www|/var/www/html|/home|/tmp|/usr|/etc|/opt)
        die "Refusing broad system directory: $app_root" ;;
esac
for required in artisan composer.json; do
    [[ -f "$app_root/$required" && ! -L "$app_root/$required" ]] || die "Missing local Laravel file: $required"
done
for required in storage bootstrap bootstrap/cache; do
    [[ -d "$app_root/$required" && ! -L "$app_root/$required" ]] || die "Required directory is missing or linked: $required"
done

# Refuse nested mounts (including same-device bind mounts) before any mutation.
mounts="$(findmnt --raw --noheadings --output TARGET)"
while IFS= read -r mount_path; do
    printf -v mount_path '%b' "$mount_path"
    [[ "$mount_path" != "$app_root/"* ]] || die "Nested mount requires a separate procedure: $mount_path"
done <<< "$mounts"
hardlink="$(find -P "$app_root" -xdev -type f -links +1 -print -quit)"
[[ -z "$hardlink" ]] || die "Hard-linked file could affect another tree: $hardlink"
special="$(find -P "$app_root" -xdev ! -type f ! -type d ! -type l -print -quit)"
[[ -z "$special" ]] || die "Special filesystem entry requires review: $special"

manifest="$(mktemp /tmp/laravel-permissions-manifest.XXXXXXXX)"
trap 'rm -f -- "$manifest"' EXIT
find -P "$app_root" -xdev -printf '%y%p\0' > "$manifest"

all_entries=() owner_entries=() runtime_entries=() code_dirs=() code_files=()
runtime_dirs=() runtime_files=() executables=() secrets=()
git_dirs=() git_files=() git_exec=() links=()
while IFS= read -r -d '' entry; do
    kind="${entry:0:1}"
    path="${entry:1}"
    relative="${path#"$app_root"/}"
    name="${path##*/}"
    all_entries+=("$path")
    runtime=0
    if [[ "$relative" == storage || "$relative" == storage/* || "$relative" == bootstrap/cache || "$relative" == bootstrap/cache/* ]]; then
        runtime=1
        runtime_entries+=("$path")
    else
        owner_entries+=("$path")
    fi
    case "$kind" in
        l) links+=("$path"); continue ;;
        d|f) ;;
        *) die "Unexpected entry: $path" ;;
    esac
    if (( ! runtime )) && [[ "/$relative/" == */.git/* ]]; then
        if [[ "$kind" == d ]]; then
            git_dirs+=("$path")
        elif [[ -x "$path" ]]; then
            git_exec+=("$path")
        else
            git_files+=("$path")
        fi
        continue
    fi
    if (( runtime )); then
        if [[ "$kind" == d ]]; then runtime_dirs+=("$path"); else runtime_files+=("$path"); fi
    else
        if [[ "$kind" == d ]]; then code_dirs+=("$path"); else code_files+=("$path"); fi
    fi
    [[ "$kind" == f ]] || continue
    # Only dependency executables retain their old executable bit. A stray 777
    # on an application JSON/PHP file is deliberately not preserved.
    if [[ "$name" == *.sh || "$relative" == artisan ]]; then
        executables+=("$path")
    elif [[ "/$relative" == */vendor/* || "/$relative" == */node_modules/* ]]; then
        if [[ -x "$path" ]]; then executables+=("$path"); fi
    fi
    case "$name" in
        .env|.env.*|*.key|*.pem|*.p12|*.pfx) secrets+=("$path") ;;
        *) [[ "$relative" != bootstrap/cache/config.php ]] || secrets+=("$path") ;;
    esac
done < "$manifest"

printf 'Application: %s\nCode owner/group: %s:www-data; runtime owner/group: www-data:www-data\n' "$app_root" "$users_owner"
printf 'Entries: %s; writable directories: %s; protected files: %s; symlinks: %s\n' \
    "${#all_entries[@]}" "${#runtime_dirs[@]}" "${#secrets[@]}" "${#links[@]}"
printf 'Policy: code 0755/0644; scripts 0750; runtime 2775/0664 (setgid); secrets 0640; Git private.\n'
printf 'Standard permissions only: pre-existing extended/default ACLs are not removed. No automatic backup.\n'
if (( ${#links[@]} )); then
    printf 'Symlink ownership only; targets are NOT traversed (configure external/shared paths separately).\n'
fi
if (( dry_run )); then
    printf 'DRY RUN: no ownership or permission changes.\n'
    exit 0
fi

trap 'printf "ERROR at line %s. Changes may be partial; no automatic rollback. Fix the cause and rerun.\n" "$LINENO" >&2' ERR

# Batch commands stay below the OS argument limit, including large dependency trees.
batch() {
    local argument_count="$1"
    shift
    local -a command=("${@:1:argument_count}")
    shift "$argument_count"
    while (( $# )); do
        "${command[@]}" "${@:1:100}"
        if (( $# <= 100 )); then break; fi
        shift 100
    done
}
batch 4 chown --no-dereference "$users_owner:www-data" -- "${owner_entries[@]}"
batch 4 chown --no-dereference www-data:www-data -- "${runtime_entries[@]}"
# Five-digit octal modes explicitly clear old setuid/setgid/sticky bits.
batch 3 chmod 00755 -- "${code_dirs[@]}"
batch 3 chmod 00644 -- "${code_files[@]}"
batch 3 chmod 02775 -- "${runtime_dirs[@]}"
batch 3 chmod 00664 -- "${runtime_files[@]}"
batch 3 chmod 00750 -- "${executables[@]}"
batch 3 chmod 00640 -- "${secrets[@]}"
batch 3 chmod 00700 -- "${git_dirs[@]}"
batch 3 chmod 00600 -- "${git_files[@]}"
batch 3 chmod 00700 -- "${git_exec[@]}"

wrong_owner="$(find -P "$app_root" -xdev \
    \( -path "$app_root/storage" -o -path "$app_root/bootstrap/cache" \) -prune \
    -o \( ! -user "$users_owner" -o ! -group www-data \) -print -quit)"
[[ -z "$wrong_owner" ]] || die "Ownership verification failed: $wrong_owner"
wrong_owner="$(find -P "$app_root/storage" "$app_root/bootstrap/cache" -xdev \
    \( ! -user www-data -o ! -group www-data \) -print -quit)"
[[ -z "$wrong_owner" ]] || die "Ownership verification failed: $wrong_owner"
printf 'Done. Code belongs to %s:www-data; storage and bootstrap/cache belong to www-data:www-data.\n' "$users_owner"
printf 'New runtime entries inherit group www-data; their owner is the creating process.\n'
printf 'Run cache-building Artisan commands (view:cache, optimize...) as www-data.\n'
printf 'Configure a compatible umask (usually 0002) for each writer; this script does not change service umasks.\n'
printf 'Check the web app and cron jobs under their actual identities.\n'
