#!/usr/bin/env bash

set -u  # Exit on undefined variable
set -o pipefail  # Exit on pipe failures

DEBUG=0
DRY_RUN=0
CURRENT_VERSION="v1.1"
FORGEJO_API_URL=${FORGEJO_API_URL:-"https://forge.dmz.skyfritt.net"}
FORGEJO_ORG=${FORGEJO_ORG:-"Skyfritt"}
FORGEJO_REPO=${FORGEJO_REPO:-"dnsdrone"}
AUTO_UPDATE=${AUTO_UPDATE:-0}
CHECK_UPDATE=${CHECK_UPDATE:-0}

RECORD_A=${RECORD_A:-false}
RECORD_AAAA=${RECORD_AAAA:-false}
RECORD_PTR4=${RECORD_PTR4:-false}
RECORD_PTR6=${RECORD_PTR6:-false}
ISP_PREFIX_LENGHT="${ISP_PREFIX_LENGHT:-false}"
API_VERSION=${API_VERSION:-2.253}
IPCALC_CMD=${IPCALC_CMD:-false}
RECORD_NAME=${RECORD_NAME:-false}

error() {
    echo "[ERROR] $1" >&2
    exit 1
}

debug() {
    [[ $DEBUG -eq 1 ]] && echo "[DEBUG] $1"
}

check_dependencies() {
    local missing=0
    for cmd in curl jq dig nsupdate; do
        if ! command -v "$cmd" >/dev/null 2>&1; then
            echo "Error: $cmd is required but not installed." >&2
            missing=1
        fi
    done

    if command -v ipcalc-ng >/dev/null 2>&1; then
        debug "Using ipcalc-ng"
        IPCALC_CMD="ipcalc-ng"
    elif command -v ipcalc >/dev/null 2>&1; then
        debug "Using ipcalc"
        IPCALC_CMD="ipcalc"
    else
        echo "Error: either ipcalc or ipcalc-ng is required but not installed." >&2
        missing=1
    fi

    [[ $missing -eq 1 ]] && error "Missing required dependencies"
}

load_env() {
    local linkName realName envFile=""
    linkName="$(basename "$0")"
    realName="$(basename "$(realpath "$0")")"
    local -r skyfrittEnvPath="/opt/skyfritt-tools-env"
    local -a searchPaths=(
        "$(pwd)/.env"
        "$(dirname "$(realpath "$0")")/.env"
        "$(dirname "$(realpath "$0")")/.env.${realName}"
        "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")/.env.${linkName}"
        "${skyfrittEnvPath}/.env.${linkName}"
    )

    debug "Checking for .env file in multiple locations"

    for path in "${searchPaths[@]}"; do
        if [[ -f "$path" ]]; then
            envFile="$path"
            debug "Found .env file: ${envFile}"
            break
        fi
    done

    [[ -z "$envFile" ]] && error "No .env file found in search paths"
    [[ ! -r "$envFile" ]] && error "Found .env file at ${envFile} but it's not readable"

    # shellcheck source=/dev/null
    if ! source "$envFile"; then
        error "Failed to source .env file: ${envFile}"
    fi

    # Validate required environment variables
    local -a required_vars=("SERVER" "ZONE_NAME")
    local missing_vars=()

    for var in "${required_vars[@]}"; do
        if [[ -z "${!var:-}" ]]; then
            missing_vars+=("$var")
        fi
    done

    # Check for missing required variables
    if [[ ${#missing_vars[@]} -gt 0 ]]; then
        error "Missing required variables in .env file: ${missing_vars[*]}"
    fi

    # Validate authentication method
    if [[ -z "${USER:-}" || -z "${PASSWORD:-}" ]] && [[ -z "${TSIG_KEYNAME:-}" || -z "${TSIG_KEY:-}" ]]; then
        error "Either USER/PASSWORD or TSIG_KEYNAME/TSIG_KEY must be set in .env file"
    fi

    # Validate values if present
    [[ -n "${RECORD_TTL:-}" ]] && [[ ! "${RECORD_TTL}" =~ ^[0-9]+$ ]] && \
        error "RECORD_TTL must be a positive integer"

    # Additional validation for specific record types
    if [[ "${RECORD_TXT_PREFIX:-false}" == "true" ]]; then
        [[ -z "${ISP_PREFIX_LENGHT:-}" ]] && \
            error "ISP_PREFIX_LENGHT must be set when RECORD_TXT_PREFIX is true"
        [[ ! "${ISP_PREFIX_LENGHT}" =~ ^[0-9]+$ ]] && \
            error "ISP_PREFIX_LENGHT must be a positive integer"
    fi

    debug "Environment loaded successfully"
}

# Function to get current IPs
get_current_ips() {
    local ipv4="" ipv6=""
    local ipv4_providers=(
        "ifconfig.me"
        "api.ipify.org"
        "icanhazip.com"
        "ipecho.net/plain"
        "checkip.amazonaws.com"
    )
    local ipv6_providers=(
        "ifconfig.me"
        "api6.ipify.org"
        "icanhazip.com"
        "v6.ident.me"
    )

    for provider in "${ipv4_providers[@]}"; do
        local temp_ipv4
        temp_ipv4=$(curl -s4 --connect-timeout 5 "$provider" 2>/dev/null)
        if [[ -n "$temp_ipv4" ]]; then
            ipv4=$temp_ipv4
            break
        fi
    done

    for provider in "${ipv6_providers[@]}"; do
        local temp_ipv6
        temp_ipv6=$(curl -s6 --connect-timeout 5 "$provider" 2>/dev/null)
        if [[ -n "$temp_ipv6" ]]; then
            ipv6=$temp_ipv6
            break
        fi
    done

    if [[ -z "$ipv4" && -z "$ipv6" ]]; then
        error "Failed to get any IP addresses from providers"
    fi

    echo "$ipv4:$ipv6"
}

get_dns_records() {
    local record_type=$1
    local result
    result=$(dig +short "$record_type" "$FQDN" "@${SERVER}" 2>/dev/null || true)
    echo "$result"
}

get_ptr_record() {
    local ip=$1
    local result
    result=$(dig +short -x "$ip" "@${SERVER}" 2>/dev/null || true)
    echo "$result"
}

update_freeipa_record() {
    local record_type=$1
    local ip=$2

    debug "Updating FreeIPA $record_type record to $ip"
    debug "Using server: ${SERVER}"
    debug "Zone: ${ZONE_NAME}"
    debug "Record: ${RECORD_NAME}"
    debug "API Version: ${API_VERSION}"

    if [[ $DRY_RUN -eq 1 ]]; then
        echo "[DRY RUN] Would update FreeIPA $record_type record for ${FQDN} to $ip"
        return
    fi

    [[ -z "$record_type" ]] && error "Record type cannot be empty"
    [[ -z "$ip" ]] && error "IP address cannot be empty"

    local data
    local curl_exit_code
    local curl_output
    local cookie_file
    local record_name
    cookie_file=$(mktemp)

    if [[ $RECORD_NAME == false ]]; then
        record_name="@"  # Empty string for root record
    else
        record_name="$RECORD_NAME"
    fi

    # First authenticate and get cookie
    curl_output=$(curl -s -k \
        -H "Accept: text/plain" \
        -H "Content-Type: application/x-www-form-urlencoded" \
        -H "Referer: https://${SERVER}/ipa" \
        -H "X-IPA-API-Version: ${API_VERSION}" \
        --data-urlencode "user=${USER}" \
        --data-urlencode "password=${PASSWORD}" \
        -c "${cookie_file}" \
        --write-out "\n%{http_code}" \
        "https://${SERVER}/ipa/session/login_password" 2>&1)
    curl_exit_code=$?

    # Parse HTTP status code from login
    local login_code
    login_code=$(echo "$curl_output" | tail -n1)

    # Check login success
    if [[ $curl_exit_code -ne 0 || $login_code -ne 200 ]]; then
        rm -f "${cookie_file}"
        error "Login failed with status $login_code: $(echo "$curl_output" | sed '$d')"
    fi

    # If record type is TXT, wrap the IP in escaped quotes
    if [[ "${record_type}" == "TXT" ]]; then
        ip="\\\"${ip}\\\""
    fi

    # Make the API call with the cookie
    curl_output=$(curl -s -k \
        -H "Accept: application/json" \
        -H "Content-Type: application/json" \
        -H "Referer: https://${SERVER}/ipa" \
        -H "X-IPA-API-Version: ${API_VERSION}" \
        -b "${cookie_file}" \
        --write-out "\n%{http_code}" \
        --data "{\"method\":\"dnsrecord_mod\",\"params\":[[\"${ZONE_NAME}\",\"${record_name}\"],{\"${record_type,,}record\":\"${ip}\", \"dnsttl\":${RECORD_TTL:-60}}]}" \
        "https://${SERVER}/ipa/session/json" 2>&1)
    curl_exit_code=$?

    rm -f "${cookie_file}"

    # Parse HTTP status code from last line
    local http_code
    http_code=$(echo "$curl_output" | tail -n1)
    data=$(echo "$curl_output" | sed '$d')

    debug "Curl exit code: $curl_exit_code"
    debug "HTTP status code: $http_code"

    if [[ $curl_exit_code -ne 0 ]]; then
        error "Curl command failed with exit code $curl_exit_code: $data"
    fi

    if [[ $http_code -ne 200 ]]; then
        error "HTTP request failed with status $http_code: $data"
    fi

    # Check for error in JSON response
    if echo "$data" | jq -e '.error' >/dev/null 2>&1; then
        local error_msg
        error_msg=$(echo "$data" | jq -r '.error.message // "Unknown error"')
        error "FreeIPA API returned error: $error_msg"
    fi

    debug "FreeIPA response: $data"
    debug "DNS record update completed successfully"
}

update_nsupdate_record() {
    local record_type=$1
    local ip=$2
    local ttl=${RECORD_TTL:-60}  # Default TTL of 60 seconds if not set in .env

    debug "Updating nsupdate $record_type record to $ip with TTL $ttl"

    if [[ $DRY_RUN -eq 1 ]]; then
        echo "[DRY RUN] Would update nsupdate $record_type record for ${FQDN} to $ip with TTL $ttl"
        return
    fi

    # Create nsupdate commands based on record type
    nsupdate -y "${TSIG_KEY_TYPE}:${TSIG_KEYNAME}:${TSIG_KEY}" <<EOF || error "nsupdate failed"
server ${SERVER}
zone ${ZONE_NAME}.
$(if [[ "$record_type" == "A" && "${RECORD_A:-false}" == "true" ]]; then
    echo "update delete ${FQDN} A"
    echo "update add ${FQDN} ${ttl} A ${ip}"
fi)
$(if [[ "$record_type" == "AAAA" && "${RECORD_AAAA:-false}" == "true" ]]; then
    echo "update delete ${FQDN} AAAA"
    echo "update add ${FQDN} ${ttl} AAAA ${ip}"
fi)
$(if [[ "$record_type" == "TXT" && "${RECORD_TXT_PREFIX:-false}" == "true" ]]; then
    echo "update delete ${FQDN} TXT"
    echo "update add ${FQDN} ${ttl} TXT ${ip}"
fi)
send
EOF
}

update_record() {
    local record_type=$1
    local current_ip=$2
    local dns_ip

    dns_ip=$(get_dns_records "$record_type")
    debug "Checking $record_type record - Current DNS: '$dns_ip', New record: '$current_ip'"

    if [[ "$dns_ip" != "$current_ip" ]]; then
        debug "Updating $record_type record from '$dns_ip' to '$current_ip'"
        if [[ -n "${USER:-}" && -n "${PASSWORD:-}" ]]; then
            update_freeipa_record "$record_type" "$current_ip"
        elif [[ -n "${TSIG_KEYNAME:-}" && -n "${TSIG_KEY:-}" ]]; then
            update_nsupdate_record "$record_type" "$current_ip"
        else
            error "No valid authentication method available"
        fi
    else
        debug "No update needed for $record_type record"
    fi
}

version_gt() {
    test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
}

check_for_updates() {
    local latest_version
    local api_endpoint="${FORGEJO_API_URL}/api/v1/repos/${FORGEJO_ORG}/${FORGEJO_REPO}/releases/latest"

    debug "Checking for updates from: $api_endpoint"

    latest_version=$(curl -s "$api_endpoint" | jq -r '.tag_name' 2>/dev/null)

    if [[ -z "$latest_version" ]]; then
        error "Failed to fetch latest version information"
    fi

    debug "Current version: $CURRENT_VERSION"
    debug "Latest version: $latest_version"

    if version_gt "$latest_version" "$CURRENT_VERSION"; then
        echo "Update available: $CURRENT_VERSION →  $latest_version"
        return 0
    else
        echo "You are running the latest version ($CURRENT_VERSION)"
        return 1
    fi
}

perform_update() {
    local latest_version
    local api_endpoint="${FORGEJO_API_URL}/api/v1/repos/${FORGEJO_ORG}/${FORGEJO_REPO}/releases/latest"
    local raw_download_url
    local current_script_path
    local temp_file

    latest_version=$(curl -s "$api_endpoint" | jq -r '.tag_name' 2>/dev/null)
    if [[ -z "$latest_version" ]]; then
        error "Failed to get latest version information. Is the repo public?"
    fi

    raw_download_url="${FORGEJO_API_URL}/${FORGEJO_ORG}/${FORGEJO_REPO}/raw/tag/${latest_version}/dnsdrone.sh"

    debug "Downloading from: $raw_download_url"

    temp_file=$(mktemp)
    current_script_path=$(realpath "$0")

    echo "Downloading update..."
    if ! curl -s -L "$raw_download_url" -o "$temp_file"; then
        rm -f "$temp_file"
        error "Failed to download update"
    fi

    if ! bash -n "$temp_file"; then
        rm -f "$temp_file"
        error "Downloaded file appears to be invalid"
    fi

    cp "$current_script_path" "${current_script_path}.backup"

    if ! mv "$temp_file" "$current_script_path"; then
        cp "${current_script_path}.backup" "$current_script_path"
        rm -f "$temp_file"
        error "Failed to install update"
    fi

    chmod +x "$current_script_path"

    echo "Successfully updated to version $latest_version"
    echo "Previous version backed up to ${current_script_path}.backup"
    exit 0
}

main() {
    while getopts "dnac" opt; do
        case $opt in
            d) DEBUG=1 ;;
            n) DRY_RUN=1 ;;
            a) AUTO_UPDATE=1 ;;
            c) CHECK_UPDATE=1 ;;
            *) error "Invalid option: -$OPTARG" ;;
        esac
    done

    if [[ $CHECK_UPDATE -eq 1 ]]; then
        check_for_updates
        exit $?
    fi

    if [[ $AUTO_UPDATE -eq 1 ]]; then
        if check_for_updates; then
            perform_update
        fi
    fi

    trap 'error "Script interrupted"' INT TERM

    check_dependencies
    load_env

    if [[ $RECORD_NAME = false ]]; then
        local FQDN="${ZONE_NAME}."
    else
        local FQDN="${RECORD_NAME}.${ZONE_NAME}."
    fi

    IFS=':' read -r current_ipv4 current_ipv6 <<< "$(get_current_ips)" || true

    if [[ -n "$current_ipv4" && "${RECORD_A:-false}" == "true" ]]; then
        update_record "A" "$current_ipv4"
    elif [[ "${RECORD_A:-false}" == "true" ]]; then
        debug "Skipping A record update - no IPv4 address available"
    fi

    if [[ -n "$current_ipv6" && "${RECORD_AAAA:-false}" == "true" ]]; then
        update_record "AAAA" "$current_ipv6"
    elif [[ "${RECORD_AAAA:-false}" == "true" ]]; then
        debug "Skipping AAAA record update - no IPv6 address available"
    fi

    if [[ "${RECORD_PTR4:-false}" == "true" ]]; then
        if [[ -n "$current_ipv4" ]]; then
            debug "Processing PTR record for IPv4"
            local expected_ptr="${RECORD_NAME}.${ZONE_NAME}."
            local current_ptr4
            current_ptr4=$(get_ptr_record "$current_ipv4")
            debug "Current PTR4 record: $current_ptr4"
            if [[ "$current_ptr4" != "$expected_ptr" ]]; then
                update_record "PTR" "$expected_ptr"
            fi
        else
            debug "Skipping PTR4 record update - no IPv4 address available"
        fi
    fi

    if [[ "${RECORD_PTR6:-false}" == "true" ]]; then
        if [[ -n "$current_ipv6" ]]; then
            debug "Processing PTR record for IPv6"
            local expected_ptr="${RECORD_NAME}.${ZONE_NAME}."
            local current_ptr6
            current_ptr6=$(get_ptr_record "$current_ipv6")
            debug "Current PTR6 record: $current_ptr6"
            if [[ "$current_ptr6" != "$expected_ptr" ]]; then
                update_record "PTR" "$expected_ptr"
            fi
        else
            debug "Skipping PTR6 record update - no IPv6 address available"
        fi
    fi

    if [[ "${RECORD_TXT_PREFIX:-false}" == true ]]; then
        if [[ -n "$current_ipv6" ]]; then
            local expected_network_address expected_network_address_without_prefix current_network_address current_network_address_quotes
            current_network_address_quotes=$(get_dns_records "TXT")
            current_network_address=${current_network_address_quotes//\"/}
            expected_network_address_without_prefix="$($IPCALC_CMD "$current_ipv6"/"$ISP_PREFIX_LENGHT" --network --no-decorate)"
            expected_network_address="${expected_network_address_without_prefix}/${ISP_PREFIX_LENGHT}"

            debug "Current network address:  ${current_network_address}"
            debug "Expected network address: ${expected_network_address}"

            if [[ "$current_network_address" != "$expected_network_address" ]]; then
                debug "Current network address differs from the expected. Updating."
                update_record "TXT" "$expected_network_address"
            else
                debug "Current network address is the same as expected. Doing nothing."
            fi
        else
            debug "Skipping TXT record update - no IPv6 address available"
        fi
    fi
}
main "$@"