#!/bin/bash
# =============================================
# Generic Staging → Production Deploy Script
# =============================================
# Usage:
#   ./deploy_generic.sh <file_list.txt> [staging_path]              Deploy
#   ./deploy_generic.sh <file_list.txt> [staging_path] --dry-run    Preview only
#   ./deploy_generic.sh --rollback <backup_folder>                  Rollback
#   ./deploy_generic.sh --list-backups                              Show all backups
#   ./deploy_generic.sh --cleanup [days]                            Delete old backups (default: 30 days)
#
# file_list.txt = one file/folder path per line. # comments and blank lines are skipped.
# Folders are auto-detected and deployed recursively (merge mode: adds/updates
# files in production, preserves existing files not in staging).

set -e

# --- Colors ---
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'

# --- Lock file (prevent concurrent deploys) ---
LOCK_FILE="/tmp/deploy_generic_$(pwd | md5sum | cut -d' ' -f1).lock"

acquire_lock() {
    if [ -f "$LOCK_FILE" ]; then
        LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null)
        if kill -0 "$LOCK_PID" 2>/dev/null; then
            echo -e "${RED}ERROR: Another deployment is running (PID: $LOCK_PID).${NC}"
            echo "If this is wrong, remove the lock: rm $LOCK_FILE"
            exit 1
        else
            echo -e "${YELLOW}Stale lock found (PID $LOCK_PID not running). Removing.${NC}"
            rm -f "$LOCK_FILE"
        fi
    fi
    echo $$ > "$LOCK_FILE"
}

release_lock() {
    rm -f "$LOCK_FILE"
}

# Release lock on exit (normal, error, or interrupt)
trap release_lock EXIT

# --- Deploy history logger ---
log_to_history() {
    local STATUS="$1"
    local FILE_COUNT="$2"
    local FILE_LIST="$3"
    local BACKUP="$4"
    local HISTORY_FILE="./deploy_backup/deploy_history.log"
    mkdir -p ./deploy_backup
    echo "$(date '+%Y-%m-%d %H:%M:%S') | $(whoami) | $STATUS | $FILE_COUNT files | list: $FILE_LIST | backup: $BACKUP" >> "$HISTORY_FILE"
}

# --- Cleanup mode ---
if [ "$1" == "--cleanup" ]; then
    DAYS="${2:-30}"
    echo -e "${YELLOW}Deleting backups older than $DAYS days...${NC}"
    DELETED=0
    if ls -d ./deploy_backup/pre_deploy_* 1>/dev/null 2>&1; then
        for dir in $(find ./deploy_backup -maxdepth 1 -name "pre_deploy_*" -type d -mtime +$DAYS); do
            echo "  Removing: $dir"
            rm -rf "$dir"
            DELETED=$((DELETED + 1))
        done
    fi
    # Clean old log files too
    if ls ./deploy_backup/deploy_*.log 1>/dev/null 2>&1; then
        for f in $(find ./deploy_backup -maxdepth 1 -name "deploy_*.log" ! -name "deploy_history.log" -type f -mtime +$DAYS); do
            echo "  Removing: $f"
            rm -f "$f"
            DELETED=$((DELETED + 1))
        done
    fi
    echo -e "${GREEN}Cleaned up $DELETED item(s). deploy_history.log preserved.${NC}"
    exit 0
fi

# --- Rollback mode ---
if [ "$1" == "--rollback" ]; then
    if [ -z "$2" ]; then
        echo -e "${RED}Usage: ./deploy_generic.sh --rollback <backup_folder>${NC}"
        echo ""
        echo "Available backups:"
        ls -dt ./deploy_backup/pre_deploy_* 2>/dev/null || echo "  No backups found."
        exit 1
    fi
    BACKUP="$2"
    if [ ! -d "$BACKUP" ]; then
        echo -e "${RED}ERROR: Backup folder '$BACKUP' not found.${NC}"
        exit 1
    fi

    acquire_lock

    echo -e "${YELLOW}Rolling back from: $BACKUP${NC}"
    FILE_COUNT=$(find "$BACKUP" -type f | wc -l)
    echo -e "${YELLOW}Files to restore: $FILE_COUNT${NC}"
    echo ""

    # Show files that will be restored
    echo "Files:"
    find "$BACKUP" -type f | while read f; do
        echo "  ${f#$BACKUP/}"
    done
    echo ""

    read -p "Proceed with rollback? (y/N): " CONFIRM
    if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then
        echo "Rollback cancelled."
        exit 0
    fi
    cp -rp "$BACKUP"/* ./
    echo -e "${GREEN}Rollback complete. $FILE_COUNT files restored.${NC}"
    log_to_history "ROLLBACK" "$FILE_COUNT" "N/A" "$BACKUP"
    exit 0
fi

# --- List backups mode ---
if [ "$1" == "--list-backups" ]; then
    echo "Available backups (newest first):"
    echo ""
    if ls -dt ./deploy_backup/pre_deploy_* 1>/dev/null 2>&1; then
        for dir in $(ls -dt ./deploy_backup/pre_deploy_*); do
            FILE_COUNT=$(find "$dir" -type f | wc -l)
            SIZE=$(du -sh "$dir" | cut -f1)
            echo "  $dir  ($FILE_COUNT files, $SIZE)"
        done
    else
        echo "  No backups found."
    fi
    echo ""
    # Show deploy history
    if [ -f "./deploy_backup/deploy_history.log" ]; then
        echo "Recent deploy history:"
        tail -10 ./deploy_backup/deploy_history.log | while read line; do
            echo "  $line"
        done
    fi
    exit 0
fi

# --- Deploy mode ---
if [ -z "$1" ]; then
    echo "Usage:"
    echo "  ./deploy_generic.sh <file_list.txt> [staging_path]              Deploy"
    echo "  ./deploy_generic.sh <file_list.txt> [staging_path] --dry-run    Preview"
    echo "  ./deploy_generic.sh --rollback <backup_folder>                  Rollback"
    echo "  ./deploy_generic.sh --list-backups                              Show backups"
    echo "  ./deploy_generic.sh --cleanup [days]                            Delete old backups"
    exit 1
fi

acquire_lock

FILE_LIST="$1"
DRY_RUN=false

# Parse args — staging path and --dry-run can be in any order
STAGING_DIR="./staging"
for arg in "${@:2}"; do
    if [ "$arg" == "--dry-run" ]; then
        DRY_RUN=true
    else
        STAGING_DIR="$arg"
    fi
done

if [ ! -f "$FILE_LIST" ]; then
    echo -e "${RED}ERROR: File list '$FILE_LIST' not found.${NC}"
    exit 1
fi

if [ ! -d "$STAGING_DIR" ]; then
    echo -e "${RED}ERROR: Staging directory '$STAGING_DIR' not found.${NC}"
    exit 1
fi

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="./deploy_backup/pre_deploy_${TIMESTAMP}"
LOG_FILE="./deploy_backup/deploy_${TIMESTAMP}.log"

# Read file list
FILES=()
while IFS= read -r line || [ -n "$line" ]; do
    line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    [[ -z "$line" || "$line" == \#* ]] && continue
    FILES+=("$line")
done < "$FILE_LIST"

if [ ${#FILES[@]} -eq 0 ]; then
    echo -e "${RED}ERROR: No files found in '$FILE_LIST'.${NC}"
    exit 1
fi

# Logging function
log() {
    local msg="$1"
    echo -e "$msg"
    # Strip color codes for log file
    echo "$(date '+%H:%M:%S') $(echo "$msg" | sed 's/\x1b\[[0-9;]*m//g')" >> "$LOG_FILE" 2>/dev/null
}

echo -e "${CYAN}==========================================${NC}"
if [ "$DRY_RUN" = true ]; then
    echo -e "${YELLOW}  *** DRY RUN — No files will be changed ***${NC}"
fi
echo -e "${CYAN}  Staging → Production Deploy${NC}"
echo -e "${CYAN}  $(date)${NC}"
echo -e "${CYAN}  Staging:  $STAGING_DIR${NC}"
echo -e "${CYAN}  Prod:     $(pwd)${NC}"
echo -e "${CYAN}  Files:    ${#FILES[@]} (from $FILE_LIST)${NC}"
echo -e "${CYAN}==========================================${NC}"

# Create log dir
mkdir -p ./deploy_backup

# --- Pre-flight ---
echo ""
log "=== Pre-flight: Checking files ==="
MISSING_STAGING=0
NEW_IN_PROD=0
CHANGED=0
UNCHANGED=0

for file in "${FILES[@]}"; do
    if [ -d "$STAGING_DIR/$file" ]; then
        # --- Directory entry ---
        STAGING_FCOUNT=$(find "$STAGING_DIR/$file" -type f | wc -l)
        if [ ! -d "./$file" ]; then
            log "  [NEW DIR]   $file/ ($STAGING_FCOUNT files)"
            NEW_IN_PROD=$((NEW_IN_PROD + 1))
        else
            DIR_DIFF=$(diff -rq "$STAGING_DIR/$file" "./$file" 2>/dev/null | grep -c 'differ\|Only in' || true)
            if [ "$DIR_DIFF" -eq 0 ]; then
                log "  [UNCHANGED] $file/ (directory, $STAGING_FCOUNT files)"
                UNCHANGED=$((UNCHANGED + 1))
            else
                log "  [CHANGED]   $file/ ($DIR_DIFF difference(s) across $STAGING_FCOUNT files)"
                CHANGED=$((CHANGED + 1))
            fi
        fi
    elif [ -f "$STAGING_DIR/$file" ]; then
        # --- File entry (original logic) ---
        if [ ! -f "./$file" ]; then
            log "  [NEW]       $file"
            NEW_IN_PROD=$((NEW_IN_PROD + 1))
        elif cmp -s "$STAGING_DIR/$file" "./$file"; then
            log "  [UNCHANGED] $file"
            UNCHANGED=$((UNCHANGED + 1))
        else
            STAGING_SIZE=$(stat -c%s "$STAGING_DIR/$file" 2>/dev/null || stat -f%z "$STAGING_DIR/$file" 2>/dev/null)
            PROD_SIZE=$(stat -c%s "./$file" 2>/dev/null || stat -f%z "./$file" 2>/dev/null)
            log "  [CHANGED]   $file  (prod: ${PROD_SIZE}B → staging: ${STAGING_SIZE}B)"
            CHANGED=$((CHANGED + 1))
        fi
    else
        log "  ${RED}MISSING in staging: $file${NC}"
        MISSING_STAGING=$((MISSING_STAGING + 1))
    fi
done

echo ""
echo -e "${CYAN}Summary:${NC}"
echo -e "  Changed:   $CHANGED"
echo -e "  New:       $NEW_IN_PROD"
echo -e "  Unchanged: $UNCHANGED"
echo -e "  Missing:   $MISSING_STAGING"

if [ $MISSING_STAGING -gt 0 ]; then
    echo -e "${RED}Aborting. $MISSING_STAGING file(s) missing in staging.${NC}"
    log_to_history "ABORTED (missing files)" "${#FILES[@]}" "$FILE_LIST" "N/A"
    exit 1
fi

if [ $CHANGED -eq 0 ] && [ $NEW_IN_PROD -eq 0 ]; then
    echo -e "${GREEN}All files are identical. Nothing to deploy.${NC}"
    exit 0
fi

# --- Dry run stops here ---
if [ "$DRY_RUN" = true ]; then
    echo ""
    echo -e "${YELLOW}Dry run complete. $((CHANGED + NEW_IN_PROD)) file(s) would be deployed.${NC}"
    echo -e "${YELLOW}Run without --dry-run to deploy.${NC}"
    exit 0
fi

# --- Confirmation ---
echo ""
read -p "Deploy $((CHANGED + NEW_IN_PROD)) file(s) to production? (y/N): " CONFIRM
if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then
    echo "Deployment cancelled."
    log_to_history "CANCELLED" "${#FILES[@]}" "$FILE_LIST" "N/A"
    exit 0
fi

# --- Step 1: Backup (preserve permissions) ---
echo ""
log "=== Step 1: Backing up production files ==="
mkdir -p "$BACKUP_DIR"

for file in "${FILES[@]}"; do
    mkdir -p "$BACKUP_DIR/$(dirname "$file")"
    if [ -d "./$file" ]; then
        cp -rp "./$file" "$BACKUP_DIR/$file"
    elif [ -f "./$file" ]; then
        cp -p "./$file" "$BACKUP_DIR/$file"
    fi
done
log "  Backup saved to $BACKUP_DIR/"

# --- Step 2: Deploy (preserve permissions) ---
echo ""
log "=== Step 2: Deploying ==="
DEPLOYED=0
DEPLOYED_DIRS=0
for file in "${FILES[@]}"; do
    if [ -d "$STAGING_DIR/$file" ]; then
        # Directory deploy (merge mode: copies staging into prod, preserves existing)
        mkdir -p "./$file"
        cp -r "$STAGING_DIR/$file/." "./$file/"
        DEPLOYED_DIRS=$((DEPLOYED_DIRS + 1))
    else
        mkdir -p "$(dirname "./$file")"
        # Preserve original file permissions if file exists
        if [ -f "./$file" ]; then
            ORIG_PERMS=$(stat -c%a "./$file" 2>/dev/null || stat -f%Lp "./$file" 2>/dev/null)
            cp "$STAGING_DIR/$file" "./$file"
            chmod "$ORIG_PERMS" "./$file"
        else
            cp "$STAGING_DIR/$file" "./$file"
            chmod 644 "./$file"
        fi
        DEPLOYED=$((DEPLOYED + 1))
    fi
done
log "  $DEPLOYED file(s) + $DEPLOYED_DIRS dir(s) deployed (permissions preserved)."

# --- Step 3: Verify ---
echo ""
log "=== Step 3: Verifying ==="
FAIL=0
for file in "${FILES[@]}"; do
    if [ -d "$STAGING_DIR/$file" ]; then
        DIFF_OUTPUT=$(diff -rq "$STAGING_DIR/$file" "./$file" 2>/dev/null | grep -E "differ|Only in $STAGING_DIR" || true)
        if [ -n "$DIFF_OUTPUT" ]; then
            log "  ${RED}MISMATCH (dir): $file/${NC}"
            echo "$DIFF_OUTPUT" | head -5 | while read diffline; do
                log "    $diffline"
            done
            FAIL=$((FAIL + 1))
        fi
    else
        if ! cmp -s "$STAGING_DIR/$file" "./$file"; then
            log "  ${RED}MISMATCH: $file${NC}"
            FAIL=$((FAIL + 1))
        fi
    fi
done

# --- Step 4: Post-deploy health check ---
echo ""
log "=== Step 4: Health check ==="

# Try to detect site URL from current directory
SITE_URL=""
DIR_NAME=$(basename "$(pwd)")
if [[ "$DIR_NAME" == *"."* ]]; then
    SITE_URL="https://$DIR_NAME"
fi

if [ -n "$SITE_URL" ]; then
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$SITE_URL" 2>/dev/null || echo "FAIL")
    if [ "$HTTP_CODE" == "200" ] || [ "$HTTP_CODE" == "302" ] || [ "$HTTP_CODE" == "301" ]; then
        log "  ${GREEN}✓ $SITE_URL → HTTP $HTTP_CODE${NC}"
    elif [ "$HTTP_CODE" == "FAIL" ]; then
        log "  ${YELLOW}⚠ Could not reach $SITE_URL (network/timeout)${NC}"
    else
        log "  ${RED}✗ $SITE_URL → HTTP $HTTP_CODE — CHECK IMMEDIATELY${NC}"
    fi

    # Check admin panel too
    ADMIN_URL="$SITE_URL/admin"
    HTTP_CODE_ADMIN=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$ADMIN_URL" 2>/dev/null || echo "FAIL")
    if [ "$HTTP_CODE_ADMIN" == "200" ] || [ "$HTTP_CODE_ADMIN" == "302" ] || [ "$HTTP_CODE_ADMIN" == "301" ]; then
        log "  ${GREEN}✓ $ADMIN_URL → HTTP $HTTP_CODE_ADMIN${NC}"
    elif [ "$HTTP_CODE_ADMIN" != "FAIL" ]; then
        log "  ${RED}✗ $ADMIN_URL → HTTP $HTTP_CODE_ADMIN — CHECK IMMEDIATELY${NC}"
    fi
else
    log "  ${YELLOW}⚠ Could not detect site URL. Verify manually.${NC}"
fi

# --- Result ---
echo ""
echo -e "${CYAN}==========================================${NC}"
if [ $FAIL -eq 0 ]; then
    echo -e "${GREEN}  DEPLOYMENT SUCCESSFUL${NC}"
    echo -e "${GREEN}  ${#FILES[@]}/${#FILES[@]} files deployed & verified${NC}"
    log_to_history "SUCCESS" "${#FILES[@]}" "$FILE_LIST" "$BACKUP_DIR"
else
    echo -e "${RED}  WARNING: $FAIL file(s) failed verification${NC}"
    log_to_history "PARTIAL ($FAIL failures)" "${#FILES[@]}" "$FILE_LIST" "$BACKUP_DIR"
fi
echo -e "${CYAN}  Backup: $BACKUP_DIR/${NC}"
echo -e "${CYAN}  Log:    $LOG_FILE${NC}"
echo -e "${CYAN}==========================================${NC}"

# --- OPcache reminder ---
echo ""
echo -e "${YELLOW}REMINDER: Clear PHP OPcache if enabled.${NC}"
echo -e "${YELLOW}  Add ?opcache_reset=1 to any page, or restart PHP-FPM.${NC}"

echo ""
echo -e "${YELLOW}ROLLBACK: ./deploy_generic.sh --rollback $BACKUP_DIR${NC}"
echo ""
