#!/bin/bash
# shellcheck source=/dev/null

CACHE_DIR="$HOME/.config/wifi-channel-watcher/"
# pull in user config (or create it, if it doesn't exist)
if [ ! "$1" == "uninstallservice" ] && [ ! -d "$CACHE_DIR" ];
then
  mkdir -p "$CACHE_DIR" && cp icon.svg "$CACHE_DIR" && cp "$PWD/config.conf" "$CACHE_DIR"config.conf && echo "Created $CACHE_DIR (first run)"
  sed -i -- "s|PROCESSED_DURING_SETUP|$CACHE_DIR|g" ~/.config/wifi-channel-watcher/config.conf && echo "Set cache directory"
  source "$CACHE_DIR"config.conf
elif [ -e "$CACHE_DIR"config.conf ];
then
  source "$CACHE_DIR"config.conf
fi

function debug() {
  if [ "$1" == "1" ];
  then
    STATE="INFO"
  elif [ "$1" == "2" ];
  then
    STATE="TEST"
  else
    STATE="WARN"
  fi
  echo "$STATE: $(date) $2" >> "$DEBUG_PATH"
}

# get the channel in use
[ "$DEBUG" == "1" ] && debug "1" "-- Start of debug for this run --"

INTERFACE="$(cat < /proc/net/wireless | awk '{print $1}' | tail -n1 | tr -d .:)"

[ "$DEBUG" == "1" ] && debug "0" "Interface check: $INTERFACE triggered."

# wifi active?
if [ "$(nmcli con show --active | awk '$3 ~ "wifi" || "*wireless"')" == "" ];
then
  echo "Wifi interface appears to be inactive or is not detected.  Using a Pi?"
  echo "For troubleshooting: https://github.com/angela-d/wifi-channel-watcher/wiki/Troubleshooting"
  exit 0
fi

# check if internal resources are accessible, general local cache
[[ ! $INTERNAL_CHECK == ""  && "$(ping -c 1 "$INTERNAL_CHECK")" ]] && INTERNAL_CONN="yes" || INTERNAL_CONN=""
[ "$DEBUG" == "1" ] && debug "1" "Internal connection value (empty = unmet): $INTERNAL_CONN"

# MAC address lookup vendor
function macsearch() {
  # look up macs without api keys from IEEE
  # cache this mac address, since it won't change (if the script runs multiple times, no need to make new calls)
  if [ ! -f "$CACHE_DIR""$1" ];
  then
    LOOKUP=$(echo "$1" | tr -d ':' | head -c 6)

    # cache the mac lookups, this will almost never change so don't litter network requests
    if [ -f "$CACHE_DIR"mac.cache ];
    then
      cat < "$CACHE_DIR"mac.cache | grep -i "$LOOKUP" | cut -d')' -f2 | tr -d '\t' > "$CACHE_DIR""$LOOKUP"
    else
      if [[ $INTERNAL_CONN == 'yes' ]];
      then
        # no cache exists, pull a copy for a local cache
        curl -L -sS -C - "$MAC_LIST" > "$CACHE_DIR"mac.cache && cat < "$CACHE_DIR"mac.cache | grep -i "$LOOKUP" | cut -d')' -f2 | tr -d '\t' > "$CACHE_DIR""$LOOKUP"
        sleep 4
      else
        # not on the internal network, obtain from the web and cache it
        curl -L -sS -C - "http://standards-oui.ieee.org/oui.txt" > "$CACHE_DIR"mac.cache && cat < "$CACHE_DIR"mac.cache | grep -i "$LOOKUP" | cut -d')' -f2 | tr -d '\t' > "$CACHE_DIR""$LOOKUP"
        sleep 4
      fi
    fi
    # if the mac doesn't match a manufacturer, it's probably spoofed
    [ -s "$CACHE_DIR""$LOOKUP" ] && cat "$CACHE_DIR""$LOOKUP" || echo "(spoofed)"
  fi
}

# for enterprise lookups, ideal for environments with multiple access points using the same ssid
# and you want to know specifically which ap you're connected to
function apsearch() {
  if [ "$INTERNAL_CONN" == "yes" ];
  then
    [ "$DEBUG" == "1" ] && debug "1" "AP List: $AP_LIST"
    AP=$(echo "$1" | tr -d ':')
    [ "$DEBUG" == "1" ] && debug "1" "Raw BSSID: $AP"
    AP=$(curl -sS "$AP_LIST" | grep -i "$AP" | head -n1 | awk '{ print $1 }')

    echo "$AP"
    [ "$DEBUG" == "1" ] && debug "1" "apsearch function reached. AP: $AP"
  fi
}

# get nearby channel usage
ME="$(/sbin/iwgetid -r)"
CHANNEL="$(/sbin/iw dev "$INTERFACE" info | grep channel | awk '{print $2}')"
# 2g or 5g? use for channel suggestions, if enabled
RADIO="$(echo $CHANNEL | awk '$1 ~ /^1$|^6$|^11$/' | sort | uniq -c | sort -n)"
[ "$DEBUG" == "1" ] && debug "1" "My SSID: $ME - Channel: $CHANNEL"

# iw scan needs elevation to scan, so nmcli is better to use, otherwise you have an equal amount of work to do to
# pass info to the userspace from root and/or specify sudo privs in visudo
#
# get a channel list into an array (needs work)
IFS=$'\n'
# shellcheck disable=SC2207
declare -a CHAN=($(nmcli -g chan dev wi list | sort | uniq -c))
[ "$DEBUG" == "1" ] && debug "2" "Unprocessed query: $(nmcli -g chan dev wi list | sort | uniq -c)"

# isolate the active usage
for ACTIVE_CHANNEL in "${CHAN[@]}";
do
  if [[ $ACTIVE_CHANNEL =~ $CHANNEL ]];
  then
    # isolate the total on the active channel
    TOTAL_ACTIVE=$(echo "$ACTIVE_CHANNEL" | awk '{print $1}')
    # it will always be +1 because of me, so subtract
    TOTAL_ACTIVE=$((TOTAL_ACTIVE-1))
    [ "$DEBUG" == "1" ] && debug "1" "On my channel (incl me) extract: $ACTIVE_CHANNEL"
    break
  fi
done

[ "$DEBUG" == "1" ] && debug "1" "Total on my channel (excl me): $TOTAL_ACTIVE"
# check for notify-send and send a notification to the user
if [ "$TOTAL_ACTIVE" -ge "$THRESHOLD" ] && [ -f /usr/bin/notify-send ];
then
  [ "$DEBUG" == "1" ] && debug "1" "/usr/bin/notify-send exists"
  # display the ssid, if enabled
  if [ "$DISPLAY_CHANNEL_BSSID" == "1" ];
  then
    [ "$DEBUG" == "1" ] && debug "1" "DISPLAY_CHANNEL_BSSID is true"
    # redundant loop; merge with CHAN at some point
    # shellcheck disable=SC2207
    declare -a DETAILED=($(nmcli -f ssid,bssid,chan,bars dev wi list | awk -v CHANNEL="$CHANNEL" -v ME="$ME" '$3 == CHANNEL' | tail -n 1))

    for NEIGHBOR in "${DETAILED[@]}"
    do
      SSID="$(printf "%s" "$NEIGHBOR" | awk '{ print $1 }')"
      # ssid is sanitized since it returns values to the screen created by untrusted individuals, because, you never know :)
      [ "$SSID" == "--" ] && SSID="[hidden]" || SSID="${SSID//[^a-zA-Z0-9\_-]/}"
      BSSID="$(printf "%s" "$NEIGHBOR" | awk '{ print $2 }')"
      [ "$DEBUG" == "1" ] && debug "1" "BSSID in neighbor loop: $BSSID"
      # get the router manufacturer
      BSSID_HUMAN_READABLE=$(macsearch "$BSSID")
      BARS="$(printf "%s" "$NEIGHBOR" | awk '{ print $4 }')"
      [[ "$INTERNAL_CONN" == "yes" ]] && AP_NAME=$(apsearch "$BSSID") || AP_NAME=""

      BSSID_DETAIL+="\\n$BARS $SSID - $BSSID_HUMAN_READABLE\\n$AP_NAME"
      [ "$DEBUG" == "1" ] && debug "1" "BSSID DETAIL: $BSSID_DETAIL"
    done
  else
    [ "$DEBUG" == "1" ] && debug "1" "WARN: DISPLAY_CHANNEL_BSSID is false, or notify-send not seen"
    BSSID_DETAIL=""
  fi

  # assess a lesser congested channel (vacant only)
  if [ "$SUGGEST_CHANNEL" == "1" ];
  then
    function arraydiff() {
      # thx fabian lee
      # https://fabianlee.org/2020/09/06/bash-difference-between-two-arrays/
      awk 'BEGIN{RS=ORS=" "}
           {NR==FNR?a[$0]++:a[$0]--}
           END{for(k in a)if(a[k])print k}' <(echo -n "${!1}") <(echo -n "${!2}")
    }

    IFS='|'
    # if radio is unset, user is on a 5g radio; channel selections in user config
    if [ ! "$RADIO" ];
    then
      # look for vacant 5GHz channels; ignoring dfs/radar + wider channels incompatible w/ some devices
      read -a VACANTS <<< "$CHANNELS_5G"
      read -a IGNORE <<< "$IGNORE_5G"
    else
      # 2GHz user
      read -a VACANTS <<< "$CHANNELS_2G"
      read -a IGNORE <<< "$IGNORE_2G"
    fi

    # splitter for the arraydiff
    IFS=' '
    # compare vacants arr w/ chan array to spot any unused channels
    # shellcheck disable=SC2207
    SUGGESTING=($(arraydiff CHAN[@] VACANTS[@]))
    [ "$DEBUG" == "1" ] && debug "1" "Channels that appear vacant: ${SUGGESTING[*]}"

    for ANALYZE in "${SUGGESTING[@]}";
    do
      [ "$DEBUG" == "1" ] && debug "1" "Channel suggestion (unfiltered/active channel): $ANALYZE"

      # 40 gets regex clipped due to the presence of dfs 140 in ignore, despite shellcheck claiming this is a literal match
      # shellcheck disable=SC2076
      if [[ ! ${IGNORE[*]} =~ "$ANALYZE" ]] || [[ "$ANALYZE" -eq 40  && ! "$RADIO" ]]
      then
        SUGGEST="$ANALYZE"
      fi
    done

    [ "$DEBUG" == "1" ] && debug "1" "Channel to suggest: $SUGGEST"
    IFS=$'\n'
    if [ -z "$SUGGEST" ] && [ "$RADIO" ];
      then
      # make a 2g suggestion based off the least-trafficked channel
      CONGESTED="$(nmcli -f chan dev wi list | awk '$1 ~ /^1$|^6$|^11$/' | sort | uniq -c | sort -n | head -n1)"
      SUGGEST="$(printf "%s" $CONGESTED | awk '{ print $2 }')"
      [ "$DEBUG" == "1" ] && debug "1" "INFO: Congested 2G chanels: $CONGESTED"
    elif [ -z "$SUGGEST" ] && [ ! "$RADIO" ];
    then
      # make a 5g suggestion based off the least-trafficked channel
      CONGESTED="$(nmcli -f chan dev wi list | awk '$1 ~ /^36$|^40$|^44$|^48$|^149$|^153$|^157$|^161$|^165$/' | sort | uniq -c | sort -n | head -n1)"
      SUGGEST="$(printf "%s" $CONGESTED | awk '{ print $2 }')"
    fi

    # if the suggested channel is the active channel, don't notify
    [ "$SUGGESTED" == "$CHANNEL" ] && HIDE_PROMPT="true" && [ "$DEBUG" == "1" ] && debug "1" "You are on the best channel: $SUGGESTED"
    SUGGESTED="\\nBetter channel: $SUGGEST"
  fi

  if [ -z "$HIDE_PROMPT" ];
  then
  # format the message to prevent notify-send from throwing errors
    PROMPT="$TOTAL_ACTIVE neighboring APs also on channel $CHANNEL\\n$BSSID_DETAIL $SUGGESTED"
    [ "$DEBUG" == "1" ] && debug "1" "Prompt output: $PROMPT"

    # send the notification
    notify-send -i "$HOME/.config/wifi-channel-watcher/icon.svg" "Wifi Channel Not Optimal" \
    -u critical \ "$PROMPT"
  fi
fi

[ "$DEBUG" == "1" ] && debug "1" "INFO: -- End of debug for this run --"

if [ "$1" == "installservice" ];
then
  # checking for systemd
  echo "Checking for systemd..."
  if [ -d /usr/lib/systemd ];
  then
    echo "Found.  Creating systemd service directory..."
    [ ! -d ~/.config/systemd/user/ ] && mkdir -p ~/.config/systemd/user/
    [ -f "$PWD/service/systemd/wifiwatcher.service" ] && cp "$PWD/service/systemd/wifiwatcher.service" ~/.config/systemd/user/wifiwatcher.service && echo "Unit file added."
    [ -f "$PWD/service/systemd/wifiwatcher.timer" ] && cp "$PWD/service/systemd/wifiwatcher.timer" ~/.config/systemd/user/wifiwatcher.timer && echo "Timer file added."
    echo "Modifying the path to this script (you'll need to modify the service file if you move the repo on your system)..."
    echo -e "\\t$PWD/channel-watch"
    sleep 3
    sed -i -- "s|SERVICE_PATH|$PWD/channel-watch|g" ~/.config/systemd/user/wifiwatcher.service
    echo "Enabling service..."
    systemctl --user enable wifiwatcher && echo "Enabling scheduler (default every 10min, see wiki for adjusting)"
    systemctl --user enable wifiwatcher.timer && echo "Starting services..."
    systemctl --user start wifiwatcher --now && systemctl --user start wifiwatcher.timer --now && echo "Service setup complete!"
  fi
elif [ "$1" == "uninstallservice" ];
then
  # checking for systemd
  echo "Checking for systemd..."
  if [ -d /usr/lib/systemd ];
  then
    echo "Found. Stopping services..."
    systemctl --user stop wifiwatcher.timer
    systemctl --user stop wifiwatcher
    systemctl --user disable wifiwatcher.timer && systemctl --user disable wifiwatcher && echo "Removed wifiwatcher from startup"
    echo "Removing service files for wifiwatcher..."
    [ -e ~/.config/systemd/user/wifiwatcher.service ]  && rm ~/.config/systemd/user/wifiwatcher.service  && echo "wifiwatcher service removed"
    [ -e ~/.config/systemd/user/wifiwatcher.timer ] && rm ~/.config/systemd/user/wifiwatcher.timer && echo "wifiwatcher timer removed"
    echo "Checking for cache directory..."
    [ -d ~/.config/wifi-channel-watcher ]  && rm -rf ~/.config/wifi-channel-watcher  && echo "Cache directory removed"
    echo "Uninstall complete!"
  fi
fi
