#!/bin/sh
set -eu

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VENV_DIR="$SCRIPT_DIR/.htpasswd-venv"

if [ ! -d "$VENV_DIR" ]; then
    python3 -m venv "$VENV_DIR"
    "$VENV_DIR/bin/pip" install --quiet passlib bcrypt
fi

PYTHON_SCRIPT="$VENV_DIR/bin/htpasswd"
if [ ! -f "$PYTHON_SCRIPT" ]; then
    cat > "$PYTHON_SCRIPT" << 'PYEOF'
#!/usr/bin/env python3
"""htpasswd - Apache htpasswd compatible user authentication file manager."""

import argparse
import base64
import hashlib
import os
import sys

try:
    import bcrypt as _bcrypt
    HAS_BCRYPT = True
except ImportError:
    HAS_BCRYPT = False

try:
    import passlib.hash
    HAS_PASSLIB = True
except ImportError:
    HAS_PASSLIB = False

try:
    import crypt as _crypt
    HAS_CRYPT = True
except ImportError:
    HAS_CRYPT = False


def apr1_hash(password, salt=None):
    if HAS_PASSLIB:
        kw = {}
        if salt:
            kw["salt"] = salt
        return passlib.hash.apr_md5_crypt.using(**kw).hash(password)
    if HAS_CRYPT:
        if salt is None:
            salt = base64.b64encode(os.urandom(6)).decode()[:8]
        result = _crypt.crypt(password, "$apr1$" + salt)
        if result is None or result == "*":
            raise RuntimeError("apr1 hash not supported by system crypt")
        return result
    raise RuntimeError("apr1 requires passlib or system crypt")


def sha_hash(password):
    sha1 = hashlib.sha1(password.encode()).digest()
    return "{SHA}" + base64.b64encode(sha1).decode()


def bcrypt_hash(password, cost=5):
    if HAS_BCRYPT:
        salt = _bcrypt.gensalt(rounds=cost, prefix=b"2b")
        return _bcrypt.hashpw(password.encode(), salt).decode().replace("$2b$", "$2y$", 1)
    if HAS_PASSLIB:
        return passlib.hash.bcrypt.using(rounds=cost, ident="2y").hash(password)
    raise RuntimeError("bcrypt requires bcrypt or passlib package")


def crypt_hash(password):
    if HAS_PASSLIB:
        return passlib.hash.des_crypt.hash(password)
    if HAS_CRYPT:
        salt = _crypt.mksalt(_crypt.METHOD_DES)
        result = _crypt.crypt(password, salt)
        if result is None or result == "*":
            raise RuntimeError("DES crypt failed")
        return result
    raise RuntimeError("crypt requires passlib or system crypt module")


def parse_htpasswd(content):
    users = {}
    for line in content.splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        if ":" in line:
            user, pw = line.split(":", 1)
            users[user] = pw
    return users


def format_htpasswd(users):
    return "\n".join(f"{u}:{p}" for u, p in users.items()) + "\n"


def main():
    parser = argparse.ArgumentParser(
        description="Manage htpasswd authentication files"
    )
    parser.add_argument("-c", action="store_true", help="Create (truncate) file")
    parser.add_argument(
        "-n", action="store_true", help="Display result on stdout"
    )
    parser.add_argument(
        "-m", action="store_true", help="Force MD5 (apr1) encryption (default)"
    )
    parser.add_argument(
        "-d", action="store_true", help="Force CRYPT encryption"
    )
    parser.add_argument(
        "-p", action="store_true", help="Plaintext password (insecure)"
    )
    parser.add_argument(
        "-s", action="store_true", help="Force SHA encryption"
    )
    parser.add_argument(
        "-B", action="store_true", help="Force bcrypt encryption"
    )
    parser.add_argument(
        "-b", action="store_true",
        help="Batch mode (password provided on command line)"
    )
    parser.add_argument(
        "-D", action="store_true", help="Delete the specified user"
    )
    parser.add_argument(
        "-C", type=int, default=5,
        help="bcrypt cost (default 5, requires -B)"
    )
    parser.add_argument("args", nargs="*", metavar="file|username|password")

    args = parser.parse_args()

    # Parse positional arguments based on mode
    if args.D:
        if len(args.args) < 2:
            parser.error("-D requires file and username")
        file_arg = args.args[0]
        username = args.args[1]
    elif args.n:
        if len(args.args) < 1:
            parser.error("-n requires username")
        username = args.args[0]
        password = args.args[1] if len(args.args) >= 2 and args.b else None
    elif args.b:
        if len(args.args) < 3:
            parser.error("requires file, username, and password")
        file_arg = args.args[0]
        username = args.args[1]
        password = args.args[2]
    else:
        if len(args.args) < 2:
            parser.error("requires file and username")
        file_arg = args.args[0]
        username = args.args[1]
        password = None

    # Handle delete
    if args.D:
        if not os.path.exists(file_arg):
            print(f"htpasswd: file {file_arg} does not exist", file=sys.stderr)
            sys.exit(1)
        with open(file_arg) as f:
            users = parse_htpasswd(f.read())
        if username not in users:
            print(f"htpasswd: user {username} not found", file=sys.stderr)
            sys.exit(1)
        del users[username]
        if users:
            with open(file_arg, "w") as f:
                f.write(format_htpasswd(users))
        else:
            os.remove(file_arg)
        return

    # Get password
    if args.b:
        if not password:
            parser.error("-b requires a password argument")
    else:
        password = getpass.getpass("New password: ")
        confirm = getpass.getpass("Re-type new password: ")
        if password != confirm:
            print("Error: passwords do not match", file=sys.stderr)
            sys.exit(1)

    # Determine hash type
    hash_type = "apr1"
    if args.p:
        hash_type = "plaintext"
    elif args.d:
        hash_type = "crypt"
    elif args.s:
        hash_type = "sha"
    elif args.B:
        hash_type = "bcrypt"

    # Generate hash
    if hash_type == "plaintext":
        hashed = password
    elif hash_type == "crypt":
        hashed = crypt_hash(password)
    elif hash_type == "sha":
        hashed = sha_hash(password)
    elif hash_type == "bcrypt":
        hashed = bcrypt_hash(password, args.C)
    else:
        hashed = apr1_hash(password)

    # Output-only mode
    if args.n:
        print(f"{username}:{hashed}")
        return

    # Create or update file
    if args.c or not os.path.exists(file_arg):
        users = {}
    else:
        with open(file_arg) as f:
            users = parse_htpasswd(f.read())

    users[username] = hashed

    with open(file_arg, "w") as f:
        f.write(format_htpasswd(users))


if __name__ == "__main__":
    import getpass
    main()
PYEOF
    chmod +x "$PYTHON_SCRIPT"
fi

exec "$VENV_DIR/bin/python3" "$PYTHON_SCRIPT" "$@"
