#!/bin/bash

declare -f kstore > /dev/null
if [ $? -ne 0 ]; then
	echo "kotp depends on kstore" >&2
	return 1
fi

which oathtool > /dev/null 2>&1
if [ $? -ne 0 ]; then
	echo "oathtool is required" >&2
	return 1
fi

_KOTP_PREFIX="${_KOTP_PREFIX:-otp}"
_KOTP_PROP="${_KOTP_PROP:-secret}"

function kotp {
	case "$1" in
		set) shift; _kotp-set "$@" ;;
		gen|get) shift; _kotp-gen "$@" ;;
		spread) shift; _kotp-spread "$@" ;;
		list) shift; _kotp-list "$@" ;;
		del|delete|rm) shift; _kotp-del "$@" ;;
		*)
			__func_head "set NAME [SECRET|-|file]    set/update otp key"
			__func_help "gen NAME [TIME]              generate otp at TIME; defaults to now"
			__func_help "spread NAME [COUNT] [TIME]   generate surrounding otp codes; COUNT defaults to 1"
			__func_help "list [TERM]                  list stored otp keys"
			__func_help "del NAME                     delete otp key"
			return 1
			;;
	esac
	return $?
}

function _kotp-key {
	echo "${_KOTP_PREFIX}:$1"
}

function _kotp-clean-secret {
	local s

	if [ $# -gt 0 ]; then
		s="$1"
	else
		s="$(cat)"
	fi

	case "$s" in
		otpauth://*)
			s="$(printf '%s' "$s" \
				| sed -n 's/.*[?&]secret=\([^&]*\).*/\1/p')"
			;;
	esac

	printf '%s' "$s" \
		| tr -d '[:space:]-' \
		| tr '[:lower:]' '[:upper:]'
}

function _kotp-read-value {
	if [ "$1" = "-" ]; then
		cat
	elif [ -n "$1" ] && [ -f "$1" ]; then
		cat "$1"
	else
		printf '%s' "$1"
	fi
}

function _kotp-set {
	if [ -z "$1" ]; then
		__func_head "NAME [SECRET|-|file]"
		return 1
	fi

	local name src secret key
	name="$1"
	src="$2"

	if [ -z "$src" ]; then
		read -rsp "OTP secret: " src
		echo
	fi

	secret="$(_kotp-read-value "$src" | _kotp-clean-secret)"
	if [ -z "$secret" ]; then
		echo "empty otp secret" >&2
		return 1
	fi

	key="$(_kotp-key "$name")"

	# Validate before storing. Most TOTP secrets are base32.
	oathtool --totp --base32 "$secret" > /dev/null 2>&1
	if [ $? -ne 0 ]; then
		echo "invalid otp secret, or oathtool cannot parse it as base32" >&2
		return 1
	fi

	kstore add "$key" "$secret" "$_KOTP_PROP" > /dev/null 2>&1
	if [ $? -ne 0 ]; then
		kstore update "$key" "$secret" "$_KOTP_PROP" || return 1
	fi

	echo "stored: $name"
}

function _kotp-secret {
	if [ -z "$1" ]; then
		__func_head "NAME"
		return 1
	fi

	kstore get "$(_kotp-key "$1")" "$_KOTP_PROP"
}

function _kotp-gen {
	if [ -z "$1" ]; then
		__func_head "NAME [TIME]"
		return 1
	fi

	local name when secret
	name="$1"
	when="$2"

	secret="$(_kotp-secret "$name")" || return 1

	if [ -n "$when" ]; then
		oathtool --totp --base32 --now "$when" "$secret"
	else
		oathtool --totp --base32 "$secret"
	fi
}

function _kotp-spread {
	if [ -z "$1" ]; then
		__func_head "NAME [COUNT] [TIME]"
		return 1
	fi

	local name count when secret i offset label
	name="$1"
	count="${2:-1}"
	when="$3"

	if ! [[ "$count" =~ ^[0-9]+$ ]]; then
		echo "COUNT must be a number" >&2
		return 1
	fi

	secret="$(_kotp-secret "$name")" || return 1

	for (( i=-count; i<=count; i++ )); do
		offset=$(( i * 30 ))

		if [ "$i" -eq 0 ]; then
			label="now"
		elif [ "$i" -lt 0 ]; then
			label="${offset}s"
		else
			label="+${offset}s"
		fi

		if [ -n "$when" ]; then
			printf '%8s  ' "$label"
			oathtool --totp --base32 --now "$when $offset seconds" "$secret"
		else
			printf '%8s  ' "$label"
			oathtool --totp --base32 --now "$offset seconds" "$secret"
		fi
	done
}

function _kotp-list {
	local term="$1"

	if [ -n "$term" ]; then
		kstore search "${_KOTP_PREFIX}:$term" "$_KOTP_PROP"
	else
		kstore search "${_KOTP_PREFIX}:" "$_KOTP_PROP"
	fi
}

function _kotp-del {
	if [ -z "$1" ]; then
		__func_head "NAME"
		return 1
	fi

	kstore del "$(_kotp-key "$1")" "$_KOTP_PROP"
}

function _kotp {
	local cur scope
	cur="${COMP_WORDS[COMP_CWORD]}"
	scope="${COMP_WORDS[COMP_CWORD-1]}"

	COMPREPLY=()

	case "$scope" in
		kotp)
			COMPREPLY=( $(compgen -W "set gen get spread list del" -- "$cur") )
			return
			;;
		gen|get|spread|del|delete|rm)
			COMPREPLY=( $(compgen -W "$(
				kstore search "${_KOTP_PREFIX}:" "$_KOTP_PROP" 2>/dev/null \
					| awk 'NR > 2 {print $1}' \
					| sed "s/^${_KOTP_PREFIX}://"
			)" -- "$cur") )
			return
			;;
		set)
			COMPREPLY=( $(compgen -f -- "$cur") )
			return
			;;
	esac
}

complete -F _kotp kotp
