#!/bin/sh
# Copy the bounded shcpd configuration set into the canonical panel state tree.
# The legacy tree is intentionally retained as a physical rollback snapshot.
set -eu

legacy=${SHCP_CONFIG_LEGACY_DIR:-/etc/shcpd}
canonical=${SHCP_CONFIG_DIR:-/etc/shcp}
lock=${SHCP_CONFIG_LOCK:-/run/lock/shcp-config.lock}
snapshot_owner=${SHCP_CONFIG_SNAPSHOT_OWNER:-root}
snapshot_group=${SHCP_CONFIG_SNAPSHOT_GROUP:-shcp}
snapshot_dir_owner=${SHCP_CONFIG_SNAPSHOT_DIR_OWNER:-root}
snapshot_dir_group=${SHCP_CONFIG_SNAPSHOT_DIR_GROUP:-root}
marker_uid=${SHCP_CONFIG_MARKER_UID:-0}
marker_gid=${SHCP_CONFIG_MARKER_GID:-0}
marker=$legacy/.shcp-config-migrated-v1

fatal() { printf '%s\n' "shcp-config-migrate: ERROR: $*" >&2; exit 1; }
warn() { printf '%s\n' "shcp-config-migrate: WARNING: $*" >&2; }

[ ! -L "$legacy" ] || fatal "$legacy must not be a symlink"
[ ! -L "$canonical" ] || fatal "$canonical must not be a symlink"
[ ! -L "$(dirname "$legacy")" ] || fatal "parent of $legacy must not be a symlink"
[ ! -L "$(dirname "$canonical")" ] || fatal "parent of $canonical must not be a symlink"
[ ! -L "$(dirname "$lock")" ] || fatal "parent of $lock must not be a symlink"
[ ! -L "$lock" ] || fatal "$lock must not be a symlink"

mkdir -p "$(dirname "$lock")"
exec 9>"$lock"
flock -x 9

ensure_canonical() {
    if [ -e "$canonical" ]; then
        [ -d "$canonical" ] || fatal "$canonical is not a directory"
    else
        install -d -m 0755 "$canonical"
    fi
    if [ "$canonical" = /etc/shcp ]; then
        [ "$(stat -c '%u:%g' "$canonical")" = 0:0 ] || fatal "$canonical must be owned by root:root"
        [ "$(stat -c '%a' "$canonical")" = 755 ] || fatal "$canonical must have mode 0755"
    fi
}

[ -e "$legacy" ] || { ensure_canonical; exit 0; }
[ -d "$legacy" ] || fatal "$legacy is not a directory"
ensure_canonical
[ ! -L "$canonical/conf.d" ] || fatal "$canonical/conf.d must not be a symlink"

# A root-owned marker distinguishes the retained rollback snapshot from an
# unresolved first migration. Canonical state is expected to evolve afterward.
if [ -e "$marker" ] || [ -L "$marker" ]; then
    [ ! -L "$marker" ] || fatal "$marker must not be a symlink"
    [ -f "$marker" ] || fatal "$marker is not a regular file"
    [ "$(cat "$marker")" = migrated-v1 ] || fatal "$marker has invalid contents"
    [ "$(stat -c '%u:%g:%a' "$marker")" = "$marker_uid:$marker_gid:444" ] || \
        fatal "$marker has invalid ownership or mode"
    exit 0
fi

# Freeze directories first so an unprivileged legacy owner cannot replace
# entries. Each regular file is then replaced atomically with a frozen inode;
# a writer holding the old inode open can no longer alter the published path.
chown "$snapshot_dir_owner:$snapshot_dir_group" "$legacy"
chmod 0755 "$legacy"
if [ -d "$legacy/conf.d" ] && [ ! -L "$legacy/conf.d" ]; then
    chown "$snapshot_owner:$snapshot_group" "$legacy/conf.d"
    chmod 0550 "$legacy/conf.d"
fi
copy_regular() {
    src=$1 dst=$2
    [ ! -L "$src" ] || fatal "$src is a symlink"
    [ -f "$src" ] || fatal "$src is not a regular file"
    src_uid=$(stat -c %u "$src")
    src_gid=$(stat -c %g "$src")
    src_mode=$(stat -c %a "$src")
    src_parent=$(dirname "$src")
    frozen=$(mktemp "$src_parent/.shcp-config-freeze.XXXXXX") || fatal "cannot freeze $src"
    trap 'rm -f "$frozen"' EXIT HUP INT TERM
    cp "$src" "$frozen"
    chown "$snapshot_owner:$snapshot_group" "$frozen"
    chmod 0440 "$frozen"
    mv -f "$frozen" "$src"
    frozen=
    trap - EXIT HUP INT TERM
    if [ -e "$dst" ] || [ -L "$dst" ]; then
        if [ -L "$dst" ] || [ ! -f "$dst" ]; then
            fatal "$dst is not a regular file"
        fi
        cmp -s "$src" "$dst" || fatal "$src and $dst differ; reconcile them before upgrading"
        return
    fi
    parent=$(dirname "$dst")
    [ ! -L "$parent" ] || fatal "$parent must not be a symlink"
    mkdir -p "$parent"
    tmp=$(mktemp "$parent/.shcp-config-migrate.XXXXXX") || fatal "cannot stage $dst"
    trap 'rm -f "$tmp"' EXIT HUP INT TERM
    cp -p "$src" "$tmp"
    chown "$src_uid:$src_gid" "$tmp"
    chmod "$src_mode" "$tmp"
    if [ -e "$dst" ] || [ -L "$dst" ]; then
        fatal "$dst appeared during migration"
    fi
    mv "$tmp" "$dst"
    trap - EXIT HUP INT TERM
}

for name in shcpd.conf php.ini backup-stream.key file-broker.key; do
    if [ -e "$legacy/$name" ] || [ -L "$legacy/$name" ]; then
        copy_regular "$legacy/$name" "$canonical/$name"
    fi
done

if [ -e "$legacy/preload.php" ] || [ -L "$legacy/preload.php" ]; then
    [ -L "$legacy/preload.php" ] || fatal "$legacy/preload.php must be the approved symlink"
    [ "$(readlink "$legacy/preload.php")" = /opt/shcp/config/preload.php ] || fatal "$legacy/preload.php has an unapproved target"
    if [ -e "$canonical/preload.php" ] || [ -L "$canonical/preload.php" ]; then
        if [ ! -L "$canonical/preload.php" ] || [ "$(readlink "$canonical/preload.php")" != /opt/shcp/config/preload.php ]; then
            fatal "$canonical/preload.php conflicts with the approved symlink"
        fi
    else
        ln -s /opt/shcp/config/preload.php "$canonical/preload.php"
    fi
fi

if [ -e "$legacy/conf.d" ] || [ -L "$legacy/conf.d" ]; then
    if [ -L "$legacy/conf.d" ] || [ ! -d "$legacy/conf.d" ]; then
        fatal "$legacy/conf.d is not a physical directory"
    fi
    mkdir -p "$canonical/conf.d"
    find "$legacy/conf.d" -mindepth 1 -maxdepth 1 -print | while IFS= read -r src; do
        name=${src##*/}
        copy_regular "$src" "$canonical/conf.d/$name"
    done
fi

find "$legacy" -mindepth 1 -maxdepth 1 -print | while IFS= read -r entry; do
    case ${entry##*/} in
        shcpd.conf|php.ini|preload.php|backup-stream.key|file-broker.key|conf.d) ;;
        *) warn "legacy entry left untouched: $entry" ;;
    esac
done

if [ -L "$legacy/preload.php" ]; then
    chown -h "$snapshot_owner:$snapshot_group" "$legacy/preload.php"
fi
marker_tmp=$(mktemp "$legacy/.shcp-config-marker.XXXXXX") || fatal "cannot stage $marker"
trap 'rm -f "$marker_tmp"' EXIT HUP INT TERM
printf '%s\n' migrated-v1 > "$marker_tmp"
chown "$marker_uid:$marker_gid" "$marker_tmp"
chmod 0444 "$marker_tmp"
mv "$marker_tmp" "$marker"
trap - EXIT HUP INT TERM
