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

readonly WIFI_INTERFACE="${WIFI_INTERFACE:-wlan0}"
readonly FALLBACK_PROFILE="${FALLBACK_PROFILE:-aspen-iridium-hotspot}"
readonly FALLBACK_ADDRESS_PREFIX="${FALLBACK_ADDRESS_PREFIX:-10.42.0.1/}"
readonly HOTSPOT_SSID="${HOTSPOT_SSID:-Aspen-Iridium-Pi}"
readonly HOTSPOT_CHANNEL="${HOTSPOT_CHANNEL:-6}"
readonly HOTSPOT_COUNTRY="${HOTSPOT_COUNTRY:-US}"
readonly HOTSPOT_RUN_DIRECTORY="${HOTSPOT_RUN_DIRECTORY:-/run/aspen-hotspot}"
readonly HOSTAPD_CONFIG="${HOTSPOT_RUN_DIRECTORY}/hostapd.conf"
readonly HOSTAPD_PID_FILE="${HOTSPOT_RUN_DIRECTORY}/hostapd.pid"
readonly DNSMASQ_CONFIG="${HOTSPOT_RUN_DIRECTORY}/dnsmasq.conf"
readonly DNSMASQ_PID_FILE="${HOTSPOT_RUN_DIRECTORY}/dnsmasq.pid"
readonly STARTUP_GRACE_SECONDS="${STARTUP_GRACE_SECONDS:-60}"
readonly CONNECTION_LOSS_GRACE_SECONDS="${CONNECTION_LOSS_GRACE_SECONDS:-60}"
readonly POLL_SECONDS="${POLL_SECONDS:-10}"
readonly ACTIVATION_VERIFY_SECONDS="${ACTIVATION_VERIFY_SECONDS:-15}"

log() {
    printf '%s\n' "$*"
}

device_property() {
    local property="$1"
    nmcli --get-values "${property}" device show "${WIFI_INTERFACE}" 2>/dev/null |
        head -n 1
}

active_connection() {
    device_property GENERAL.CONNECTION
}

device_state_code() {
    device_property GENERAL.STATE |
        sed -E 's/^([0-9]+).*/\1/'
}

ipv4_address() {
    device_property IP4.ADDRESS
}

hotspot_is_active() {
    [[ -s "${HOSTAPD_PID_FILE}" ]] &&
        kill -0 "$(cat "${HOSTAPD_PID_FILE}")" 2>/dev/null &&
        [[ -s "${DNSMASQ_PID_FILE}" ]] &&
        kill -0 "$(cat "${DNSMASQ_PID_FILE}")" 2>/dev/null &&
        ip -o -4 address show dev "${WIFI_INTERFACE}" |
            grep -q " ${FALLBACK_ADDRESS_PREFIX}"
}

station_is_usable() {
    local connection
    local state
    local address

    connection="$(active_connection)"
    state="$(device_state_code)"
    address="$(ipv4_address)"

    [[ -n "${connection}" ]] &&
        [[ "${connection}" != "--" ]] &&
        [[ "${connection}" != "${FALLBACK_PROFILE}" ]] &&
        [[ "${state}" == "100" ]] &&
        [[ -n "${address}" ]] &&
        [[ "${address}" != "--" ]]
}

activate_fallback() {
    log "Starting fallback hotspot"

    if ! command -v hostapd >/dev/null || ! command -v dnsmasq >/dev/null; then
        log "hostapd and dnsmasq are required for fallback hotspot"
        return 1
    fi

    local hotspot_password
    hotspot_password="$(
        nmcli --show-secrets \
            --get-values 802-11-wireless-security.psk \
            connection show "${FALLBACK_PROFILE}" 2>/dev/null
    )"
    if (( ${#hotspot_password} < 8 || ${#hotspot_password} > 63 )); then
        log "Fallback hotspot password is missing or invalid"
        return 1
    fi

    install -d -m 0700 "${HOTSPOT_RUN_DIRECTORY}"

    umask 077
    {
        printf 'interface=%s\n' "${WIFI_INTERFACE}"
        printf 'driver=nl80211\n'
        printf 'ssid=%s\n' "${HOTSPOT_SSID}"
        printf 'country_code=%s\n' "${HOTSPOT_COUNTRY}"
        printf 'hw_mode=g\n'
        printf 'channel=%s\n' "${HOTSPOT_CHANNEL}"
        printf 'ieee80211d=1\n'
        printf 'wmm_enabled=1\n'
        printf 'auth_algs=1\n'
        printf 'ignore_broadcast_ssid=0\n'
        printf 'wpa=2\n'
        printf 'wpa_key_mgmt=WPA-PSK\n'
        printf 'rsn_pairwise=CCMP\n'
        printf 'ieee80211w=0\n'
        printf 'wpa_passphrase=%s\n' "${hotspot_password}"
    } > "${HOSTAPD_CONFIG}"
    unset hotspot_password

    {
        printf 'interface=%s\n' "${WIFI_INTERFACE}"
        printf 'bind-interfaces\n'
        printf 'dhcp-range=10.42.0.10,10.42.0.254,255.255.255.0,1h\n'
        printf 'dhcp-option=3,10.42.0.1\n'
        printf 'dhcp-option=6,10.42.0.1\n'
        printf 'no-resolv\n'
        printf 'domain-needed\n'
        printf 'bogus-priv\n'
    } > "${DNSMASQ_CONFIG}"

    # A saved station profile can remain named on wlan0 while NetworkManager
    # is stuck in a connecting state. Explicitly disconnect the device before
    # handing it to hostapd so station autoconnect cannot race AP activation.
    nmcli radio wifi on
    nmcli device disconnect "${WIFI_INTERFACE}" >/dev/null 2>&1 || true

    # This Pi Zero firmware fails client authentication when NetworkManager's
    # supplicant advertises mixed WPA key-management modes. Use hostapd for a
    # strict WPA2-PSK/CCMP AP, while retaining NetworkManager for station mode.
    systemctl stop NetworkManager.service

    ip link set "${WIFI_INTERFACE}" down
    ip address flush dev "${WIFI_INTERFACE}"
    ip link set "${WIFI_INTERFACE}" up
    ip address add 10.42.0.1/24 dev "${WIFI_INTERFACE}"

    rm -f "${HOSTAPD_PID_FILE}" "${DNSMASQ_PID_FILE}"
    if ! hostapd -B -P "${HOSTAPD_PID_FILE}" "${HOSTAPD_CONFIG}"; then
        log "Could not start hostapd; restoring NetworkManager"
        systemctl start NetworkManager.service
        return 1
    fi

    if ! dnsmasq \
        --conf-file="${DNSMASQ_CONFIG}" \
        --pid-file="${DNSMASQ_PID_FILE}"; then
        log "Could not start hotspot DHCP; restoring NetworkManager"
        kill "$(cat "${HOSTAPD_PID_FILE}")" 2>/dev/null || true
        systemctl start NetworkManager.service
        return 1
    fi

    local waited=0
    while (( waited < ACTIVATION_VERIFY_SECONDS )); do
        if hotspot_is_active; then
            log "Fallback hotspot is active at 10.42.0.1"
            return 0
        fi
        sleep 1
        waited=$((waited + 1))
    done

    log "Fallback profile activated but did not obtain 10.42.0.1; will retry"
    kill "$(cat "${DNSMASQ_PID_FILE}")" 2>/dev/null || true
    kill "$(cat "${HOSTAPD_PID_FILE}")" 2>/dev/null || true
    systemctl start NetworkManager.service
    return 1
}

log "Waiting ${STARTUP_GRACE_SECONDS}s for a saved Wi-Fi network"
sleep "${STARTUP_GRACE_SECONDS}"

disconnected_since=0
last_status=""

while true; do
    if hotspot_is_active; then
        disconnected_since=0
        if [[ "${last_status}" != "hotspot" ]]; then
            log "Fallback hotspot remains active at 10.42.0.1"
            last_status="hotspot"
        fi
    elif station_is_usable; then
        disconnected_since=0
        connection="$(active_connection)"
        address="$(ipv4_address)"
        status="station:${connection}:${address}"
        if [[ "${status}" != "${last_status}" ]]; then
            log "Usable Wi-Fi connection: ${connection} at ${address}"
            last_status="${status}"
        fi
    else
        now="$(date +%s)"
        connection="$(active_connection)"
        state="$(device_state_code)"
        address="$(ipv4_address)"

        if (( disconnected_since == 0 )); then
            disconnected_since="${now}"
            last_status="waiting"
            log "No usable Wi-Fi (connection=${connection:---}, state=${state:---}, address=${address:---}); allowing ${CONNECTION_LOSS_GRACE_SECONDS}s for recovery"
        elif (( now - disconnected_since >= CONNECTION_LOSS_GRACE_SECONDS )); then
            if activate_fallback; then
                disconnected_since=0
                last_status="hotspot"
            else
                disconnected_since="${now}"
                last_status="waiting"
            fi
        fi
    fi

    sleep "${POLL_SECONDS}"
done
