#!/bin/bash

# SSL Certificate Manager for Custom Domains
# Uses certbot for Let's Encrypt certificates and manages Apache VirtualHosts
#
# Usage: ./ssl-manager.sh <command> <domain>
#
# Commands:
#   provision <domain>   - Request cert via certbot, create VirtualHost, reload httpd
#   revoke <domain>      - Revoke cert, remove VirtualHost, reload httpd
#   status <domain>      - Return cert status as JSON
#   verify-dns <domain>  - Check A record points to SERVER_IP
#
# Exit codes:
#   0 - Success
#   1 - General error
#   2 - DNS verification failed
#   3 - Certbot rate limit hit
#   4 - Apache config error
#   5 - Permission denied

set -e

# Configuration
LOCK_FILE="/var/lock/ssl-manager.lock"
VHOST_DIR="/etc/apache2/conf.d/custom-domains"
CERTBOT_WEBROOT="/var/www/html"
LOG_FILE="/var/log/ssl-manager.log"

# Load environment variables from .env.local if available
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
if [[ -f "$PROJECT_DIR/.env.local" ]]; then
    source "$PROJECT_DIR/.env.local"
fi

# Required environment variables
: "${SERVER_IP_ADDRESS:?SERVER_IP_ADDRESS environment variable is required}"
: "${CERTBOT_EMAIL:?CERTBOT_EMAIL environment variable is required}"
: "${PORT:=3001}"

# Logging function
log() {
    local level="$1"
    shift
    echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $*" >> "$LOG_FILE"
}

# JSON output helper
json_output() {
    local success="$1"
    local message="$2"
    local extra="$3"

    if [[ "$success" == "true" ]]; then
        if [[ -n "$extra" ]]; then
            echo "{\"success\":true,\"message\":\"$message\",$extra}"
        else
            echo "{\"success\":true,\"message\":\"$message\"}"
        fi
    else
        echo "{\"success\":false,\"error\":\"$message\"}"
    fi
}

# Validate domain format (basic check, main validation done in Node.js)
validate_domain() {
    local domain="$1"
    if [[ ! "$domain" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$ ]]; then
        json_output "false" "Invalid domain format"
        exit 1
    fi
    # Prevent path traversal
    if [[ "$domain" == *".."* ]] || [[ "$domain" == *"/"* ]]; then
        json_output "false" "Invalid domain format"
        exit 1
    fi
}

# Get certificate expiration date
get_cert_expiry() {
    local domain="$1"
    local cert_path="/etc/letsencrypt/live/$domain/fullchain.pem"

    if [[ -f "$cert_path" ]]; then
        openssl x509 -enddate -noout -in "$cert_path" 2>/dev/null | cut -d= -f2
    fi
}

# Convert date to ISO format
date_to_iso() {
    local date_str="$1"
    if [[ -n "$date_str" ]]; then
        date -d "$date_str" -Iseconds 2>/dev/null || echo ""
    fi
}

# Verify DNS A record
verify_dns() {
    local domain="$1"

    validate_domain "$domain"

    # Get A record IP (filter for valid IPv4, handles CNAME chains)
    local found_ip
    found_ip=$(dig +short A "$domain" 2>/dev/null | grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}$' | head -1)

    if [[ -z "$found_ip" ]]; then
        json_output "false" "No A record found for $domain"
        exit 2
    fi

    if [[ "$found_ip" == "$SERVER_IP_ADDRESS" ]]; then
        json_output "true" "DNS verification successful" "\"foundIp\":\"$found_ip\",\"expectedIp\":\"$SERVER_IP_ADDRESS\""
        exit 0
    else
        json_output "false" "DNS A record points to $found_ip, expected $SERVER_IP_ADDRESS"
        exit 2
    fi
}

# Check certificate status
check_status() {
    local domain="$1"

    validate_domain "$domain"

    local cert_path="/etc/letsencrypt/live/$domain/fullchain.pem"
    local vhost_path="$VHOST_DIR/custom-$domain.conf"

    if [[ ! -f "$cert_path" ]]; then
        json_output "true" "Certificate not found" "\"exists\":false,\"domain\":\"$domain\",\"vhostEnabled\":false"
        exit 0
    fi

    local expiry_date
    expiry_date=$(get_cert_expiry "$domain")
    local expiry_iso
    expiry_iso=$(date_to_iso "$expiry_date")

    local vhost_enabled="false"
    if [[ -f "$vhost_path" ]]; then
        vhost_enabled="true"
    fi

    # Calculate days until expiry
    local expiry_epoch
    expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null || echo "0")
    local now_epoch
    now_epoch=$(date +%s)
    local days_until_expiry=$(( (expiry_epoch - now_epoch) / 86400 ))

    json_output "true" "Certificate found" "\"exists\":true,\"domain\":\"$domain\",\"expiresAt\":\"$expiry_iso\",\"daysUntilExpiry\":$days_until_expiry,\"vhostEnabled\":$vhost_enabled"
    exit 0
}

# Create Apache VirtualHost config
create_vhost() {
    local domain="$1"
    local vhost_path="$VHOST_DIR/custom-$domain.conf"

    cat > "$vhost_path" << EOF
# Auto-generated VirtualHost for custom domain: $domain
# Generated by ssl-manager.sh on $(date -Iseconds)

<VirtualHost $SERVER_IP_ADDRESS:443>
    ServerName $domain
    ServerAlias www.$domain

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/$domain/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/$domain/privkey.pem

    # Proxy to Next.js application (uses shared balancer pool from pre_virtualhost_global.conf)
    ProxyPreserveHost On

    # Exclude error pages from proxying (prevents cascade when Next.js is down)
    ProxyPass /400.shtml !
    ProxyPass /502.shtml !
    ProxyPass /503.shtml !
    ProxyPass /504.shtml !

    ProxyPass / balancer://nextjs/
    ProxyPassReverse / http://127.0.0.1:$PORT/

    # Pass the original host header for routing
    RequestHeader set X-Forwarded-Host "$domain"
    RequestHeader set X-Forwarded-Proto "https"

    # Security headers
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</VirtualHost>
EOF

    log "INFO" "Created VirtualHost config: $vhost_path"
}

# Provision SSL certificate
provision() {
    local domain="$1"

    validate_domain "$domain"

    log "INFO" "Starting certificate provisioning for $domain"

    # Check if cert already exists
    local cert_path="/etc/letsencrypt/live/$domain/fullchain.pem"
    if [[ -f "$cert_path" ]]; then
        log "INFO" "Certificate already exists for $domain"
        local expiry_date
        expiry_date=$(get_cert_expiry "$domain")
        local expiry_iso
        expiry_iso=$(date_to_iso "$expiry_date")

        # Ensure VirtualHost exists
        create_vhost "$domain"

        # Reload Apache
        if ! systemctl reload httpd 2>/dev/null; then
            log "ERROR" "Failed to reload httpd"
            json_output "false" "Failed to reload Apache configuration"
            exit 4
        fi

        json_output "true" "Certificate already exists" "\"certPath\":\"$cert_path\",\"expiresAt\":\"$expiry_iso\""
        exit 0
    fi

    # Request certificate from Let's Encrypt for both www and non-www
    log "INFO" "Requesting certificate from Let's Encrypt for $domain and www.$domain"

    local certbot_output
    local certbot_exit_code

    certbot_output=$(certbot certonly \
        --webroot \
        -w "$CERTBOT_WEBROOT" \
        -d "$domain" \
        -d "www.$domain" \
        --non-interactive \
        --agree-tos \
        --email "$CERTBOT_EMAIL" \
        2>&1) || certbot_exit_code=$?

    if [[ -n "$certbot_exit_code" ]] && [[ "$certbot_exit_code" -ne 0 ]]; then
        log "ERROR" "Certbot failed: $certbot_output"

        # Check for rate limit error
        if echo "$certbot_output" | grep -qi "rate limit"; then
            json_output "false" "Rate limit exceeded. Please try again later."
            exit 3
        fi

        json_output "false" "Certificate request failed: $(echo "$certbot_output" | tail -1)"
        exit 1
    fi

    log "INFO" "Certificate obtained successfully for $domain"

    # Create VirtualHost config
    create_vhost "$domain"

    # Test Apache config
    if ! apachectl configtest 2>/dev/null; then
        log "ERROR" "Apache config test failed"
        # Remove bad config
        rm -f "$VHOST_DIR/custom-$domain.conf"
        json_output "false" "Apache configuration test failed"
        exit 4
    fi

    # Reload Apache
    if ! systemctl reload httpd 2>/dev/null; then
        log "ERROR" "Failed to reload httpd"
        json_output "false" "Failed to reload Apache configuration"
        exit 4
    fi

    log "INFO" "VirtualHost enabled and Apache reloaded for $domain"

    # Get expiry date
    local expiry_date
    expiry_date=$(get_cert_expiry "$domain")
    local expiry_iso
    expiry_iso=$(date_to_iso "$expiry_date")

    json_output "true" "Certificate provisioned successfully" "\"certPath\":\"$cert_path\",\"expiresAt\":\"$expiry_iso\""
    exit 0
}

# Revoke SSL certificate
revoke() {
    local domain="$1"

    validate_domain "$domain"

    log "INFO" "Starting certificate revocation for $domain"

    local cert_path="/etc/letsencrypt/live/$domain/fullchain.pem"
    local vhost_path="$VHOST_DIR/custom-$domain.conf"
    local errors=""

    # Remove VirtualHost config first (so Apache doesn't break)
    if [[ -f "$vhost_path" ]]; then
        rm -f "$vhost_path"
        log "INFO" "Removed VirtualHost config: $vhost_path"
    fi

    # Reload Apache to remove the site
    if ! systemctl reload httpd 2>/dev/null; then
        log "WARN" "Failed to reload httpd during revocation"
        errors="Failed to reload Apache; "
    fi

    # Revoke and delete certificate if it exists
    if [[ -f "$cert_path" ]]; then
        log "INFO" "Revoking certificate for $domain"

        if ! certbot revoke --cert-name "$domain" --non-interactive --delete-after-revoke 2>/dev/null; then
            log "WARN" "Certbot revoke failed, attempting delete only"
            certbot delete --cert-name "$domain" --non-interactive 2>/dev/null || true
        fi

        log "INFO" "Certificate revoked for $domain"
    else
        log "INFO" "No certificate found for $domain, skipping revocation"
    fi

    if [[ -n "$errors" ]]; then
        json_output "true" "Certificate revoked with warnings: $errors"
    else
        json_output "true" "Certificate revoked successfully"
    fi
    exit 0
}

# Acquire lock for concurrent operation protection
acquire_lock() {
    exec 200>"$LOCK_FILE"
    if ! flock -n 200; then
        json_output "false" "Another SSL operation is in progress. Please try again."
        exit 1
    fi
}

# Main entry point
main() {
    local command="$1"
    local domain="$2"

    # Commands that don't need locking
    case "$command" in
        verify-dns)
            verify_dns "$domain"
            ;;
        status)
            check_status "$domain"
            ;;
    esac

    # Commands that need locking
    acquire_lock

    case "$command" in
        provision)
            provision "$domain"
            ;;
        revoke)
            revoke "$domain"
            ;;
        *)
            json_output "false" "Unknown command: $command. Valid commands: provision, revoke, status, verify-dns"
            exit 1
            ;;
    esac
}

# Ensure we have required commands
check_dependencies() {
    local missing=""

    for cmd in certbot apachectl dig openssl; do
        if ! command -v "$cmd" &>/dev/null; then
            missing="$missing $cmd"
        fi
    done

    if [[ -n "$missing" ]]; then
        json_output "false" "Missing required commands:$missing"
        exit 1
    fi
}

# Validate arguments
if [[ $# -lt 1 ]]; then
    json_output "false" "Usage: $0 <command> [domain]"
    exit 1
fi

if [[ $# -lt 2 ]] && [[ "$1" != "help" ]]; then
    json_output "false" "Domain argument required"
    exit 1
fi

check_dependencies
main "$@"
