#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SQL generator for creating Guacamole user with custom password

Uses same hashing algorithm as Guacamole:
- SHA-256(password_bytes + salt_bytes)
- Random 32-byte salt

Usage:
    python generate_guacamole_user.py --username admin --password MySecurePass123
    python generate_guacamole_user.py --username admin --password MySecurePass123 --admin
"""

import hashlib
import secrets
import argparse
import sys
import io

# Fix Windows encoding issues
if sys.platform == 'win32':
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')


def generate_guacamole_password_hash(password: str) -> tuple[bytes, bytes]:
    """
    Generate hash and salt for Guacamole password
    
    CORRECT ALGORITHM (verified 2025-10-29):
    Guacamole uses: SHA-256(password_string + salt_hex_string)
    IMPORTANT: Salt converted to HEX string BEFORE hashing!
    
    Args:
        password: Password in plain text
        
    Returns:
        Tuple (password_hash, password_salt) as bytes for PostgreSQL
    """
    # Generate random 32-byte salt
    salt_bytes = secrets.token_bytes(32)
    
    # CRITICAL: Convert salt to HEX STRING (uppercase)
    # Guacamole hashes: password + hex(salt), NOT password + binary(salt)!
    salt_hex_string = salt_bytes.hex().upper()
    
    # Compute SHA-256(password_string + salt_hex_string)
    # Concatenate password STRING + salt HEX STRING, then encode to UTF-8
    hash_input = password + salt_hex_string
    password_hash = hashlib.sha256(hash_input.encode('utf-8')).digest()
    
    return password_hash, salt_bytes


def bytes_to_postgres_hex(data: bytes) -> str:
    """
    Convert bytes to PostgreSQL hex format for decode()
    
    Args:
        data: Bytes to convert
        
    Returns:
        String in 'HEXSTRING' format for use in decode('...', 'hex')
    """
    return data.hex().upper()


def generate_sql(username: str, password: str, is_admin: bool = False) -> str:
    """
    Generate SQL for creating Guacamole user
    
    Args:
        username: Username
        password: Password
        is_admin: If True, grant full administrator privileges
        
    Returns:
        SQL script to execute
    """
    password_hash, password_salt = generate_guacamole_password_hash(password)
    
    hash_hex = bytes_to_postgres_hex(password_hash)
    salt_hex = bytes_to_postgres_hex(password_salt)
    
    sql = f"""-- Generated Guacamole user creation SQL
-- Username: {username}
-- Password: {'*' * len(password)} (length: {len(password)})
-- Generated with: generate_guacamole_user.py

-- Create user entity
INSERT INTO guacamole_entity (name, type) 
VALUES ('{username}', 'USER');

-- Create user with password hash
INSERT INTO guacamole_user (entity_id, password_hash, password_salt, password_date)
SELECT
    entity_id,
    decode('{hash_hex}', 'hex'),
    decode('{salt_hex}', 'hex'),
    CURRENT_TIMESTAMP
FROM guacamole_entity 
WHERE name = '{username}' AND guacamole_entity.type = 'USER';
"""

    if is_admin:
        sql += f"""
-- Grant all system permissions (administrator)
INSERT INTO guacamole_system_permission (entity_id, permission)
SELECT entity_id, permission::guacamole_system_permission_type
FROM (
    VALUES
        ('{username}', 'CREATE_CONNECTION'),
        ('{username}', 'CREATE_CONNECTION_GROUP'),
        ('{username}', 'CREATE_SHARING_PROFILE'),
        ('{username}', 'CREATE_USER'),
        ('{username}', 'CREATE_USER_GROUP'),
        ('{username}', 'ADMINISTER')
) permissions (username, permission)
JOIN guacamole_entity ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER';

-- Grant permission to read/update/administer self
INSERT INTO guacamole_user_permission (entity_id, affected_user_id, permission)
SELECT guacamole_entity.entity_id, guacamole_user.user_id, permission::guacamole_object_permission_type
FROM (
    VALUES
        ('{username}', '{username}', 'READ'),
        ('{username}', '{username}', 'UPDATE'),
        ('{username}', '{username}', 'ADMINISTER')
) permissions (username, affected_username, permission)
JOIN guacamole_entity          ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER'
JOIN guacamole_entity affected ON permissions.affected_username = affected.name AND guacamole_entity.type = 'USER'
JOIN guacamole_user            ON guacamole_user.entity_id = affected.entity_id;
"""

    return sql


def main():
    parser = argparse.ArgumentParser(
        description='Generate SQL for creating Guacamole user with custom password',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
    # Create regular user
    python generate_guacamole_user.py --username john --password SecurePass123
    
    # Create administrator user
    python generate_guacamole_user.py --username admin --password AdminPass456 --admin
    
    # Save to file
    python generate_guacamole_user.py --username admin --password AdminPass456 --admin > 002-custom-admin.sql
    
    # Apply directly to running database
    python generate_guacamole_user.py --username admin --password AdminPass456 --admin | \\
        docker compose exec -T postgres psql -U guacamole_user -d guacamole_db
        
SECURITY NOTES:
    - Never commit generated SQL files with passwords to git!
    - Use strong passwords (minimum 16 characters, mixed case, numbers, symbols)
    - Change default passwords immediately after deployment
    - Store passwords securely (password manager, secrets vault)
"""
    )
    
    parser.add_argument(
        '--username',
        required=True,
        help='Username for the new Guacamole user'
    )
    
    parser.add_argument(
        '--password',
        required=True,
        help='Password for the new user (plain text)'
    )
    
    parser.add_argument(
        '--admin',
        action='store_true',
        help='Grant administrator privileges (ADMINISTER system permission)'
    )
    
    parser.add_argument(
        '--verify',
        action='store_true',
        help='Verify password by generating hash twice'
    )
    
    args = parser.parse_args()
    
    # Validate password strength
    if len(args.password) < 8:
        print("[WARNING] Password is too short (< 8 characters)", file=sys.stderr)
        print("   Recommended: minimum 16 characters with mixed case, numbers, symbols", file=sys.stderr)
        response = input("Continue anyway? (y/N): ")
        if response.lower() != 'y':
            sys.exit(1)
    
    # Verify if requested
    if args.verify:
        print("[VERIFY] Verifying hash generation...", file=sys.stderr)
        hash1, salt1 = generate_guacamole_password_hash(args.password)
        hash2, salt2 = generate_guacamole_password_hash(args.password)
        
        # Salts should be different (random)
        if salt1 == salt2:
            print("[ERROR] Salt generation not random!", file=sys.stderr)
            sys.exit(1)
        
        # But if we use same salt, hash should be same
        # Use correct algorithm: SHA256(password_string + salt_hex_string)
        salt_hex_string = salt1.hex().upper()
        hash_test = hashlib.sha256((args.password + salt_hex_string).encode('utf-8')).digest()
        if hash_test == hash1:
            print("[OK] Hash generation verified", file=sys.stderr)
        else:
            print("[ERROR] Hash generation mismatch!", file=sys.stderr)
            sys.exit(1)
    
    # Generate SQL
    sql = generate_sql(args.username, args.password, args.admin)
    
    # Output
    print(sql)
    
    # Print info to stderr (so it doesn't interfere with piping SQL)
    role = "Administrator" if args.admin else "Regular User"
    print(f"\n[OK] SQL generated successfully!", file=sys.stderr)
    print(f"   Username: {args.username}", file=sys.stderr)
    print(f"   Role: {role}", file=sys.stderr)
    print(f"   Password length: {len(args.password)} characters", file=sys.stderr)
    print(f"\n[INFO] To apply this SQL:", file=sys.stderr)
    print(f"   docker compose exec -T postgres psql -U guacamole_user -d guacamole_db < output.sql", file=sys.stderr)


if __name__ == '__main__':
    main()

