# -*- mode: sh -*-
# shellcheck shell=bash

function _help-flag {
    [[ " ${*} " == *" --help "* ]] || [[ " ${*} " == *" -h "* ]]
}

function join-string {
    local IFS="${1}"
    echo "${*:2}"
}

function hex2dec {
    printf "%d\n" "0x${1}"
}

function dec2hex {
    printf "%x\n" "${1}"
}

function header {
    local header

    header="${*}"

    echo
    echo "${*}"
    printf '=%.0s' $(seq 1 "${#header}")
    echo
}

function system-uuid {
    sudo cat /sys/class/dmi/id/product_uuid
}

function anykey {
    read -rp "Press any key to continue …"
}

function loadenv {
    local pat out file ret
    local -a files

    files=( "${@}" )

    if ! stat -c '' "${files[@]}" 2> >( grep -oE "'.+'.+" >&2 ) >/dev/null; then
        return 1
    fi

    # NOTE: This will never be truly safe.
    pat='^\s*(|#.*|[A-Za-z_][A-Za-z0-9_]+=((?!\$\()[^`])*)$'
    if out="$(grep -vP "${pat}" "${files[@]}")"; then
        echo "loadenv: Some files has suspicious content:"
        echo "${out}" | pr -to 2
        return 2
    fi

    set -a
    for file in "${files[@]}"; do
        # shellcheck disable=SC1090
        if ! source "${file}"; then
            ret=3
        fi
    done
    set +a

    return "${ret:-0}"
}

################################################################################
## Utils

function which {
    # shellcheck disable=SC2230
    (alias; declare -f) | /usr/bin/which --tty-only                            \
                                         --read-alias                          \
                                         --read-functions                      \
                                         --show-tilde                          \
                                         --show-dot "${@}"
}

function paths {
    echo -e "${PATH//:/\\n}"
}

function manpaths {
    local path

    path="$(manpath 2>/dev/null)"

    echo -e "${path//:/\\n}"
}

function uv-manpaths {
    find ~/.local/share/uv/tools -maxdepth 3 -type d -name man
}

function search {
    find . -name "${@}" |& grep -v ': Permission denied'
}

function calc {
    local args

    args="${*}"

    echo $((args))
}

function makepasswd-alphanum {
    local alphanum

    alphanum="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

    makepasswd -c "${alphanum}" "${@}"
}

function cmd-which {
    local cmd
    local -a args

    args=( "${@}" )

    case ${#args[@]} in
        0)
            echo "Usage: cmd-which <COMMAND> [FLAGS...]"
            return 2
            ;;
        1)
            echo "Usage: ${1}-which [FLAGS...] <COMMAND>"
            return 2
            ;;
        *)  ;;
    esac

    cmd="$(type -P "${args[-1]}")"
    unset 'args[${#args[@]}-1]'

    if [ ! -x "${cmd}" ]; then
        echo "No executable named <${cmd}> found!" >&2
        return 2
    fi

    eval "${args[*]} $(command -v "${cmd}")"
}

function fwatch {
    local secs out header align

    if [ "${#}" -lt 2 ] || _help-flag; then
        echo "Usage: ${FUNCNAME[0]} <SLEEP> <CMD>..."
        echo
        echo "A watch that works for functions!"
        return 2
    fi

    case "${1}" in
        -n) secs="${2}"; shift 2 ;;
        *)  secs="2"             ;;
    esac

    header="Every ${secs}s: ${*}"
    align=$(($(/usr/bin/tput cols) - ${#header}))

    while true; do
        out="${header}$(printf "%${align}s\n" "$(hostname): $(date)")\n\n"
        out+="$("${@}")"
        echo -e "\033[2J\033[H${out}"
        sleep "${secs}" || return 1
    done
}
alias poor-watch=fwatch

_completion_loader watch
complete -F _comp_cmd_watch fwatch
complete -F _comp_cmd_watch poor-watch

function iso8601-date {
    date --iso-8601=days
}

function iso8601-time {
    date --iso-8601=seconds
}

function epoch-time {
    date +'%s'
}

function fill {
    fmt -sw 80 | expand -i
}

################################################################################
## Misc

function lpass {
    local cmd

    if ! command -v lpass > /dev/null; then
        echo "The lpass CLI is not installed!"
        return 1
    fi

    for arg in "${@}"; do
        case "${arg}" in
            # My own aliases
            logged-in|copy-*)                                cmd="${arg}"     ;;

            # Upstream commands
            add|duplicate|edit|export|generate|import|login) cmd="${arg}"     ;;
            logout|ls|mv|passwd|rm|share|status|show|sync)   cmd="${arg}"     ;;

            # Flags that should immediately interrupt
            -v|--version|--help|-h) command lpass "${@}"; return              ;;
            *)  ;;
        esac
    done

    if [ -z "${cmd-}" ]; then
        echo "Unknown command '${cmd}'"
        return 2
    fi

    case "${cmd-}" in
        logged-in) command lpass "${@}"    ; return                           ;;
        status)    command lpass "${@}"    ; return                           ;;
        logout)    command lpass "${@}" -f ; return                           ;;
        *)                                                                    ;;
    esac

    if ! command lpass logged-in; then
        command lpass login mattias.jc.bengtsson@gmail.com
    fi

    case "${cmd-}" in
        login) return                                                         ;;
        *)     command lpass "${@}"                                           ;;
    esac
}

function remake {
    if [ $# -lt 1 ] || _help-flag; then
        echo "Usage: remake <file>..."
        return 2
    fi
    touch --date=@1 "${@}" && make "${@}"
}

function beamer {
    local document docname

    document="${1}"

    if [ ! "${document##*.}" = "md" ]; then
        echo "Usage: ${FUNCNAME[0]} <MARDKOWN>"
        return 2
    fi

    docname="${document%.*}"

    pandoc -t beamer "${docname}.md" -o "${docname}.pdf"
}

function jhparm {
    local disk

    if [ $# -lt 1 ]; then
        echo "Usage: ${0} <DISK>" >&2
        return 2
    fi
    disk="${1}"

    sudo dd                                                                    \
         if="${disk}"                                                          \
         of=/dev/null                                                          \
         bs=50M                                                                \
         count=10000                                                           \
         status=progress                                                       \
         iflag=direct
}

function reuse {
    # Filter out warnings about not being able to write to /etc/apt/apt.conf.d/
    command reuse "${@}" 2> >(grep -vE '(debian_support.py|apt_pkg.init)')
}

################################################################################
## File management

function mkcd {
    mkdir -p "${@}" && cd "${!#}" || return $?
}

function cpcd {
    mkdir -p "${!#}" && cp -r "${@}" && cd "${!#}" || return $?
}

function mvcd {
    mkdir -p "${!#}" && mv "${@}" && cd "${!#}" || return $?
}

_completion_loader _longopt
complete -d mkcd
complete -F _longopt cpcd
complete -F _longopt mvcd

################################################################################
## Tarballs

function unpack {
    local opts tarball usage opt_delete

    usage="usage: unpack [-d] <TARBALL...>"

    opts="$(getopt -o h,d --long help,delete -n unpack -- "${@}")" || return 2
    eval set -- "${opts}"

    opt_delete=false

    while true; do
        case "${1}" in
            -h | --help)   usage
                           return
                           ;;
            -d | --delete) opt_delete=true
                           shift
                           ;;
            -- ) shift; break ;;
            * ) break ;;
        esac
    done

    if [ "${#}" -eq 0 ]; then
        echo -e "${usage}" >&2
        return 2
    fi

    for tarball in "${@}"; do
        if [ ! -f "${tarball}" ]; then
            echo -e "ERROR: ${tarball} is not a file!\n\n${usage}" >&2
            return 3
        fi
    done

    for tarball in "${@}"; do
        echo "Extracting ${tarball} ..."
        tar axf "${tarball}"
        if [ "${opt_delete}" = true ]; then
            rm "${tarball}"
        fi
    done
}

function pack {
    local tarball files usage

    usage="usage: pack <DIR>\n   or: pack <TARBALL> <FILES...>"

    if _help-flag "${@}"; then
        echo -e "${usage}"
        return
    fi

    case "${#}" in
        0)  echo -e "${usage}" >&2
            return 2
            ;;
        1)  if [ ! -d "${1}" ]; then
                echo -e "ERROR: ${1} is not a directory!\n\n${usage}" >&2
                return 3
            fi
            files="${1}"
            tarball="${files}.tar.zst"
            ;;
        *)  tarball="${1}"
            files="${2}"
            ;;
    esac

    tar acf "${tarball}" "${files}"
}

################################################################################
## Login tracking stuff (move to separate script

function logins {
    local since until

    since="${1:-today}"
    until="${2:-$(date '+%Y-%m-%d' -d "${since}+1 days")}"

    journalctl --since "${since}" --until "${until}" -t lockmon --no-pager     \
        | cut -d ' ' -f '1-3 6-'                                               \
        | uniq
}

function logins2 {
    local since verbose line time ptime msg

    verbose=false

    if [[ "${1}" = *"--verbose" ]] || [[ "${1}" = *"-v" ]]; then
        verbose=true
        shift
    fi

    if [[ "${2}" = *"--verbose" ]] || [[ "${2}" = *"-v" ]]; then
        verbose=true
    fi

    since="${1}"

    logins="$(logins "${since}")"

    case "${logins}" in
        *"No entries"*)
            echo "No entries!"
            return
            ;;
        *)  ;;
    esac

    while read -r line; do
        ptime="${time}"
        time="$(cut -d ' ' -f 3 <<< "${line}" | cut -d ':' -f -2)"
        msg="$(cut -d ' ' -f '4-' <<< "${line}")"
        case "${msg}" in
            "Monitoring Terminated.")
                test -n "${ptime}" && echo "${time}"
                echo -n "Shutdown: ${time} – "
                continue
                ;;
            "Screen Locked")
                test -n "${ptime}" && echo "${time}"
                echo -n "  Locked: ${time} – "
                continue
                ;;
            "Monitoring Started." | "Screen Unlocked")
                if [ -z "${ptime}" ]; then
                    if [ "${msg}" = "Monitoring Started." ]; then
                        echo -n "Shutdown:       – "
                    else
                        echo -n "  Locked:       – "
                    fi
                fi

                echo "${time}"
                echo -n "Unlocked: ${time} – "
                continue
                ;;
            *)
                echo "Got unexpected log message:" >&2
                echo "   ${msg}" >&2
                echo
                echo "See the full logs:"
                logins "${since}"
                return 1
        esac
    done <<< "$(grep -v -- '--' <<< "${logins}")"

    case "${msg}" in
        "Monitoring Started." | "Screen Unlocked")
            date +"%H:%M"
            ;;
        *)
            echo
            ;;
    esac

    if [ "${verbose}" = true ]; then
        echo ---------------------
        logins "${since}"
    fi
}

################################################################################
## Data formats JSON, YAML, CSV etc

function ypterm {
    yaml2json < "${1:-/dev/stdin}" | jpterm | json2yaml
}

function yjq {
    yaml2json -i /dev/stdin | jq "${@}" | json2yaml
}

function yjqp {
    yaml2json < "${1:-/dev/stdin}" | jqp "${@}"
}

function csv2tsv {
    csvformat -T "${1}" > "${2:-/dev/stdout}"
}

function tsv2csv {
    csvformat -t "${1}" > "${2:-/dev/stdout}"
}

################################################################################
## Projects

function pcd {
    if [ ! $# -eq 1 ] || _help-flag; then
        echo "Usage: ${FUNCNAME[0]} <DIR>"
        return 2
    fi

    # shellcheck disable=SC2164
    cd "${HOME}/Code/github.com/mattiasb/${1}"
}

function ccd {
    local project

    if project="${1:-$(project choose)}" && [ -n "${project}" ]; then
        # shellcheck disable=SC2164
        cd "${project}"
    fi
}

function _comp-project-personal {
    local base="${HOME}/Code/github.com/mattiasb"

    mapfile -t COMPREPLY < <(project complete "${base}" "${2:-}"); wait $!
}

function _comp-project {
    mapfile -t COMPREPLY < <(project complete "${PWD}" "${2:-}"); wait $!
}

complete -F _comp-project ccd
complete -F _comp-project-personal pcd

################################################################################
## Test code

function test-create {
    if [ ! $# -eq 1 ] || _help-flag; then
        echo "Usage: test-create <NAME>"
        return 2
    fi

    mkcd "${HOME}/Code/Test/${1}"
}

function test-cd {
    if [ ! $# -eq 1 ] || _help-flag; then
        echo "Usage: test-cd <NAME>"
        return 2
    fi

    cd "${HOME}/Code/Test/${1}" || return
}

function _complete-test-folder {
    local tests=("${HOME}/Code/Test/${2}"*)
    [[ -e ${tests[0]} ]] && COMPREPLY=( "${tests[@]##*/}" )
}
complete -F _complete-test-folder test-create
complete -F _complete-test-folder test-cd

################################################################################
## Git

function gcd {
    cd "$(git rev-parse --show-toplevel)/${*}" || exit $?
}

function rcd {
    cd "$(repo --show-toplevel 2>/dev/null)/${*}" || exit $?
}

function gg {
    if [ $# -ne 1 ] || _help-flag; then
        echo "Usage: gg <url>"
        return 2
    fi
    git get "${1}" && cd "$(git get --show-path "${1}")" || return $?

    project index
}

function magit {
    local path

    if _help-flag; then
        echo "Usage: magit [<PATH>]"
        return
    fi

    path="${1:-.}"

    pushd "${path}" >/dev/null || return $?

    git rev-parse --is-inside-work-tree >/dev/null || return $?

    /bin/emacs --init-directory=~/.config/emacs                                \
               -nw                                                             \
               --eval '(progn (magit-status) (delete-other-windows))'

    popd >/dev/null || return 3
}

complete -d magit

################################################################################
## SSH

function ssh-regenerate-hostkey {
    local host exitcode

    exitcode=0

    if [ "${#}" -lt 1 ] || _help-flag "${@}"; then
        echo "Usage: ${FUNCNAME[0]} <server>..."
        return 2
    fi

    for host in "${@}"; do
        if ! key="$(ssh-keyscan "${host}" 2> >(grep -v '^#' >&2))"; then
            echo "${host}: Couldn't fetch host key from server" >&2
            exitcode=2
        fi
        ssh-remove-hostkey "${host}"
        echo "${key}" >> ~/.ssh/known_hosts
    done

    return "${exitcode}"
}
alias ssh-add-hostkey='ssh-regenerate-hostkey'

function ssh-remove-hostkey {
    local host ips exitcode

    exitcode=0

    if [ "${#}" -lt 1 ] || _help-flag "${@}"; then
        echo "Usage: ${FUNCNAME[0]} <server>..."
        return 2
    fi

    for host in "${@}"; do
        mapfile -t ips < <(dig +short "${host}")
        for i in "${ips[@]}" "${host}"; do
            if ! ssh-keygen -R "${i}"; then
                exitcode=2
            fi
        done
    done

    return "${exitcode}"
}

function ssh-tmux {
    ssh -t "$@" tmux new-session -A -s ssh_tmux
}
alias ssht=ssh-tmux

function ssh-wait {
    local ignore

    ignore='(Connection refused|No route to host)'

    until ssh "${@}" true 2> >(grep -v -E "${ignore}" >&2); do
        echo -n .
        sleep 3
    done && {
        echo
        ssh "${@}"
    }
}

function list-ssh-servers {
    avahi-browse _ssh._tcp -t                                                  \
        | grep --color=never IPv4                                              \
        | awk '{ printf "%-10s %s.local\n", $2, $4 }'                          \
        | sort
}

function mdns-ssh {
    local connection

    if _help-flag "${@}"; then
        echo "Usage: mdns-ssh [-u <USER> --user <USER>]"
        echo
        echo "Choose and connect to a host over SSH, using mdns to get"
        echo "a list of possible candidates."
        return
    fi

    connection="$( avahi-browse _ssh._tcp -t                                   \
                 | grep --color=never IPv4                                     \
                 | awk '{ printf "%s.local\n", $4 }'                           \
                 | sort                                                        \
                 | fzf
                 )"

    opts="$(getopt -o u: --long user: -n "mdns-ssh" -- "$@")" || return 2
    eval set -- "${opts}"
    while true; do
        case "$1" in
            -u | --user )
                connection="${2}@${connection}"
                shift 2
                ;;
            -- ) shift; break ;;
            * ) break ;;
        esac
    done

    # shellcheck disable=SC2029
    ssh "${connection}" "${@}"
}

_completion_loader ssh
complete -F _comp_cmd_ssh ssh-add-hostkey
complete -F _comp_cmd_ssh ssh-regenerate-hostkey
complete -F _comp_cmd_ssh ssh-remove-hostkey
complete -F _comp_cmd_ssh ssh-wait
complete -F _comp_cmd_ssh ssh-tmux

################################################################################
## IP Calculation

function ip2mac {
    local values out

    IFS='.' read -ra values <<< "${1}"
    out=()

    for v in "${values[@]}"; do
        out+=("$(dec2hex "${v}")")
    done

    join-string ':' "${out[@]}"
}

function mac2ip {
    local values out

    IFS=':' read -ra values <<< "${1}"
    out=()

    for v in "${values[@]}"; do
        out+=("$(hex2dec "${v}")")
    done

    join-string '.' "${out[@]}"
}

################################################################################
## Networking

function host-wait {
    local host success

    host="${1}"
    success=true

    set +m

    { until ping -c1 -w1 -W1 "${host}" >/dev/null; do
          echo -ne .
      done &
    } 2>/dev/null

    # shellcheck disable=SC2064
    trap "success=false; kill ${!}; disown ${!}; trap - SIGINT" SIGINT
    wait "${!}"

    if [ "${success}" = true ]; then
        echo Up!
    else
        echo
    fi
}

function speedtest {
    local prefix

    case "${#}" in
        0) prefix="" ;;
        *) prefix="ssh -t ${*}"
    esac

    ${prefix} iperf3                                                           \
              -R -u                                                            \
              -6                                                               \
              -c speedtest.cityhost.se                                         \
              -p 5201-5206                                                     \
              -i 1                                                             \
              -t 10
}

function simple-dns {
    local -a domains

    local ip_count
    local -a args

    domains=( "${@}" )
    if [ "${#domains[@]}" -le 0 ]; then
        domains=( "test.tld" )
    fi

    ip_count=200

    for domain in "${domains[@]}"; do
        args+=( "--host-record=${domain},127.0.0.$(( ++ip_count ))" )
    done

    dnsmasq -Rhd -p 10053 -a 127.0.0.153 "${args[@]}"
}

function simple-dhcp {
    local interface

    if [ "${#}" -lt 2 ]; then
        cat <<EOM
Usage: ${FUNCNAME[0]} <INTERFACE> <RANGE>

Where RANGE is something like:

   192.168.127.2,192.168.127.254,255.255.255.0,6h

EOM
        return 2
    fi

    interface="${1}"
    range="${2}"

    sudo dnsmasq -d --interface "${interface}" --dhcp-range="${range}" --port=0
}

function ip-of {
    local addr

    if [ ! $# -eq 1 ] || _help-flag; then
        echo "Usage: ip-of <address>"
        return 2
    fi

    addr="${1}"

    if ipcalc -cs "${addr}"; then
        echo "${addr}"
    else
        dig +short "${addr}"
    fi
}

function device-route-for {
    local addr

    if [ ! $# -eq 1 ] || _help-flag; then
        echo "Usage: get-route-device-for <address>"
        return 2
    fi

    addr="${1}"

    ip -o route get "$(ip-of "${addr}")" | sed -E 's/^.+?dev ([^ ]+?) .+$/\1/'
}

function show-ip-ranges {
    if  [ "${#}" -lt 1 ] || _help-flag; then
        echo "Usage: ${FUNCNAME[0]} <IP-RANGE>..."
        return 2
    fi

    ranges=("${@}")

    for range in "${ranges[@]}"; do
        echo -ne "${range}: "
        ipcalc "${range}" --minaddr --maxaddr                                  \
            | tr '\n' ':'                                                      \
            | sed -E 's/MINADDR=([0-9\.]+):MAXADDR=([0-9\.]+):/\1 ⇒ \2\n/'
    done | column -t
}

_completion_loader host
complete -F _comp_cmd_host host-wait
complete -F _comp_cmd_host speedtest

################################################################################
## Ansible

function ansible-record-hostkeys {
    local host_pattern inventory

    if [ ! $# -eq 2 ] || _help-flag; then
        echo "Usage: ansible-record-hostkeys <inventory> <host-pattern>"
        echo
        echo "Import ssh host keys from hosts"
        return 2
    fi

    inventory="${1}"
    host_pattern="${2}"

    export ANSIBLE_HOST_KEY_CHECKING=False                                     \
        && ansible -e 'record_host_keys=True'                                  \
                   -m ping                                                     \
                   -i "${inventory}"                                           \
                   "${host_pattern}"
}

################################################################################
## Containers

function http-serve {
    local path

    path="$(readlink -f "${1:-.}")"

    mkdir -p "${path}"

    podman run                                                                 \
           --rm                                                                \
           --publish 8080:80                                                   \
           --name nginx-serve                                                  \
           --volume "${path}:/usr/share/nginx/html:ro"                         \
           nginx:1.14-alpine
}

function container-volume-by-age {
    local vol cri rx_date rx_time rx_ms rx_tz
    local -a cmd

    cri="${1:-podman}"

    case "${cri}" in
        podman) cmd=(podman)      ;;
        docker) cmd=(sudo docker) ;;
        *) echo "Unknown container runtime: ${cri}!" >&2
           return 2
    esac

    rx_date='[0-9]{4}-[0-9]{2}-[0-9]{2}'
    rx_time='[0-9]{2}:[0-9]{2}:[0-9]{2}'
    rx_ms='\.[0-9]+'
    rx_tz='\+[0-9]+ [A-Z]+'
    rx="(${rx_date}) (${rx_time})${rx_ms} ${rx_tz}"

    for vol in $("${cmd[@]}" volume ls --format "{{.Name}}"); do
        "${cmd[@]}" volume inspect "${vol}" -f "{{ .CreatedAt }}"              \
            | sed -r "s/${rx}/\1 \2 ${vol}/"
    done | column -t | sort
}

alias docker-volume-by-age='container-volume-by-age docker'
alias podman-volume-by-age='container-volume-by-age podman'

function docker-rootkit {
    local -a cmd

    cmd=(docker run --rm -it -v /:/host --privileged fedora chroot /host)

    if [ ! -w /run/docker.sock ]; then
        cat <<EOM
You're *not* in the docker group!
If you were, this command would make you root:

  \$ ${cmd[*]}

EOM
    else
        echo "\$ ${cmd[*]}"
        echo "You're root!"
        echo
        "${cmd[@]}"
    fi
}

################################################################################
## Kubernetes

function minikube {
    case "${1:-}" in
        start) command minikube --profile=local start --keep-context "${@:2}" ;;
        *)     command minikube --profile=local "${@}"                        ;;
    esac
}

################################################################################
