#!/bin/bash
# VM Deployment Script
# Clones VM from template, configures it, and optionally runs Ansible playbooks

set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
ANSIBLE_DIR="$ROOT_DIR/ansible"

# Logging
log_info() {
    echo -e "${GREEN}[INFO]${NC} $*"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $*"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $*"
}

log_step() {
    echo -e "${BLUE}[STEP]${NC} $*"
}

# Clone VM from template
clone_vm() {
    local template_id=$1
    local new_vm_id=$2
    local vm_name=$3
    local clone_type=${4:-linked}  # Default to linked clone

    log_step "Cloning VM from template $template_id (${clone_type} clone)..."

    # Check if VM ID exists (cluster-wide if in a cluster)
    if pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | grep -q "\"vmid\":$new_vm_id"; then
        log_error "VM ID $new_vm_id already exists in the cluster"
        return 1
    elif qm status "$new_vm_id" &>/dev/null; then
        log_error "VM ID $new_vm_id already exists on this node"
        return 1
    fi

    # Run clone and capture output
    local clone_output
    if [[ "$clone_type" == "full" ]]; then
        clone_output=$(qm clone "$template_id" "$new_vm_id" --name "$vm_name" --full 2>&1)
    else
        clone_output=$(qm clone "$template_id" "$new_vm_id" --name "$vm_name" 2>&1)
    fi

    # Check if clone succeeded
    if [[ $? -ne 0 ]] || ! qm status "$new_vm_id" &>/dev/null; then
        log_error "Failed to clone VM"
        echo "$clone_output"
        return 1
    fi

    log_info "VM $new_vm_id ($vm_name) created as ${clone_type} clone"
}

# Configure VM resources
configure_vm() {
    local vm_id=$1
    local cores=$2
    local memory=$3

    log_step "Configuring VM $vm_id..."

    qm set "$vm_id" --cores "$cores" --memory "$memory"
    log_info "VM configured: $cores cores, ${memory}MB RAM"
}

# Resize disk to selected size
resize_disk() {
    local vm_id=$1
    local disk_size=$2
    shift 2
    local playbooks=("$@")

    # Check if MSSQL is in playbooks (requires minimum 20GB)
    local has_mssql=false
    for playbook in "${playbooks[@]}"; do
        if [[ "$playbook" == "mssql" ]]; then
            has_mssql=true
            break
        fi
    done

    # If MSSQL is selected and disk size is less than 20GB, warn and use 20GB
    if [[ "$has_mssql" == "true" ]]; then
        local disk_size_num="${disk_size%G}"
        if [[ $disk_size_num -lt 20 ]]; then
            log_warn "MSSQL requires minimum 20GB, increasing from ${disk_size} to 20G"
            disk_size="20G"
        fi
    fi

    log_step "Resizing disk to ${disk_size}..."

    # Resize scsi0 to selected size
    if qm disk resize "$vm_id" scsi0 "$disk_size"; then
        log_info "Disk resized to ${disk_size} (filesystem will auto-grow on boot)"
    else
        log_warn "Failed to resize disk to ${disk_size}"
        return 1
    fi
}

# Configure network (DHCP or static)
configure_network() {
    local vm_id=$1
    local network_mode=$2
    local static_ip=${3:-}
    local gateway=${4:-}
    local netmask=${5:-24}

    log_step "Configuring network for VM $vm_id..."

    if [[ "$network_mode" == "static" ]] && [[ -n "$static_ip" ]]; then
        qm set "$vm_id" --ipconfig0 "ip=${static_ip}/${netmask},gw=${gateway}"
        log_info "Static IP configured: $static_ip"
    else
        qm set "$vm_id" --ipconfig0 "ip=dhcp"
        log_info "DHCP configured"
    fi
}

# Start VM and wait for it to boot
start_and_wait_vm() {
    local vm_id=$1
    local max_wait=${2:-180}

    log_step "Starting VM $vm_id..."
    qm start "$vm_id"
    log_info "VM started"

    log_step "Waiting for QEMU agent (max ${max_wait}s)..."

    local attempts=0
    local interval=3
    local max_attempts=$((max_wait / interval))

    while [[ $attempts -lt $max_attempts ]]; do
        if qm agent "$vm_id" ping &>/dev/null; then
            log_info "QEMU agent responded after $((attempts * interval)) seconds"
            return 0
        fi

        if (( attempts % 10 == 0 )) && (( attempts > 0 )); then
            echo -n " [${attempts}s]"
        else
            echo -n "."
        fi

        sleep $interval
        ((attempts++))
    done

    echo
    log_error "QEMU agent did not respond after ${max_wait} seconds"
    return 1
}

# Get VM IP address
get_vm_ip() {
    local vm_id=$1
    local max_wait=${2:-60}  # Wait up to 60 seconds for IP
    local attempts=0
    local interval=3

    log_info "Waiting for VM to get IP address (max ${max_wait}s)..." >&2

    while [[ $attempts -lt $((max_wait / interval)) ]]; do
        # Try to get IP from QEMU agent, excluding loopback and common private networks
        local ip
        ip=$(qm agent "$vm_id" network-get-interfaces 2>/dev/null | \
             grep -oP '"ip-address" : "\K[0-9.]+' | \
             grep -v "127.0.0.1" | \
             grep -v "^172\.17\." | \
             grep -v "^172\.18\." | \
             head -1)

        if [[ -n "$ip" ]]; then
            echo "$ip"
            return 0
        fi

        # Show progress
        if (( attempts > 0 )); then
            echo -n "." >&2
        fi

        sleep $interval
        ((attempts++))
    done

    echo >&2
    log_warn "No IP found via QEMU agent, trying config fallback..." >&2

    # Fallback: try to get from Proxmox config (for static IPs)
    local ip
    ip=$(qm config "$vm_id" | grep -oP 'ip=\K[0-9.]+' | head -1)

    if [[ -n "$ip" ]]; then
        echo "$ip"
        return 0
    fi

    return 1
}

# Wait for SSH to be ready
wait_for_ssh() {
    local vm_ip=$1
    local max_wait=${2:-30}
    local attempts=0
    local interval=2

    log_step "Waiting for SSH to be ready on ${vm_ip} (max ${max_wait}s)..." >&2

    while [[ $attempts -lt $((max_wait / interval)) ]]; do
        # Just check if SSH port is open and accepting connections
        if timeout 3 nc -z "$vm_ip" 22 &>/dev/null; then
            log_info "SSH port is open, waiting 3 seconds for service to stabilize..." >&2
            sleep 3
            log_info "SSH is ready" >&2
            return 0
        fi

        echo -n "." >&2
        sleep $interval
        ((attempts++))
    done

    echo >&2
    log_warn "SSH port did not open after ${max_wait} seconds" >&2
    return 1
}

# Run Ansible playbooks
run_ansible_playbooks() {
    local vm_ip=$1
    shift
    local playbooks=("$@")

    if [[ ${#playbooks[@]} -eq 0 ]]; then
        log_info "No playbooks selected"
        return 0
    fi

    log_step "Running Ansible playbooks..."

    cd "$ANSIBLE_DIR" || return 1

    # Source defaults.conf for cloud-init credentials
    local config_file="$ROOT_DIR/config/defaults.conf"
    if [[ -f "$config_file" ]]; then
        # Validate config contains only KEY=VALUE, comments, and blank lines
        if grep -Pv '^\s*$|^\s*#|^[A-Za-z_][A-Za-z0-9_]*=' "$config_file" | grep -q .; then
            log_error "defaults.conf contains invalid lines. Only KEY=VALUE, comments, and blank lines are allowed."
            return 1
        fi
        # shellcheck source=/dev/null
        source "$config_file"
    else
        log_error "Configuration file not found: $config_file"
        return 1
    fi

    # Export password for Ansible
    export ANSIBLE_PASSWORD="${DEFAULT_CLOUDINIT_PASSWORD}"

    # Set timestamped Ansible log file
    local timestamp=$(date +%Y%m%d_%H%M%S)
    local ansible_log="$ROOT_DIR/logs/ansible-${timestamp}.log"
    export ANSIBLE_LOG_PATH="$ansible_log"
    log_info "Ansible logs: $ansible_log"

    # Valid playbook names
    local VALID_PLAYBOOKS="base-config users nginx postgresql mysql mssql mongodb docker-compose k3s k3s-cluster-server k3s-cluster-agent monitoring security-harden"

    local failed=0
    for playbook in "${playbooks[@]}"; do
        # Strip path separators (security: prevent path traversal)
        playbook="${playbook//\//}"
        playbook="${playbook//\\/}"

        # Validate playbook name against known list
        if ! echo "$VALID_PLAYBOOKS" | grep -qw "$playbook"; then
            log_error "Unknown playbook: $playbook"
            log_error "Valid playbooks: $VALID_PLAYBOOKS"
            ((failed++))
            continue
        fi

        local playbook_file="playbooks/${playbook}.yml"

        if [[ ! -f "$playbook_file" ]]; then
            log_warn "Playbook not found: $playbook_file"
            ((failed++))
            continue
        fi

        log_info "Running playbook: $playbook"

        # Pass passwords via stdin JSON to avoid exposure in process table
        if ansible-playbook -i "${vm_ip}," \
            --extra-vars "@/dev/stdin" \
            "$playbook_file" <<EOF
{"ansible_password": "${DEFAULT_CLOUDINIT_PASSWORD}", "ansible_become_password": "${DEFAULT_CLOUDINIT_PASSWORD}", "lab_user": "${LAB_USER:?ERROR: LAB_USER not set in defaults.conf}", "lab_password": "${LAB_PASSWORD:?ERROR: LAB_PASSWORD not set in defaults.conf}"}
EOF
        then
            log_info "✓ Playbook completed: $playbook"
        else
            log_error "✗ Playbook failed: $playbook"
            ((failed++))
        fi
    done

    if [[ $failed -eq 0 ]]; then
        log_info "All playbooks completed successfully"
        return 0
    else
        log_warn "$failed playbook(s) failed"
        return 1
    fi
}

# Main deployment function
deploy_vm() {
    local template_id=$1
    local new_vm_id=$2
    local vm_name=$3
    local cores=$4
    local memory=$5
    local disk_size=$6
    local network_mode=$7
    local clone_type=$8
    shift 8
    local playbooks=("$@")

    # Validate VM name (security: prevent injection)
    if [[ ! "$vm_name" =~ ^[a-zA-Z0-9._-]+$ ]]; then
        log_error "Invalid VM name: $vm_name (only alphanumeric, dots, hyphens, underscores allowed)"
        return 1
    fi

    echo "========================================"
    echo "VM Deployment"
    echo "========================================"
    echo "Template ID: $template_id"
    echo "New VM ID:   $new_vm_id"
    echo "VM Name:     $vm_name"
    echo "Resources:   ${cores} cores, ${memory}MB RAM, ${disk_size} disk"
    echo "Network:     $network_mode"
    echo "Clone Type:  $clone_type"
    echo "Playbooks:   ${playbooks[*]:-none}"
    echo

    # Clone VM
    if ! clone_vm "$template_id" "$new_vm_id" "$vm_name" "$clone_type"; then
        return 1
    fi

    # Resize disk to selected size (before starting VM)
    resize_disk "$new_vm_id" "$disk_size" "${playbooks[@]}"

    # Configure resources
    configure_vm "$new_vm_id" "$cores" "$memory"

    # Configure network
    configure_network "$new_vm_id" "$network_mode"

    # Start and wait
    if ! start_and_wait_vm "$new_vm_id" 180; then
        log_error "Failed to start VM"
        return 1
    fi

    # Get IP
    log_step "Getting VM IP address..."
    local vm_ip
    if ! vm_ip=$(get_vm_ip "$new_vm_id"); then
        log_error "Failed to get VM IP address"
        return 1
    fi
    log_info "VM IP: $vm_ip"

    # Wait for SSH to be ready before running Ansible
    if [[ ${#playbooks[@]} -gt 0 ]]; then
        if ! wait_for_ssh "$vm_ip" 30; then
            log_warn "SSH not ready, but continuing anyway"
        fi
    fi

    # Run Ansible playbooks
    if [[ ${#playbooks[@]} -gt 0 ]]; then
        if ! run_ansible_playbooks "$vm_ip" "${playbooks[@]}"; then
            log_warn "Some playbooks failed, but VM is running"
        fi
    fi

    echo
    echo "========================================"
    log_info "Deployment Complete!"
    echo "========================================"
    echo "VM ID:   $new_vm_id"
    echo "Name:    $vm_name"
    echo "IP:      $vm_ip"
    echo

    return 0
}

# Usage
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    if [[ $# -lt 8 ]]; then
        echo "Usage: $0 <template_id> <new_vm_id> <vm_name> <cores> <memory> <disk_size> <network_mode> <clone_type> [playbook1] [playbook2] ..."
        echo "  disk_size: Disk size (e.g., 20G, 40G, 60G, 100G)"
        echo "  clone_type: 'linked' (default, faster) or 'full' (independent copy)"
        exit 1
    fi

    deploy_vm "$@"
fi
