#!/usr/bin/env python3
"""
Simple HTTP server for ACWS Web Client
Serves the static HTML file and servers.ini configuration
Protected with HTTP Basic Authentication
"""

import http.server
import socketserver
import os
import sys
import base64
import configparser
from pathlib import Path

# Try to import bcrypt for secure password hashing
try:
    import bcrypt
    HAS_BCRYPT = True
except ImportError:
    HAS_BCRYPT = False
    print("WARNING: bcrypt not installed. Using less secure hashlib for password verification.")
    print("To install bcrypt: pip install bcrypt")
    import hashlib


class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
    """Custom request handler with HTTP Basic Authentication"""
    
    # Class variable to store users (loaded once, shared across all requests)
    _users = None
    _auth_loaded = False
    _auth_ini_path = None
    
    @classmethod
    def set_auth_ini_path(cls, auth_ini_path):
        """Set the path to auth.ini file"""
        cls._auth_ini_path = auth_ini_path
        cls._auth_loaded = False  # Reset so auth is reloaded with new path
        cls._users = None
    
    @classmethod
    def load_auth(cls):
        """Load authentication credentials from auth.ini file (class method)"""
        if cls._auth_loaded:
            return cls._users
        
        cls._users = {}
        
        if not cls._auth_ini_path or not cls._auth_ini_path.exists():
            if cls._auth_ini_path:
                print(f"WARNING: {cls._auth_ini_path} not found. Server is running without authentication.")
                print(f"To enable authentication, create {cls._auth_ini_path} with user credentials.")
            cls._auth_loaded = True
            return cls._users
        
        try:
            config = configparser.ConfigParser()
            config.read(cls._auth_ini_path)
            
            for username in config.sections():
                password_hash = config.get(username, 'password_hash', fallback='')
                if password_hash:
                    cls._users[username] = password_hash
            
            if cls._users:
                print(f"Loaded {len(cls._users)} user(s) from {cls._auth_ini_path}")
            else:
                print(f"WARNING: No users found in {cls._auth_ini_path}. Server is running without authentication.")
        
        except Exception as e:
            print(f"ERROR: Failed to load authentication file {cls._auth_ini_path}: {e}")
            print("Server is running without authentication.")
        
        cls._auth_loaded = True
        return cls._users
    
    def __init__(self, *args, directory=None, **kwargs):
        if directory is None:
            directory = Path(__file__).parent.absolute()
        super().__init__(*args, directory=directory, **kwargs)
        # Load auth once on first request
        self.__class__.load_auth()
    
    @property
    def users(self):
        """Get users dictionary (cached)"""
        return self.__class__._users or {}
    
    def verify_password(self, provided_password, stored_hash):
        """Verify a password against a stored hash"""
        if not stored_hash:
            return False
        
        if HAS_BCRYPT:
            # bcrypt hash format: $2b$12$salt+hash
            if stored_hash.startswith('$2b$') or stored_hash.startswith('$2a$'):
                try:
                    # bcrypt automatically includes salt in the hash
                    return bcrypt.checkpw(provided_password.encode('utf-8'), stored_hash.encode('utf-8'))
                except Exception as e:
                    print(f"ERROR: Password verification failed: {e}")
                    return False
            else:
                # Legacy format - assume it's a bcrypt hash without prefix check
                try:
                    return bcrypt.checkpw(provided_password.encode('utf-8'), stored_hash.encode('utf-8'))
                except:
                    return False
        else:
            # Fallback to SHA256 (less secure, not recommended for production)
            # For this fallback, we expect stored_hash to be a hex string
            if len(stored_hash) == 64:  # SHA256 hex string length
                provided_hash = hashlib.sha256(provided_password.encode('utf-8')).hexdigest()
                return provided_hash == stored_hash
            return False
    
    def check_authentication(self):
        """Check if the request is authenticated"""
        # If no users are configured, allow access
        if not self.users:
            return True
        
        auth_header = self.headers.get('Authorization')
        
        if not auth_header:
            return False
        
        try:
            # Parse Basic Auth header: "Basic base64(username:password)"
            auth_type, auth_string = auth_header.split(' ', 1)
            
            if auth_type.lower() != 'basic':
                return False
            
            decoded = base64.b64decode(auth_string).decode('utf-8')
            username, password = decoded.split(':', 1)
            
            if username in self.users:
                return self.verify_password(password, self.users[username])
        
        except Exception as e:
            print(f"Authentication error: {e}")
            return False
        
        return False
    
    def do_AUTHENTICATE(self):
        """Handle authentication requests"""
        if self.check_authentication():
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
        else:
            self.send_response(401)
            self.send_header('WWW-Authenticate', 'Basic realm="ACWS Control Panel"')
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(b'<h1>401 Unauthorized</h1><p>Authentication required.</p>')
    
    def do_GET(self):
        """Handle GET requests with authentication"""
        if not self.check_authentication():
            self.send_response(401)
            self.send_header('WWW-Authenticate', 'Basic realm="ACWS Control Panel"')
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(b'<h1>401 Unauthorized</h1><p>Authentication required.</p>')
            return
        
        super().do_GET()
    
    def do_HEAD(self):
        """Handle HEAD requests with authentication"""
        if not self.check_authentication():
            self.send_response(401)
            self.send_header('WWW-Authenticate', 'Basic realm="ACWS Control Panel"')
            self.end_headers()
            return
        
        super().do_HEAD()
    
    def end_headers(self):
        # Add CORS headers to allow cross-origin requests if needed
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        super().end_headers()
    
    def log_message(self, fmt, *args):
        """Override to show cleaner log messages"""
        print(f"[{self.log_date_time_string()}] {fmt % args}")


def main(ip, port, script_dir):
    """Start the HTTP server with CLI options and authentication"""
    script_dir = Path(script_dir).absolute()
    os.chdir(script_dir)
    
    # Set the auth.ini path based on script directory
    auth_ini_path = script_dir / 'auth.ini'
    MyHTTPRequestHandler.set_auth_ini_path(auth_ini_path)
    
    # Check if index.html exists
    if not (script_dir / 'index.html').exists():
        print(f"ERROR: index.html not found in {script_dir}")
        print("Please make sure index.html is in the same directory as server.py")
        sys.exit(1)
    
    # Preload authentication configuration
    users = MyHTTPRequestHandler.load_auth()
    if users:
        auth_status = f"Authentication ENABLED ({len(users)} user(s))"
    else:
        auth_status = "Authentication DISABLED (no users configured)"
    
    max_attempts = 10
    for attempt in range(max_attempts):
        try:
            with socketserver.TCPServer((ip, port),
                                        MyHTTPRequestHandler,
                                        bind_and_activate=True) as httpd:
                print("=" * 60)
                print("ACWS Web Client - HTTP Server")
                print("=" * 60)
                print(f"Serving directory: {script_dir}")
                print(f"Authentication: {auth_status}")
                print(f"Server running at: http://{ip}:{port}/")
                print(f"Open in browser: http://{ip}:{port}/index.html")
                print("=" * 60)
                print("Press Ctrl+C to stop the server")
                print("=" * 60)
                httpd.serve_forever()
        except OSError as e:
            if e.errno == 48 or "Address already in use" in str(e):
                port += 1
                if attempt < max_attempts - 1:
                    print(f"Port {port - 1} is in use, trying port {port}...")
                    continue
            raise
    print(f"ERROR: Could not find an available port after {max_attempts} attempts")
    sys.exit(1)
