#!/usr/bin/bash

# This project is licensed under the MIT License (see LICENSE).

set -euo pipefail

readonly VERSION=v0.5.1

readonly default_config='[DEFAULT]
lowercase=true
uppercase=true
digits=true
symbols=true'

PASSLESS_FINDER="${PASSLESS_FINDER:-fzf}"

if [[ -n "${PASSLESS_CONFIG-}" ]]; then
    git_repo="${PASSLESS_CONFIG%/*}"
    config="$PASSLESS_CONFIG"
else
    git_repo="${XDG_CONFIG_HOME:-$HOME/.config}"/passless
    config="$git_repo"/passless.conf.gpg
fi

if command -v git > /dev/null && [[ -e "$git_repo"/.git ]]; then
    git_enabled=true
else
    git_enabled=false
fi

decrypted=
decrypted_checksum=
trap 'rm -f "$decrypted" "$decrypted_checksum"' EXIT INT TERM

usage() {
    echo 'usage: passless [options] [command|SITE]

options:
  -h            Show help message
  -f CONFIG     Path to config file
  -l LOGIN      Get password of SITE with given login
  -s            Print password
  -p            Push commits to git remote after editing
  -P            Do not pull from git remote before editing
  -v            Show version

commands:
  edit          Edit config file
  list          List sites and logins
  find          Select site with fuzzy finder
  log           Show git commit log
  status        Show git status
  push          Push commits to git remote
  pull          Pull commits from git remote
  encrypt FILE  Encrypt existing file'
}

check_cmd() {
    command -v "$1" > /dev/null
}

get_copy_cmd() {
    if [[ -n "$WAYLAND_DISPLAY" ]] && check_cmd wl-copy; then
        echo wl-copy
    elif check_cmd xsel; then
        echo xsel -ib
    else
        echo "Copy command not found: install 'wl-copy' or 'xsel'" >&2
        exit 1
    fi
}

git_pull() {
    $git_enabled || return 0
    git -C "$git_repo" pull >&2
}

git_commit() {
    $git_enabled || return 0
    git -C "$git_repo" add "$config" && git -C "$git_repo" commit
}

git_push() {
    $git_enabled || return 0
    git -C "$git_repo" push
}

git_log() {
    $git_enabled || return 0
    git -C "$git_repo" log --oneline
}

git_status() {
    $git_enabled || return 0
    git -C "$git_repo" status
}

print_list() {
    while read -r line; do
        awk '{
            x=1
            while (x<=NF) {
                split($x,a,"=")
                if (a[1] == "section") printf "%s", a[2]
                else if (a[1] == "login") printf " | %s\n", a[2]
                x++
            }
        }' <<< "$line"
    done < <(iniq -O login "$decrypted")
}

decrypt() {
    decrypted=$(mktemp)

    if [[ -n "${PASSLESS_PLAIN_FILE-}" ]]; then
        cat "$config" > "$decrypted"
    else
        gpg -d "$config" > "$decrypted" 2> /dev/null

        decrypted_checksum=$(mktemp)
        sha256sum "$decrypted" > "$decrypted_checksum"
    fi
}

checksum() {
    [[ -e "$decrypted_checksum" ]] && sha256sum -c "$decrypted_checksum" \
        --status
}

encrypt() {
    local f="${1-$decrypted}"

    if [[ -n "${PASSLESS_PLAIN_FILE-}" ]]; then
        cat "$f" > "$config"
    elif ! checksum; then
        gpg --default-recipient-self --yes -o "$config" -e "$f"
        git_commit
        if ${git_do_push-false}; then
            git_push
        fi
    fi
}

edit_config() {
    if [[ ! -e "$config" ]]; then
        decrypted=$(mktemp)
        echo "$default_config" > "$decrypted"
    else
        if ${git_do_pull-true}; then
            git_pull
        fi
        decrypt
    fi
    $EDITOR "$decrypted"
    encrypt
}

handle_bool() {
    [[ "$1" == true ]] && echo "$2" || echo "$3"
}

get_section() {
    local name="${1//\./\\.}"
    local opts=''
    local i=0
    local n

    shift
    if ! n="$(iniq -p "$name" -n "$decrypted")"; then
        echo "Section not found: $name" >&2
        exit 2
    fi

    while (( i < n )); do
        opts="$(iniq -p "$name" -i $i "$decrypted")"
        [[ -z "$1" ]] && break
        while IFS='=' read -r k v; do
            if [[ "$k" == login && "$v" == "$1" ]]; then
                echo "$opts"
                return
            fi
        done <<< "$opts"
        i=$((i + 1))
    done

    echo "$opts"
}

generate_password() {
    local master=''
    local login=''
    local args=''
    local opts

    decrypt
    opts="$(get_section "$@")"

    while IFS='=' read -r k v; do
        case $k in
            password)
                if ${show-false}; then
                    echo "$v"
                else
                    echo "$v" | $(get_copy_cmd)
                fi
                exit ;;
            master_password) master="$v" ;;
            login) login="$v" ;;
            length) args+=" -L $v" ;;
            counter) args+=" -C $v" ;;
            lowercase) args+=" $(handle_bool "$v" '-l' '--no-lowercase')" ;;
            uppercase) args+=" $(handle_bool "$v" '-u' '--no-uppercase')" ;;
            digits) args+=" $(handle_bool "$v" '-d' '--no-digits')" ;;
            symbols) args+=" $(handle_bool "$v" '-s' '--no-symbols')" ;;
            *)
                echo "Invalid key: $k" >&2
                exit 1
        esac
    done <<< "$opts"

    if ! ${show-false}; then
        copy=true
    fi

    args="$1 $login $master ${copy+-c} $args"

    lesspass $args
    exit
}

while getopts ':hf:l:spPv' opt; do
    case "$opt" in
        h) usage; exit ;;
        f) config="$OPTARG" ;;
        l) with_login="$OPTARG" ;;
        s) show=true ;;
        p) git_do_push=true ;;
        P) git_do_pull=false ;;
        v) echo "$VERSION"; exit ;;
        *) usage >&2; exit 2
    esac
done

shift $((OPTIND - 1))

if [[ "${1-}" == encrypt ]]; then
    shift
    if [[ -z "${1-}" ]]; then
        echo "'encrypt' requires a file to be specified" >&2
        usage >&2
        exit 2
    fi
    encrypt "$1"
    exit
fi

if [[ ! -e "$config" ]]; then
    echo "$config does not exist; use 'edit' to create it" >&2
    exit 1
fi

case "${1-}" in
    edit) edit_config; exit ;;
    list) shift; do_list=true ;;
    find) shift; do_find=true ;;
    log) git_log; exit ;;
    status) git_status; exit ;;
    push) git_push; exit ;;
    pull) git_pull; exit ;;
esac

[[ $# -gt 0 ]] && generate_password "$1" "${with_login-}"

if ${show-false}; then
    echo "'-s' requires a site to be specified" >&2
    usage >&2
    exit 2
fi

decrypt

if ${do_find-false}; then
    site="$(print_list | $PASSLESS_FINDER)"
    generate_password "${site%% |*}" "${site##*| }"
elif ${do_list-false}; then
    print_list
else
    iniq "$decrypted"
fi
