#!/usr/bin/env bash
set -euo pipefail

usage() {
    cat <<'EOF'
Usage:
  deploy_billing_readiness.sh \
    --app-root /path/to/app \
    --package-root /path/to/extracted/package \
    --backup-dir /path/to/backup \
    [--private-config-path /home/icyla826/private_config.php] \
    [--php-bin /usr/bin/php] \
    [--db-defaults-file /path/to/.my.cnf --db-name database]

This script deploys the sanitized application, migrates private_config.php out
of the webroot, and quarantines files that are not in the release manifest.
Runtime uploads are preserved, except unexpected executable files, which are
also quarantined. When database arguments are supplied it backs up the database.
EOF
}

APP_ROOT=""
PACKAGE_ROOT=""
BACKUP_DIR=""
PRIVATE_CONFIG_PATH="/home/icyla826/private_config.php"
PHP_BIN="php"
DB_DEFAULTS_FILE=""
DB_NAME=""

while [ "$#" -gt 0 ]; do
    case "$1" in
        --app-root)
            APP_ROOT=${2:-}
            shift 2
            ;;
        --package-root)
            PACKAGE_ROOT=${2:-}
            shift 2
            ;;
        --backup-dir)
            BACKUP_DIR=${2:-}
            shift 2
            ;;
        --private-config-path)
            PRIVATE_CONFIG_PATH=${2:-}
            shift 2
            ;;
        --php-bin)
            PHP_BIN=${2:-}
            shift 2
            ;;
        --db-defaults-file)
            DB_DEFAULTS_FILE=${2:-}
            shift 2
            ;;
        --db-name)
            DB_NAME=${2:-}
            shift 2
            ;;
        --help|-h)
            usage
            exit 0
            ;;
        *)
            echo "Unknown argument: $1" >&2
            usage >&2
            exit 1
            ;;
    esac
done

if [ -z "$APP_ROOT" ] || [ -z "$PACKAGE_ROOT" ] || [ -z "$BACKUP_DIR" ]; then
    usage >&2
    exit 1
fi
if [ -z "$PRIVATE_CONFIG_PATH" ] || [ "${PRIVATE_CONFIG_PATH#/}" = "$PRIVATE_CONFIG_PATH" ]; then
    echo "Private configuration path must be absolute." >&2
    exit 1
fi

APP_ROOT=$(cd "$APP_ROOT" 2>/dev/null && pwd) || {
    echo "Application root does not exist: $APP_ROOT" >&2
    exit 1
}
PACKAGE_ROOT=$(cd "$PACKAGE_ROOT" 2>/dev/null && pwd) || {
    echo "Package root does not exist: $PACKAGE_ROOT" >&2
    exit 1
}
OVERLAY="$PACKAGE_ROOT/app"

if [ ! -d "$OVERLAY" ]; then
    echo "Package overlay not found: $OVERLAY" >&2
    exit 1
fi
if [ ! -f "$APP_ROOT/core/config.php" ]; then
    echo "Target does not look like an ICYLAB OS application." >&2
    exit 1
fi
case "$PRIVATE_CONFIG_PATH" in
    "$APP_ROOT"|"$APP_ROOT"/*)
        echo "Private configuration must be outside the application root." >&2
        exit 1
        ;;
esac
if [ -e "$OVERLAY/core/private_config.php" ]; then
    echo "Refusing deployment: package contains core/private_config.php." >&2
    exit 1
fi
if ! command -v "$PHP_BIN" >/dev/null 2>&1; then
    echo "PHP binary was not found: $PHP_BIN" >&2
    exit 1
fi
if ! command -v tar >/dev/null 2>&1; then
    echo "tar is required." >&2
    exit 1
fi

mkdir -p "$BACKUP_DIR"
BACKUP_DIR=$(cd "$BACKUP_DIR" && pwd)

FILES_LIST="$BACKUP_DIR/deployed_files.txt"
EXISTING_LIST="$BACKUP_DIR/existing_files.txt"
NEW_LIST="$BACKUP_DIR/new_files.txt"
LIVE_FILES_LIST="$BACKUP_DIR/live_files.txt"
UNEXPECTED_LIST="$BACKUP_DIR/unexpected_files.txt"
RUNTIME_SUSPICIOUS_LIST="$BACKUP_DIR/runtime_suspicious_files.txt"
: > "$FILES_LIST"
: > "$EXISTING_LIST"
: > "$NEW_LIST"
: > "$LIVE_FILES_LIST"
: > "$UNEXPECTED_LIST"
: > "$RUNTIME_SUSPICIOUS_LIST"

(
    cd "$OVERLAY"
    find . -type f -print | sed 's#^./##' | LC_ALL=C sort
) > "$FILES_LIST"

if grep -Eq '(^|/)core/private_config\.php$' "$FILES_LIST"; then
    echo "Refusing deployment: file manifest contains core/private_config.php." >&2
    exit 1
fi

# Un archivo fuera del manifiesto podría ser código huérfano o persistencia
# maliciosa. Se excluyen sólo los datos runtime y la configuración que será
# migrada; todo lo demás se conserva en cuarentena antes del overlay.
(
    cd "$APP_ROOT"
    find . -type f -print \
        | sed 's#^./##' \
        | LC_ALL=C sort \
        | grep -Ev '^(uploads/|storage/|assets/uploads/|core/private_config\.php$)' \
        || true
) > "$LIVE_FILES_LIST"
comm -23 "$LIVE_FILES_LIST" "$FILES_LIST" > "$UNEXPECTED_LIST"

(
    cd "$APP_ROOT"
    for runtime_root in uploads storage assets/uploads; do
        [ -d "$runtime_root" ] || continue
        find "$runtime_root" -type f \
            | grep -Ei '(^|/)(\.htaccess|\.user\.ini|php\.ini)$|\.(php|phtml|phar|cgi|pl|py|sh|bash|rb|jsp|asp|aspx)$' \
            || true
    done
) | LC_ALL=C sort -u > "$RUNTIME_SUSPICIOUS_LIST"

while IFS= read -r relative; do
    [ -n "$relative" ] || continue
    if [ -e "$APP_ROOT/$relative" ]; then
        printf '%s\n' "$relative" >> "$EXISTING_LIST"
    else
        printf '%s\n' "$relative" >> "$NEW_LIST"
    fi
done < "$FILES_LIST"

if [ -s "$EXISTING_LIST" ]; then
    tar -C "$APP_ROOT" -czf "$BACKUP_DIR/files-before.tar.gz" -T "$EXISTING_LIST"
else
    tar -czf "$BACKUP_DIR/files-before.tar.gz" --files-from /dev/null
fi

if [ -f "$APP_ROOT/core/private_config.php" ]; then
    cp -p "$APP_ROOT/core/private_config.php" "$BACKUP_DIR/private_config.php.before"
    chmod 0600 "$BACKUP_DIR/private_config.php.before" 2>/dev/null || true

    mkdir -p "$(dirname "$PRIVATE_CONFIG_PATH")"
    if [ -f "$PRIVATE_CONFIG_PATH" ]; then
        if ! cmp -s "$APP_ROOT/core/private_config.php" "$PRIVATE_CONFIG_PATH"; then
            echo "Refusing migration: public and external private_config.php differ." >&2
            exit 1
        fi
    else
        install -m 0600 "$APP_ROOT/core/private_config.php" "$PRIVATE_CONFIG_PATH"
    fi
    cmp -s "$APP_ROOT/core/private_config.php" "$PRIVATE_CONFIG_PATH" || {
        echo "Private configuration verification failed." >&2
        exit 1
    }
    rm -f "$APP_ROOT/core/private_config.php"
fi
if [ ! -r "$PRIVATE_CONFIG_PATH" ]; then
    echo "External private configuration is missing or unreadable: $PRIVATE_CONFIG_PATH" >&2
    exit 1
fi
chmod 0600 "$PRIVATE_CONFIG_PATH" 2>/dev/null || true

if [ -n "$DB_DEFAULTS_FILE" ] || [ -n "$DB_NAME" ]; then
    if [ -z "$DB_DEFAULTS_FILE" ] || [ -z "$DB_NAME" ]; then
        echo "Both --db-defaults-file and --db-name are required for a database backup." >&2
        exit 1
    fi
    if [ ! -f "$DB_DEFAULTS_FILE" ]; then
        echo "Database defaults file was not found." >&2
        exit 1
    fi
    if ! command -v mysqldump >/dev/null 2>&1; then
        echo "mysqldump is required for the requested database backup." >&2
        exit 1
    fi
    mysqldump \
        --defaults-extra-file="$DB_DEFAULTS_FILE" \
        --single-transaction \
        --quick \
        --routines \
        --triggers \
        "$DB_NAME" > "$BACKUP_DIR/database-before.sql"
    chmod 0600 "$BACKUP_DIR/database-before.sql" 2>/dev/null || true
fi

QUARANTINE_DIR="$BACKUP_DIR/quarantine"
mkdir -p "$QUARANTINE_DIR"
quarantine_file() {
    relative=$1
    [ -n "$relative" ] || return 0
    case "$relative" in
        /*|../*|*/../*|*/..)
            echo "Unsafe quarantine path: $relative" >&2
            exit 1
            ;;
    esac
    source_path="$APP_ROOT/$relative"
    target_path="$QUARANTINE_DIR/$relative"
    [ -e "$source_path" ] || return 0
    mkdir -p "$(dirname "$target_path")"
    mv "$source_path" "$target_path"
}

while IFS= read -r relative; do
    quarantine_file "$relative"
done < "$UNEXPECTED_LIST"
while IFS= read -r relative; do
    quarantine_file "$relative"
done < "$RUNTIME_SUSPICIOUS_LIST"

# Deploy using a tar stream so hidden files and directory structure are preserved.
tar -C "$OVERLAY" -cf - . | tar -C "$APP_ROOT" -xf -

find "$APP_ROOT/scripts" "$APP_ROOT/ops" -maxdepth 1 -type f -name '*.sh' -exec chmod 0750 {} \; 2>/dev/null || true
find "$APP_ROOT/scripts" -maxdepth 1 -type f -name '*.php' -exec chmod 0640 {} \; 2>/dev/null || true
chmod 0640 "$APP_ROOT/core/private_config.example.php" 2>/dev/null || true

LINT_LOG="$BACKUP_DIR/php-lint.txt"
: > "$LINT_LOG"
while IFS= read -r relative; do
    case "$relative" in
        *.php)
            "$PHP_BIN" -l "$APP_ROOT/$relative" >> "$LINT_LOG"
            ;;
    esac
done < "$FILES_LIST"

SHELL_LINT_LOG="$BACKUP_DIR/shell-lint.txt"
: > "$SHELL_LINT_LOG"
while IFS= read -r relative; do
    case "$relative" in
        *.sh)
            bash -n "$APP_ROOT/$relative"
            printf 'OK %s\n' "$relative" >> "$SHELL_LINT_LOG"
            ;;
    esac
done < "$FILES_LIST"

if [ -n "$DB_DEFAULTS_FILE" ] && [ -n "$DB_NAME" ]; then
    if ! command -v mysql >/dev/null 2>&1; then
        echo "mysql client is required to apply the v7 migration." >&2
        exit 1
    fi
    mysql --defaults-extra-file="$DB_DEFAULTS_FILE" "$DB_NAME" \
        < "$APP_ROOT/database/migrations/2026_07_14_billing_v7_production.sql" \
        > "$BACKUP_DIR/migration-v7.txt"
    chmod 0600 "$BACKUP_DIR/migration-v7.txt" 2>/dev/null || true
fi

{
    echo "deployed_at_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    echo "app_root=$APP_ROOT"
    echo "package_root=$PACKAGE_ROOT"
    echo "backup_dir=$BACKUP_DIR"
    echo "private_config_path=$PRIVATE_CONFIG_PATH"
    echo "files_count=$(wc -l < "$FILES_LIST" | tr -d ' ')"
    echo "new_files_count=$(wc -l < "$NEW_LIST" | tr -d ' ')"
    echo "quarantined_unexpected_count=$(wc -l < "$UNEXPECTED_LIST" | tr -d ' ')"
    echo "quarantined_runtime_suspicious_count=$(wc -l < "$RUNTIME_SUSPICIOUS_LIST" | tr -d ' ')"
    echo "database_backup=$([ -f "$BACKUP_DIR/database-before.sql" ] && echo yes || echo no)"
} > "$BACKUP_DIR/deployment-record.txt"

cat <<EOF
ICYLAB Billing v7 deployed successfully.

Backup: $BACKUP_DIR
Next mandatory command:
  $PHP_BIN $APP_ROOT/scripts/billing_production_preflight.php --env=production --stage=readiness --strict --json=$BACKUP_DIR/preflight.json

Runtime gates remain controlled by the external private configuration. Do not issue a DTE until the production preflight passes.
After read/finance preflight, reconcile existing links with:
  $PHP_BIN $APP_ROOT/scripts/reconcile_quote_invoice_statuses.php --env=production --limit=10000 --apply=RECONCILE_QUOTES
EOF
