#! /usr/bin/env python3
import argparse
import sys
import subprocess
from typing import Optional


def has_ykman() -> bool:
    try:
        subprocess.run(["ykman", "--version"], check=True, stdout=subprocess.DEVNULL)
        return True
    except subprocess.CalledProcessError:
        return False


def has_yk_plugged_in() -> bool:
    devices = subprocess.run(["ykman", "list"], check=True, stdout=subprocess.PIPE)
    devices = devices.stdout.decode("utf-8").split("\n")
    return len(devices) > 1


def is_interface_up(interface: str) -> bool:
    try:
        subprocess.run(
            ["ip", "link", "show", interface], check=True, stdout=subprocess.DEVNULL
        )
        return True
    except subprocess.CalledProcessError:
        return False


def get_oath_code(service: str) -> Optional[int]:
    response = subprocess.run(
        ["ykman", "oath", "accounts", "code", "Guru"],
        check=True,
        stdout=subprocess.PIPE,
    )
    output = response.stdout.decode("utf-8")
    if not output:
        return None
    return int(output.split("\n")[0].split(" ")[-1])


def get_password(label: str, ns: str, key: str) -> str:
    # Try to find it
    try:
        result = subprocess.run(
            ["secret-tool", "lookup", ns, key], check=True, stdout=subprocess.PIPE
        )
        return result.stdout.decode("utf-8")
    except subprocess.CalledProcessError:
        # If we are here, it doesn't exist
        print(f"Enter your {label}")
        subprocess.run(["secret-tool", "store", "--label", label, ns, key], check=True)
        return get_password(label, ns, key)


def handle_connect(args: argparse.Namespace) -> int:
    if not has_yk_plugged_in():
        print("Could not find YubiKey. Is it plugged in?", file=sys.stderr)
        return 1

    # If we are connected to AS54041, we need to briefly kill the connection
    if args.wireguard_support and is_interface_up("vpn"):
        print("Bringing down AS54041 VPN")
        subprocess.run(["sudo", "wg-quick", "down", "vpn"], check=True)

    # Get the base password
    base_password = get_password("Guru VPN Password", "guru-vpn", "base-password")

    # Fetch the credentials from the Yubikey
    oath_code = get_oath_code("Guru")
    print(f"Using OATH code: {oath_code}")
    if not len(str(oath_code)) == 6:
        print("Invalid OATH code length. Try again in a minute.", file=sys.stderr)
        return 1

    # Construct the one-time password
    password = f"{base_password}{oath_code}"

    # Connect via nmcli
    print("Bringing up Guru VPN")
    subprocess.run(
        [
            "nmcli",
            "connection",
            "modify",
            "Guru VPN",
            "vpn.secrets",
            f"password={password}",
        ],
        check=True,
    )
    subprocess.run(["nmcli", "connection", "up", "Guru VPN"], check=True)

    # Bring AS54041 back up
    print("Bringing up AS54041 VPN")
    if args.wireguard_support:
        subprocess.run(["sudo", "wg-quick", "up", "vpn"], check=True)


def handle_disconnect(args: argparse.Namespace) -> int:
    # Disconnect from Guru VPN
    print("Bringing down Guru VPN")
    result = subprocess.run(["nmcli", "connection", "down", "Guru VPN"])
    return result.returncode


def main() -> int:
    # Handle program arguments
    ap = argparse.ArgumentParser(
        prog="guru-vpn", description="Utility for connecting to the Guru VPN"
    )
    ap.add_argument(
        "operation",
        choices=["connect", "disconnect", "reconnect"],
        help="Operation to perform",
    )
    ap.add_argument(
        "-w",
        "--wireguard-support",
        help="Handles wireguard interfaces",
        action="store_true",
    )
    args = ap.parse_args()

    # Ensure we can actually get credentials from the Yubikey
    if not has_ykman():
        print("Could not execute `ykman`. Is it installed?", file=sys.stderr)
        return 1

    # Handle subcommands
    cmd_fns = {
        "connect": handle_connect,
        "disconnect": handle_disconnect,
        "reconnect": lambda args: handle_disconnect(args) or handle_connect(args),
    }
    return cmd_fns[args.operation](args)


if __name__ == "__main__":
    sys.exit(main())