#!/usr/bin/env python3
"""
User authentication and management module for multi-user support.

Handles user registration and authentication using SQLite
database stored on persistent Modal Volume.
"""

import os
import sqlite3
import threading
from pathlib import Path
from typing import Optional, Dict, Any, Tuple, List
from datetime import datetime
import hashlib

# Thread-local storage for authenticated username
# This allows the auth function to set the username, and get_current_user() to retrieve it
_thread_local = threading.local()


def get_db_path() -> Path:
    """Get the path to the SQLite database file."""
    base = Path(os.getenv("UHT_WORKDIR", "/var/lib/uht-discovery"))
    db_path = base / "users.db"
    return db_path


def init_user_db() -> None:
    """Initialize the user database and create table if it doesn't exist."""
    db_path = get_db_path()
    db_path.parent.mkdir(parents=True, exist_ok=True)
    
    conn = sqlite3.connect(str(db_path))
    cursor = conn.cursor()
    
    # Create users table
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS users (
            username TEXT PRIMARY KEY,
            password_hash TEXT NOT NULL,
            status TEXT NOT NULL CHECK(status IN ('pending', 'approved', 'blocked')),
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            approved_at TIMESTAMP,
            approved_by TEXT,
            last_login TIMESTAMP
        )
    """)
    
    # Create index on status for faster queries
    cursor.execute("""
        CREATE INDEX IF NOT EXISTS idx_status ON users(status)
    """)
    
    conn.commit()
    conn.close()


def _hash_password(password: str) -> str:
    """Hash a password using bcrypt."""
    try:
        import bcrypt
        # Generate salt and hash password
        salt = bcrypt.gensalt(rounds=12)
        hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
        return hashed.decode('utf-8')
    except ImportError:
        # Fallback to simple hash if bcrypt not available (not secure, but allows testing)
        # This should not be used in production
        return hashlib.sha256(password.encode('utf-8')).hexdigest()


def _verify_password(password: str, password_hash: str) -> bool:
    """Verify a password against a hash."""
    try:
        import bcrypt
        return bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8'))
    except ImportError:
        # Fallback for testing without bcrypt
        expected_hash = hashlib.sha256(password.encode('utf-8')).hexdigest()
        return password_hash == expected_hash


def register_user(username: str, password: str) -> Tuple[bool, str]:
    """
    Register a new user with approved status.
    
    Args:
        username: Username (must be alphanumeric + underscores, 3-50 chars)
        password: Plain text password (minimum 8 characters)
    
    Returns:
        Tuple of (success: bool, message: str)
    """
    import logging
    logger = logging.getLogger(__name__)
    # Validate username
    if not username:
        return False, "Username cannot be empty"
    
    if len(username) < 3 or len(username) > 50:
        return False, "Username must be between 3 and 50 characters"
    
    if not username.replace('_', '').replace('-', '').isalnum():
        return False, "Username can only contain letters, numbers, underscores, and hyphens"
    
    # Validate password
    if len(password) < 8:
        return False, "Password must be at least 8 characters long"
    
    # Initialize database
    init_user_db()
    
    db_path = get_db_path()
    conn = sqlite3.connect(str(db_path))
    cursor = conn.cursor()
    
    try:
        # Check if username already exists
        cursor.execute("SELECT username FROM users WHERE username = ?", (username,))
        if cursor.fetchone():
            conn.close()
            return False, "Username already exists"
        
        # Hash password and insert user
        password_hash = _hash_password(password)
        cursor.execute("""
            INSERT INTO users (username, password_hash, status, approved_at, approved_by)
            VALUES (?, ?, 'approved', CURRENT_TIMESTAMP, 'self-service')
        """, (username, password_hash))
        
        conn.commit()
        conn.close()
        
        logger.info(f"New user registration: '{username}' registered (approved)")
        return True, f"Registration successful. Username: {username}"
    
    except sqlite3.Error as e:
        conn.rollback()
        conn.close()
        return False, f"Database error: {str(e)}"


# Global flag to track if database has been initialized
_db_initialized = False

def validate_user(username: str, password: str) -> Tuple[Optional[str], Optional[Dict[str, Any]], Optional[str]]:
    """
    Validate user credentials and return user record if approved.
    
    Args:
        username: Username to validate
        password: Plain text password
    
    Returns:
        Tuple of (error_code, user_record, error_message):
        - If valid: (None, user_dict, None)
        - If user not found: ("USER_NOT_FOUND", None, "User not found")
        - If invalid password: ("INVALID_CREDENTIALS", None, "Invalid password")
        - If pending: ("PENDING_APPROVAL", None, "Account pending administrator approval")
        - If blocked: ("ACCOUNT_BLOCKED", None, "Account has been blocked")
        - On error: ("ERROR", None, "Database error occurred")
    """
    import logging
    logger = logging.getLogger(__name__)
    
    # Only initialize if not already done (optimization)
    global _db_initialized
    if not _db_initialized:
        init_user_db()
        _db_initialized = True
    
    db_path = get_db_path()
    conn = sqlite3.connect(str(db_path))
    cursor = conn.cursor()
    
    try:
        # Get user record
        cursor.execute("""
            SELECT username, password_hash, status, created_at, approved_at, approved_by
            FROM users WHERE username = ?
        """, (username,))
        
        row = cursor.fetchone()
        if not row:
            logger.warning(f"Login attempt: User '{username}' not found")
            conn.close()
            return ("USER_NOT_FOUND", None, "User not found")
        
        username_db, password_hash, status, created_at, approved_at, approved_by = row
        
        # Verify password
        if not _verify_password(password, password_hash):
            logger.warning(f"Login attempt: Invalid password for user '{username}'")
            conn.close()
            return ("INVALID_CREDENTIALS", None, "Invalid password")
        
        # Check user status
        if status == 'pending':
            logger.info(f"Login attempt: User '{username}' is pending approval (auto-allowing)")
            status = 'approved'
            cursor.execute("""
                UPDATE users
                SET status = 'approved',
                    approved_at = COALESCE(approved_at, CURRENT_TIMESTAMP),
                    approved_by = COALESCE(approved_by, 'self-service')
                WHERE username = ?
            """, (username,))
            conn.commit()
        
        if status == 'blocked':
            logger.warning(f"Login attempt: Blocked user '{username}' attempted login")
            conn.close()
            return ("ACCOUNT_BLOCKED", None, "Account has been blocked. Please contact an administrator.")
        
        if status != 'approved':
            logger.warning(f"Login attempt: User '{username}' has unexpected status '{status}'")
            conn.close()
            return ("ERROR", None, f"Account has invalid status: {status}")
        
        # Update last login
        cursor.execute("""
            UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE username = ?
        """, (username,))
        conn.commit()
        
        logger.info(f"Successful login: User '{username}' authenticated")
        
        # Return user record
        user_record = {
            'username': username_db,
            'status': status,
            'created_at': created_at,
            'approved_at': approved_at,
            'approved_by': approved_by,
            'last_login': datetime.now().isoformat()
        }
        
        # Store username in thread-local storage for get_current_user()
        _thread_local.username = username
        
        conn.close()
        return (None, user_record, None)
    
    except sqlite3.Error as e:
        logger.error(f"Database error during user validation: {e}")
        conn.close()
        return ("ERROR", None, "Database error occurred. Please try again later.")


def approve_user(username: str, approved_by: str) -> Tuple[bool, str]:
    """
    Approve a pending user account.
    
    Args:
        username: Username to approve
        approved_by: Admin username who is approving
    
    Returns:
        Tuple of (success: bool, message: str)
    """
    import logging
    logger = logging.getLogger(__name__)
    
    init_user_db()
    
    db_path = get_db_path()
    conn = sqlite3.connect(str(db_path))
    cursor = conn.cursor()
    
    try:
        # Check if user exists
        cursor.execute("SELECT status FROM users WHERE username = ?", (username,))
        row = cursor.fetchone()
        if not row:
            conn.close()
            return False, f"User '{username}' not found"
        
        current_status = row[0]
        if current_status == 'approved':
            conn.close()
            return False, f"User '{username}' is already approved"
        
        # Update user status
        cursor.execute("""
            UPDATE users 
            SET status = 'approved', 
                approved_at = CURRENT_TIMESTAMP,
                approved_by = ?
            WHERE username = ?
        """, (approved_by, username))
        
        conn.commit()
        conn.close()
        
        logger.info(f"User approval: '{username}' approved by '{approved_by}'")
        return True, f"User '{username}' has been approved"
    
    except sqlite3.Error as e:
        conn.rollback()
        conn.close()
        return False, f"Database error: {str(e)}"


def get_user_status(username: str) -> Optional[str]:
    """
    Get the status of a user account.
    
    Args:
        username: Username to check
    
    Returns:
        Status string ('pending', 'approved', 'blocked') or None if user doesn't exist
    """
    init_user_db()
    
    db_path = get_db_path()
    conn = sqlite3.connect(str(db_path))
    cursor = conn.cursor()
    
    try:
        cursor.execute("SELECT status FROM users WHERE username = ?", (username,))
        row = cursor.fetchone()
        conn.close()
        
        return row[0] if row else None
    
    except sqlite3.Error:
        conn.close()
        return None


def get_authenticated_username() -> Optional[str]:
    """
    Get the currently authenticated username from thread-local storage.
    This is set by validate_user() during authentication.
    
    Returns:
        Username if authenticated, None otherwise
    """
    return getattr(_thread_local, 'username', None)


def set_authenticated_username(username: str) -> None:
    """
    Set the authenticated username in thread-local storage.
    Used for legacy auth fallback or custom authentication flows.
    
    Args:
        username: Username to set
    """
    _thread_local.username = username


def list_pending_users() -> List[Dict[str, Any]]:
    """
    List all pending users awaiting approval.
    
    Returns:
        List of user records with pending status
    """
    init_user_db()
    
    db_path = get_db_path()
    conn = sqlite3.connect(str(db_path))
    cursor = conn.cursor()
    
    try:
        cursor.execute("""
            SELECT username, created_at
            FROM users
            WHERE status = 'pending'
            ORDER BY created_at
        """)
        
        rows = cursor.fetchall()
        conn.close()
        
        return [{'username': row[0], 'created_at': row[1]} for row in rows]
    
    except sqlite3.Error:
        conn.close()
        return []


def is_user_admin(username: str) -> bool:
    """
    Check if a user has admin privileges.

    Currently uses UHT_ADMIN_USER env var comparison. Database `is_admin` column
    can be added later to support multiple admins.

    Args:
        username: Username to check

    Returns:
        True if user is an admin, False otherwise
    """
    if not username:
        return False
    admin_user = os.getenv("UHT_ADMIN_USER")
    return admin_user is not None and username == admin_user
