import subprocess
import os
import re
import tempfile

try:
    from .validation import (
        is_valid_domain_name,
        is_valid_hostname,
        is_valid_system_username,
        is_valid_php_pm,
        is_valid_php_size,
    )
except ImportError:
    from validation import (
        is_valid_domain_name,
        is_valid_hostname,
        is_valid_system_username,
        is_valid_php_pm,
        is_valid_php_size,
    )

NGINX_SSL_HARDENING = """    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
"""


INSTALL_DIR = "/opt/limristem-web"
STATIC_DIR = os.path.join(INSTALL_DIR, "static")


def _write_text_file(path, content, mode=0o644):
    directory = os.path.dirname(path)
    os.makedirs(directory, exist_ok=True)
    fd, temp_path = tempfile.mkstemp(dir=directory, prefix=".tmp_", text=True)
    try:
        with os.fdopen(fd, "w") as handle:
            handle.write(content)
        os.chmod(temp_path, mode)
        os.replace(temp_path, path)
    except Exception:
        try:
            os.unlink(temp_path)
        except OSError:
            pass
        raise


def _stage_nginx_config(config_path, content, link_path=None):
    previous_content = None
    previous_link = None
    if os.path.exists(config_path):
        with open(config_path, "r") as handle:
            previous_content = handle.read()
    if link_path and os.path.lexists(link_path):
        previous_link = os.path.realpath(link_path)

    try:
        _write_text_file(config_path, content)
        if link_path:
            os.symlink(config_path, link_path + ".new")
            os.replace(link_path + ".new", link_path)

        test = subprocess.run(["nginx", "-t"], capture_output=True, text=True)
        if test.returncode != 0:
            raise RuntimeError(test.stderr.strip() or "nginx configuration test failed")

        subprocess.run(["systemctl", "reload", "nginx"], check=True)
        return True, "Nginx configuration applied"
    except Exception as exc:
        try:
            if previous_content is None and os.path.exists(config_path):
                os.remove(config_path)
            elif previous_content is not None:
                _write_text_file(config_path, previous_content)
            if link_path:
                if previous_link is None and os.path.lexists(link_path):
                    os.remove(link_path)
                elif previous_link is not None:
                    if os.path.lexists(link_path):
                        os.remove(link_path)
                    os.symlink(previous_link, link_path)
        except OSError:
            pass
        return False, str(exc)


def _domain_config_paths(domain):
    if not is_valid_domain_name(domain):
        raise ValueError("Invalid domain name")
    return (
        f"/etc/nginx/sites-available/{domain}",
        f"/etc/nginx/sites-enabled/{domain}",
    )

def create_system_user(username, password):
    username = (username or "").strip()
    if not is_valid_system_username(username):
        return False, "Invalid username"
    if not password:
        return False, "Password is required"

    # Using subprocess.run with list args avoids shell injection for arguments
    # 1. Create user with no shell
    cmd_user = ["useradd", "-m", "-d", f"/home/{username}", "-s", "/usr/sbin/nologin", username]
    
    # 2. Set password safely using chpasswd via stdin
    cmd_pass = ["chpasswd"]
    pass_input = f"{username}:{password}".encode()
    
    # 3. Configure permissions
    cmd_chown = ["chown", "root:root", f"/home/{username}"]
    cmd_chmod = ["chmod", "755", f"/home/{username}"]
    cmd_www = ["mkdir", "-p", f"/home/{username}/www"]
    cmd_chown_www = ["chown", f"{username}:{username}", f"/home/{username}/www"]

    # 4. Configure sshd for Chroot (Append to sshd_config if not exists)
    # Note: This is a simple append. A real production system might need more robust parsing.
    # We check if the match block exists to avoid duplicates.
    sshd_config_entry = f"\nMatch User {username}\n    ChrootDirectory /home/{username}\n    ForceCommand internal-sftp\n    AllowTcpForwarding no\n    X11Forwarding no\n"
    
    try:
        subprocess.run(cmd_user, check=True)
    except subprocess.CalledProcessError as e:
        if e.returncode == 9:
            # User exists, proceed to import
            pass 
        else:
            return False, f"User creation failed: {e}"

    try:
        # Continue with config even if user existed (ensure password/permissions are set)
        subprocess.run(cmd_pass, input=pass_input, check=True)
        subprocess.run(cmd_chown, check=True)
        subprocess.run(cmd_chmod, check=True)
        subprocess.run(cmd_www, check=True)
        subprocess.run(cmd_chown_www, check=True)
        
        # Check if user already in sshd_config
        grep_cmd = ["grep", f"Match User {username}", "/etc/ssh/sshd_config"]
        grep_res = subprocess.run(grep_cmd, capture_output=True)
        
        if grep_res.returncode != 0:
             # Append to sshd_config using tee -a
             with subprocess.Popen(["tee", "-a", "/etc/ssh/sshd_config"], stdin=subprocess.PIPE) as proc:
                 proc.communicate(input=sshd_config_entry.encode())
             
             # Restart SSH
             subprocess.run(["systemctl", "restart", "ssh"], check=True)

        return True, "User created/imported successfully"
    except subprocess.CalledProcessError as e:
        return False, f"Command failed: {e}"
    except Exception as e:
        return False, str(e)

def scan_existing_hosts(username):
    """Scans Nginx configs for hosts belonging to this user."""
    hosts = []
    if not is_valid_system_username(username):
        return hosts
    try:
        # Grep for server_name in all sites-enabled to find candidates, 
        # then check if they belong to user (either active or suspended)
        # Assuming we can't just grep root because suspended hosts point to /opt/limristem-web...
        # But we know the user. If we grep for /home/{user}, we miss suspended.
        # But if we don't know the domain, how do we find them?
        # We can scan ALL files and check ownership of the config? No, root owns config.
        # We can look for the socket path? unix:/run/php/php...-{domain}.sock
        # But suspended hosts don't have PHP block usually (or commented out).
        
        # Strategy:
        # 1. Active: grep 'root /home/{username}'
        # 2. Suspended: We need a way to link suspended host to user.
        #    - We can check if /home/{username}/www/{domain} exists?
        #    - Or rely on the fact that we name config files /etc/nginx/sites-available/{domain}
        #    - And we can check directories in /home/{username}/www/
        
        # Let's scan directories in user home!
        user_www = f"/home/{username}/www"
        if os.path.exists(user_www):
            for domain in os.listdir(user_www):
                if os.path.isdir(os.path.join(user_www, domain)) and is_valid_domain_name(domain):
                    # Check if Nginx config exists
                    config_path = f"/etc/nginx/sites-enabled/{domain}"
                    if os.path.exists(config_path):
                        # Analyze config
                        with open(config_path, 'r') as f:
                            content = f.read()
                            
                        # Detect PHP version
                        php_v = '8.2' # Default
                        m_php = re.search(r'php(\d+\.\d+)-fpm', content)
                        if m_php:
                            php_v = m_php.group(1)
                            
                        # Detect Suspension
                        is_suspended = False
                        suspension_reason = None
                        if f"{STATIC_DIR}/error_pages" in content:
                            is_suspended = True
                            if "disk_full.html" in content: suspension_reason = "disk_full"
                            elif "bandwidth.html" in content: suspension_reason = "bandwidth"
                            else: suspension_reason = "manual"
                            
                        hosts.append({
                            'domain': domain,
                            'php_version': php_v,
                            'root_dir': f"/home/{username}/www/{domain}/public",
                            'is_suspended': is_suspended,
                            'suspension_reason': suspension_reason
                        })

    except Exception as e:
        print(f"Scan error: {e}")
    return hosts

def check_nginx_host_exists(domain):
    """Checks if Nginx config for domain exists."""
    if not is_valid_domain_name(domain):
        return False
    return os.path.exists(f"/etc/nginx/sites-available/{domain}")

def get_nginx_host_details(domain):
    """Parses existing Nginx config to extract user and php version."""
    details = {'user': None, 'php_version': '8.2'} # defaults
    if not is_valid_domain_name(domain):
        return details
    config_path = f"/etc/nginx/sites-available/{domain}"
    
    if not os.path.exists(config_path):
        return details
        
    try:
        with open(config_path, 'r') as f:
            content = f.read()
            
        # Extract root to find user
        # root /home/{user}/www/{domain}/public;
        m_root = re.search(r'\s*root\s+(.*?);', content)
        if m_root:
            root_path = m_root.group(1).strip()
            # Try to extract user from standard pattern /home/{user}/...
            m_user = re.search(r'^/home/([^/]+)/', root_path)
            if m_user:
                details['user'] = m_user.group(1)
                
        # Extract PHP version
        m_php = re.search(r'php(\d+\.\d+)-fpm', content)
        if m_php:
            details['php_version'] = m_php.group(1)
            
    except Exception as e:
        print(f"Error parsing nginx config: {e}")
        
    return details

def create_php_pool(domain, php_version, user, settings=None):
    """Creates a dedicated PHP-FPM pool for the host."""
    if not is_valid_domain_name(domain):
        return False, "Invalid domain name"
    if not is_valid_system_username(user):
        return False, "Invalid system user"

    pool_name = domain.replace('.', '_')
    socket_path = f"/run/php/php{php_version}-{domain}.sock"
    config_path = f"/etc/php/{php_version}/fpm/pool.d/{domain}.conf"
    
    # Defaults
    s = {
        'pm': 'ondemand',
        'max_children': 5,
        'start_servers': 2,
        'min_spare': 1,
        'max_spare': 3,
        'memory_limit': '128M',
        'upload_max': '10M',
        'post_max': '10M',
        'max_exec': 30
    }
    if settings:
        s.update(settings)

    # Defense-in-depth: validate all settings before interpolation
    if not is_valid_php_pm(s.get('pm', 'ondemand')):
        s['pm'] = 'ondemand'
    if not is_valid_php_size(s.get('memory_limit', '128M')):
        s['memory_limit'] = '128M'
    if not is_valid_php_size(s.get('upload_max', '10M')):
        s['upload_max'] = '10M'
    if not is_valid_php_size(s.get('post_max', '10M')):
        s['post_max'] = '10M'
    try:
        s['max_children'] = int(s.get('max_children', 5))
        s['start_servers'] = int(s.get('start_servers', 2))
        s['min_spare'] = int(s.get('min_spare', 1))
        s['max_spare'] = int(s.get('max_spare', 3))
        s['max_exec'] = int(s.get('max_exec', 30))
    except (TypeError, ValueError):
        s['max_children'] = 5
        s['start_servers'] = 2
        s['min_spare'] = 1
        s['max_spare'] = 3
        s['max_exec'] = 30

    error_log_dir = f"/var/log/php{php_version}-fpm"
    error_log_path = f"{error_log_dir}/{domain}.error.log"

    config = f"""
[{pool_name}]
user = {user}
group = {user}
listen = {socket_path}
listen.owner = www-data
listen.group = www-data

pm = {s['pm']}
pm.max_children = {s['max_children']}
pm.start_servers = {s['start_servers']}
pm.min_spare_servers = {s['min_spare']}
pm.max_spare_servers = {s['max_spare']}
pm.process_idle_timeout = 10s

php_admin_value[memory_limit] = {s['memory_limit']}
php_admin_value[upload_max_filesize] = {s['upload_max']}
php_admin_value[post_max_size] = {s['post_max']}
php_admin_value[max_execution_time] = {s['max_exec']}
php_admin_flag[log_errors] = on
php_admin_value[error_log] = {error_log_path}
php_admin_value[open_basedir] = /home/{user}/www/{domain}
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
php_admin_flag[expose_php] = off
php_admin_flag[display_errors] = off
php_admin_flag[display_startup_errors] = off
php_admin_flag[allow_url_include] = off
php_admin_flag[allow_url_fopen] = off
php_admin_flag[session.cookie_secure] = on
php_admin_flag[session.cookie_httponly] = on
php_admin_value[session.cookie_samesite] = Strict
php_admin_flag[session.use_strict_mode] = on
php_admin_value[session.save_path] = /home/{user}/www/{domain}/tmp/sessions
php_admin_value[upload_tmp_dir] = /home/{user}/www/{domain}/tmp/uploads
"""
    try:
        os.makedirs(error_log_dir, exist_ok=True)
        # Create per-host tmp directories for PHP sessions and uploads
        domain_base = f"/home/{user}/www/{domain}"
        for subdir in ('tmp/sessions', 'tmp/uploads'):
            tmp_path = f"{domain_base}/{subdir}"
            os.makedirs(tmp_path, exist_ok=True)
            try:
                subprocess.run(["chown", f"{user}:{user}", tmp_path], capture_output=True, check=True)
                subprocess.run(["chmod", "700", tmp_path], capture_output=True, check=True)
            except Exception:
                pass
        # Create log file and set ownership so the pool user can write to it
        if not os.path.exists(error_log_path):
            open(error_log_path, 'a').close()
        subprocess.run(["chown", f"{user}:{user}", error_log_path], capture_output=True, check=True)
        _write_text_file(config_path, config)
        
        service_name = f"php{php_version}-fpm"
        
        # Check if service is running
        status_res = subprocess.run(["systemctl", "is-active", service_name], capture_output=True, text=True)
        
        if status_res.returncode == 0:
            # Active: Reload
            action = "reload"
            cmd = ["systemctl", "reload", service_name]
        else:
            # Inactive: Start
            action = "start"
            cmd = ["systemctl", "restart", service_name] # Restart covers start/restart needs
            
        res = subprocess.run(cmd, capture_output=True, text=True)
        
        if res.returncode != 0:
            return False, f"Failed to {action} {service_name}: {res.stderr}"
            
        return True, socket_path
    except Exception as e:
        return False, str(e)

def remove_php_pool(domain, php_version):
    if not is_valid_domain_name(domain):
        return
    config_path = f"/etc/php/{php_version}/fpm/pool.d/{domain}.conf"
    if os.path.exists(config_path):
        try:
            os.remove(config_path)
            subprocess.run(["systemctl", "reload", f"php{php_version}-fpm"], check=True)
        except Exception:
            pass

def update_host_settings(domain, old_php, new_php, user, user_home, speed_limit, traffic_limit=0, php_settings=None, conn_limit=10):
    """Updates Nginx config and PHP pool."""
    # Check if SSL is enabled
    try:
        from .ssl_manager import get_domain_ssl_status, update_nginx_ssl
    except ImportError:
        from ssl_manager import get_domain_ssl_status, update_nginx_ssl
    ssl_enabled = False
    try:
        status, _ = get_domain_ssl_status(domain)
        # Simplified check: if certificate file exists or 443 block found
        # Ideally get_domain_ssl_status returns precise bool
        # Let's assume get_domain_ssl_status returns (expiry_date_str, type) if valid, else (None, "No SSL")
        if status and status != "None":
            ssl_enabled = True
    except Exception:
        pass

    # 1. Handle PHP Version Change
    if old_php != new_php:
        remove_php_pool(domain, old_php)
    
    # 2. Ensure Pool exists (create or recreate)
    # Always recreate pool to apply settings
    success, socket_or_msg = create_php_pool(domain, new_php, user, php_settings)
    if not success:
        return False, f"Pool creation failed: {socket_or_msg}"
    
    # 3. Rewrite Nginx Config
    success, msg = create_nginx_host(domain, new_php, user_home, speed_limit_kbps=speed_limit, conn_limit=conn_limit)
    if not success:
        return False, msg

    # 4. Restore SSL if it was enabled
    if ssl_enabled:
        # Re-apply SSL configuration
        # Find certificates in standard locations
        # Let's Encrypt: /etc/letsencrypt/live/{domain}/fullchain.pem
        # Self-signed/Manual: /etc/nginx/ssl/{domain}.crt
        cert_path = f"/etc/letsencrypt/live/{domain}/fullchain.pem"
        key_path = f"/etc/letsencrypt/live/{domain}/privkey.pem"
        if not os.path.exists(cert_path):
             cert_path = f"/etc/nginx/ssl/{domain}.crt"
             key_path = f"/etc/nginx/ssl/{domain}.key"
        
        if os.path.exists(cert_path) and os.path.exists(key_path):
            try:
                update_nginx_ssl(domain, cert_path, key_path)
            except Exception as e:
                pass
    
    return True, "Host settings updated"

def suspend_host(domain, reason="suspended"):
    """
    Suspends a host by replacing its Nginx config with one pointing to an error page.
    Reason can be: 'suspended', 'disk_full', 'bandwidth'.
    """
    if not is_valid_domain_name(domain):
        return False, "Invalid domain name"

    error_page_path = f"{STATIC_DIR}/error_pages/{reason}.html"
    # Fallback if specific page missing
    if not os.path.exists(error_page_path):
        error_page_path = f"{STATIC_DIR}/error_pages/suspended.html"
        
    # We need to serve this single static file for all requests
    # Best way: try_files pointing to this file alias?
    # Or root pointing to error_pages dir and index {reason}.html?
    
    error_root = f"{STATIC_DIR}/error_pages"
    error_index = f"{reason}.html"
    if not os.path.exists(f"{error_root}/{error_index}"):
        error_index = "suspended.html"

    config = f"""
server {{
    listen 80;
    server_name {domain};
    root {error_root};
    index {error_index};
    
    error_page 404 /index.html;
    
    location / {{
        add_header Cache-Control "no-store, no-cache, must-revalidate";
        try_files $uri /{error_index};
    }}
}}
"""
    config_path, link_path = _domain_config_paths(domain)
    success, msg = _stage_nginx_config(config_path, config, link_path=link_path)
    return success, "Host suspended" if success else msg

def unsuspend_host(domain, php_version, user_home, speed_limit=0, conn_limit=10, max_body_size_mb=0, client_body_timeout=0, client_header_timeout=0, keepalive_timeout=0):
    """Restores original Nginx config."""
    # We just call create_nginx_host again to regenerate valid config
    # create_nginx_host will recreate the Nginx file and reload Nginx
    success, msg = create_nginx_host(domain, php_version, user_home, speed_limit_kbps=speed_limit, conn_limit=conn_limit, max_body_size_mb=max_body_size_mb, client_body_timeout=client_body_timeout, client_header_timeout=client_header_timeout, keepalive_timeout=keepalive_timeout)
    
    if success:
        # Check and restore SSL if certificates exist
        cert_path = f"/etc/letsencrypt/live/{domain}/fullchain.pem"
        key_path = f"/etc/letsencrypt/live/{domain}/privkey.pem"
        if not os.path.exists(cert_path):
             cert_path = f"/etc/nginx/ssl/{domain}.crt"
             key_path = f"/etc/nginx/ssl/{domain}.key"
        
        if os.path.exists(cert_path) and os.path.exists(key_path):
            try:
                try:
                    from .ssl_manager import update_nginx_ssl
                except ImportError:
                    from ssl_manager import update_nginx_ssl
                update_nginx_ssl(domain, cert_path, key_path)
            except Exception as e:
                print(f"Failed to restore SSL for {domain}: {e}")
                
    return success, msg

def delete_host_system(domain, php_version, user):
    """Deletes Nginx config, PHP pool, and logs."""
    if not is_valid_domain_name(domain):
        return False, "Invalid domain name"
    try:
        config_path, link_path = _domain_config_paths(domain)
        # Remove Nginx
        if os.path.lexists(link_path):
            os.remove(link_path)
        if os.path.exists(config_path):
            os.remove(config_path)
            
        # Remove PHP Pool
        remove_php_pool(domain, php_version)
        
        # Reload Nginx
        subprocess.run(["systemctl", "reload", "nginx"], check=True)
        
        # Remove Logs? Maybe keep for audit. User asked to "delete host".
        # Usually we keep content but delete config.
        
        return True, "Host deleted system-side"
    except Exception as e:
        return False, str(e)

def update_system_user(username, password, email, quota_mb, cpu, ram, io):
    """Updates system user limits and password."""
    if not is_valid_system_username((username or "").strip()):
        return False, "Invalid username"
    try:
        # Update Password
        if password:
            cmd_pass = ["chpasswd"]
            pass_input = f"{username}:{password}".encode()
            subprocess.run(cmd_pass, input=pass_input, check=True)
            
        # Update System Quota? Not implemented natively on FS, just DB tracking for now.
        # Limits are stored in DB and enforced by monitor/cron.
        # But we could set cgroups here if advanced. For now, DB update is handled by caller.
        return True, "User updated"
    except Exception as e:
        return False, str(e)

def suspend_system_user(username):
    """Locks user and suspends all owned hosts."""
    if not is_valid_system_username((username or "").strip()):
        return False, "Invalid username"
    try:
        subprocess.run(['usermod', '-L', username], check=True)
        return True, "User locked system-side"
    except Exception as e:
        return False, str(e)

def unsuspend_system_user(username):
    """Unlocks user."""
    if not is_valid_system_username((username or "").strip()):
        return False, "Invalid username"
    try:
        subprocess.run(['usermod', '-U', username], check=True)
        return True, "User unlocked system-side"
    except Exception as e:
        return False, str(e)

def delete_system_user_complete(username, home_dir):
    """Deletes system user and home directory."""
    if not is_valid_system_username((username or "").strip()):
        return False, "Invalid username"
    try:
        # Kill processes
        try:
            subprocess.run(['pkill', '-u', username], check=False)
        except Exception:
            pass
            
        # Userdel -r
        subprocess.run(['userdel', '-r', username], check=True)
        return True, "User deleted system-side"
    except Exception as e:
        # Fallback if userdel failed but we need to ensure home is gone
        if os.path.exists(home_dir):
             try:
                 import shutil
                 shutil.rmtree(home_dir)
             except Exception:
                 pass
        return False, str(e)

def get_nginx_config_content(domain):
    """Reads Nginx config content."""
    if not is_valid_domain_name(domain):
        raise ValueError("Invalid domain name")
    path = f"/etc/nginx/sites-available/{domain}"
    if os.path.exists(path):
        with open(path, 'r') as f:
            return f.read()
    return ""

def save_nginx_config_content(domain, content):
    """Saves manual Nginx config content."""
    if not is_valid_domain_name(domain):
        return False, "Invalid domain name"
    if not content or not content.strip():
        return False, "Configuration content is empty"
    config_path, link_path = _domain_config_paths(domain)
    success, msg = _stage_nginx_config(config_path, content, link_path=link_path)
    return success, "Config saved" if success else msg

def create_nginx_host(domain, php_version, user_home, speed_limit_kbps=0, conn_limit=10, host_type='php', template='default', is_default=False, max_body_size_mb=0, client_body_timeout=0, client_header_timeout=0, keepalive_timeout=0):
    if not is_valid_domain_name(domain):
        return False, "Invalid domain name"
    if host_type not in {'php', 'html'}:
        return False, "Invalid host type"
    if template not in {'default', 'wordpress', 'symfony', 'laravel'}:
        return False, "Invalid template"
    if not user_home.startswith("/home/"):
        return False, "Invalid user home directory"

    # Define root directory structure: /home/{user}/www/{domain}/public
    # user_home comes in as /home/{user}
    web_root = f"{user_home}/www/{domain}/public"

    # Ensure directories exist
    try:
        # Create full path
        cmd_mkdir = ["mkdir", "-p", web_root]
        subprocess.run(cmd_mkdir, check=True)
        
        # Set permissions
        # We need to know the username. inferred from user_home basename?
        # user_home is /home/username
        username = os.path.basename(user_home)
        if not is_valid_system_username(username):
            return False, "Invalid system user"
        
        # recursive chown for the domain dir downwards
        domain_dir = f"{user_home}/www/{domain}"
        cmd_chown = ["chown", "-R", f"{username}:{username}", domain_dir]
        subprocess.run(cmd_chown, check=True)
        
        # chmod? 755 is standard
        cmd_chmod = ["chmod", "-R", "755", domain_dir]
        subprocess.run(cmd_chmod, check=True)

        # Create per-host tmp directories for PHP sessions and uploads (security isolation)
        for subdir in ('tmp/sessions', 'tmp/uploads'):
            tmp_path = f"{domain_dir}/{subdir}"
            subprocess.run(["mkdir", "-p", tmp_path], check=True)
            subprocess.run(["chown", f"{username}:{username}", tmp_path], check=True)
            subprocess.run(["chmod", "700", tmp_path], check=True)

        # Copy courtesy page
        # Assuming app is running from INSTALL_DIR or we can find static path.
        # Simplest is to assume standard install path or relative.
        # We are in limristem_web/user_host_manager.py, so static is ../static/courtesy_page/index.html
        # But running as package...
        try:
            # Try absolute path first (prod)
            courtesy_src = os.path.join(STATIC_DIR, "courtesy_page/index.html")
            if not os.path.exists(courtesy_src):
                # Try relative (dev)
                courtesy_src = os.path.join(os.path.dirname(__file__), "static/courtesy_page/index.html")
            
            if os.path.exists(courtesy_src):
                import shutil
                shutil.copy(courtesy_src, f"{web_root}/index.html")
                # Fix ownership of index.html
                subprocess.run(["chown", f"{username}:{username}", f"{web_root}/index.html"], check=True)
        except Exception as e:
            print(f"Failed to copy courtesy page: {e}")
        
    except Exception as e:
        return False, f"Directory creation failed: {e}"

    limit_directive = ""
    if int(speed_limit_kbps) > 0:
        limit_directive = f"limit_rate {speed_limit_kbps}k;"
        
    conn_limit_directive = ""
    if int(conn_limit) > 0:
        # Requires limit_conn_zone defined in global config
        conn_limit_directive = f"limit_conn addr {conn_limit};"

    log_path = f"/var/log/nginx/{domain}.access.log"
    # Use dedicated socket if available (standard naming we defined)
    # If not using dedicated pool function, this might fail, so we should rely on consistent usage.
    # We will assume every host creation now implies a dedicated pool.
    socket_path = f"unix:/run/php/php{php_version}-{domain}.sock"

    # Template Logic
    try_files_str = "$uri $uri/ =404"
    if template == 'wordpress':
        try_files_str = "$uri $uri/ /index.php?$args"
    elif template == 'symfony' or template == 'laravel':
        try_files_str = "$uri /index.php$is_args$args"

    # Sanitize max body size for nginx client_max_body_size (0 = default 10MB)
    try:
        s_limit_mb = int(max_body_size_mb) if max_body_size_mb and int(max_body_size_mb) > 0 else 10
        s_limit_mb = min(s_limit_mb, 1024)
    except (TypeError, ValueError):
        s_limit_mb = 10

    # Timeout hardening: 0 = use secure defaults
    try:
        s_body_timeout = int(client_body_timeout) if client_body_timeout and int(client_body_timeout) > 0 else 10
    except (TypeError, ValueError):
        s_body_timeout = 10
    try:
        s_header_timeout = int(client_header_timeout) if client_header_timeout and int(client_header_timeout) > 0 else 10
    except (TypeError, ValueError):
        s_header_timeout = 10
    try:
        s_keepalive_timeout = int(keepalive_timeout) if keepalive_timeout and int(keepalive_timeout) > 0 else 15
    except (TypeError, ValueError):
        s_keepalive_timeout = 15
        
    php_block = ""
    if host_type == 'php':
        php_block = f"""
    location ~ \\.php$ {{
        include snippets/fastcgi-php.conf;
        fastcgi_pass {socket_path};
    }}"""

    proxy_block = ""
    if is_default:
        proxy_loc_file = f"{INSTALL_DIR}/instance/panel_proxy_location.conf"
        if os.path.exists(proxy_loc_file):
            proxy_block = f"    include {proxy_loc_file};"

    listen_str = "listen 80 default_server;" if is_default else "listen 80;"
    config = f"""
server {{
    {listen_str}
    server_name {domain};
    root {web_root};
    index index.php index.html;
    server_tokens off;
    
    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    
    # Timeout hardening (mitigates Slowloris and DoS)
    client_body_timeout {s_body_timeout}s;
    client_header_timeout {s_header_timeout}s;
    keepalive_timeout {s_keepalive_timeout}s;
    send_timeout 10s;
    
    # Limit payload size
    client_max_body_size {s_limit_mb}m;
    
    # Flush logs every 5 seconds to ensure bandwidth stats are updated near real-time
    access_log {log_path} combined buffer=4k flush=5s;
    error_log /var/log/nginx/{domain}.error.log;
    
    {limit_directive}
    {conn_limit_directive}
    
    # Custom 503 for Connection Limit
    error_page 503 @conn_limit;
    location @conn_limit {{
        root {STATIC_DIR}/error_pages;
        rewrite ^ /connection_limit.html break;
    }}

    {proxy_block}

    # Only allow necessary HTTP methods
    if ($request_method !~ ^(GET|HEAD|POST)$ ) {{
        return 405;
    }}

    location / {{
        try_files {try_files_str};
    }}
    {php_block}
}}
"""
    config_path, link_path = _domain_config_paths(domain)
    success, msg = _stage_nginx_config(config_path, config, link_path=link_path)
    return success, "Host created and Nginx reloaded" if success else msg

def update_panel_config(limit_mb, timeout_seconds=300):
    """Updates the Limristem Web panel Nginx config and Gunicorn Service."""
    config_path = f"{INSTALL_DIR}/instance/panel_nginx.conf"
    
    # Verify directory exists
    if not os.path.exists(os.path.dirname(config_path)):
        # If instance dir missing for some reason
        return False, "Instance directory missing"
        
    try:
        # 1. Update Nginx Config
        config_content = f"""
client_max_body_size {limit_mb}M;
proxy_read_timeout {timeout_seconds}s;
proxy_connect_timeout {timeout_seconds}s;
proxy_send_timeout {timeout_seconds}s;
"""
        previous_content = None
        if os.path.exists(config_path):
            with open(config_path, "r") as handle:
                previous_content = handle.read()
        _write_text_file(config_path, config_content)
        
        # Test and Reload Nginx
        test = subprocess.run(["nginx", "-t"], capture_output=True, text=True)
        if test.returncode != 0:
            if previous_content is not None:
                _write_text_file(config_path, previous_content)
            return False, f"Nginx Config Error: {test.stderr}"
        subprocess.run(["systemctl", "reload", "nginx"], check=True)
        
        # 2. Update Systemd Service (Gunicorn Timeout)
        service_path = "/etc/systemd/system/limristem-web.service"
        if os.path.exists(service_path):
            with open(service_path, "r") as f:
                content = f.read()
            
            # Use regex to find ExecStart and update --timeout
            # Standard ExecStart: .../gunicorn ... --timeout 123 ... or missing
            # Simple approach: If --timeout exists, replace value. If not, append to end of command (before quote/newline if needed)
            
            # Assuming simple structure from install.sh
            # ExecStart=$INSTALL_DIR/venv/bin/gunicorn -w 1 -b 127.0.0.1:4317 'app:create_app()'
            
            new_content = content
            if "--timeout" in content:
                # Replace existing value
                new_content = re.sub(r'--timeout\s+\d+', f'--timeout {timeout_seconds}', content)
            else:
                # Append to gunicorn command
                # Look for 'gunicorn' line
                if "gunicorn" in content:
                    # Insert before the last quote or end of line?
                    # The command usually ends with 'app:create_app()'
                    # Let's replace 'app:create_app()' with '--timeout {sec} app:create_app()'
                    new_content = content.replace("'app:create_app()'", f"--timeout {timeout_seconds} 'app:create_app()'")
            
            if new_content != content:
                with open(service_path, "w") as f:
                    f.write(new_content)
                
                subprocess.run(["systemctl", "daemon-reload"], check=True)
                # Restart is required for Gunicorn timeout to take effect
                # We do it async or just do it?
                # Restarting immediately kills the current request!
                # We'll rely on Nginx update primarily for now, but perform restart.
                # The browser will likely see a 502 or connection reset, which confirms reload.
                # Use Popen to detach and sleep to allow response to flush
                subprocess.Popen(["bash", "-c", "sleep 2; systemctl restart limristem-web"], start_new_session=True)

        return True, "Panel limits and timeouts updated"
    except Exception as e:
        return False, str(e)

def update_system_hostname(new_hostname):
    """Updates the system hostname."""
    new_hostname = (new_hostname or "").strip()
    if not is_valid_hostname(new_hostname):
        return False, "Invalid hostname"
    try:
        # 1. hostnamectl
        subprocess.run(["hostnamectl", "set-hostname", new_hostname], check=True)
        
        # 2. Update /etc/hosts — preserve all existing entries, only update/add the hostname line
        with open("/etc/hosts", "r") as f:
            lines = f.readlines()
            
        new_lines = []
        updated = False
        for line in lines:
            stripped = line.strip()
            # Update 127.0.0.1 lines that have the old hostname (not pure localhost lines)
            if stripped.startswith("127.0.0.1") and "localhost" not in stripped:
                parts = stripped.split()
                # If this line has more than one alias and one of them looks like a hostname
                # (not just 127.0.0.1), replace the non-IP alias with new hostname
                if len(parts) >= 2:
                    new_parts = ["127.0.0.1"]
                    replaced = False
                    for p in parts[1:]:
                        if not replaced and p != "localhost":
                            new_parts.append(new_hostname)
                            replaced = True
                        else:
                            new_parts.append(p)
                    if replaced:
                        new_lines.append(" ".join(new_parts) + "\n")
                        updated = True
                        continue
            new_lines.append(line)
        if not updated:
            new_lines.append(f"127.0.0.1 {new_hostname}\n")
        _write_text_file("/etc/hosts", "".join(new_lines))
                    
        return True, "System hostname updated"
    except Exception as e:
        return False, str(e)

def regenerate_panel_nginx_config(hostname, ssl_type='self', proxy_path=None, install_dir="/opt/limristem-web"):
    """
    Regenerates the panel Nginx configuration.
    Mimics the logic in install.sh.
    """
    hostname = (hostname or "").strip()
    if not is_valid_hostname(hostname):
        return False, "Invalid hostname"
    try:
        # 1. Update the dedicated panel listener (port 4316)
        # Assuming SSL certs exist at standard location
        # If ssl_type is manual/self, we use panel.crt/key
        # If letsencrypt, we might use live paths, but initially we set up for 4316 using internal certs usually.
        # Wait, user wants LE for panel. If using LE, certbot manages the config modification mostly.
        # But we need to set the server_name first.
        
        panel_config = f"""
server {{
    listen 8016;
    server_name {hostname};
    return 301 https://$host:4316$request_uri;
}}

server {{
    listen 4316 ssl;
    server_name {hostname};
    server_tokens off;
    
    ssl_certificate {install_dir}/instance/panel.crt;
    ssl_certificate_key {install_dir}/instance/panel.key;
{NGINX_SSL_HARDENING}
    include {install_dir}/instance/panel_nginx.conf;
    
    location / {{
        proxy_pass http://127.0.0.1:4317;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }}
}}
"""
        success, msg = _stage_nginx_config(
            "/etc/nginx/sites-available/limristem-web-panel",
            panel_config,
            link_path="/etc/nginx/sites-enabled/limristem-web-panel",
        )
        if not success:
            return False, msg
            
        # 2. Update Proxy (Port 80/443) if enabled (check if file exists or proxy_path provided)
        proxy_file = "/etc/nginx/sites-available/limristem-web"
        if os.path.exists(proxy_file):
            # Extract existing proxy path if not provided
            if not proxy_path:
                with open(proxy_file, 'r') as f:
                    m = re.search(r'location\s+(/[^ ]+)\s+{', f.read())
                    if m:
                        proxy_path = m.group(1)
            
            if not proxy_path: proxy_path = "/panel" # Fallback
            
            proxy_config = f"""
server {{
    listen 80;
    server_name {hostname};
    return 301 https://$host$request_uri;
}}

server {{
    listen 443 ssl;
    server_name {hostname};
    server_tokens off;

    ssl_certificate {install_dir}/instance/panel.crt;
    ssl_certificate_key {install_dir}/instance/panel.key;
{NGINX_SSL_HARDENING}

    # Panel Proxy
    location {proxy_path} {{
        include {install_dir}/instance/panel_nginx.conf;
        
        proxy_pass http://127.0.0.1:4317/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Prefix {proxy_path};
    }}

    # Default Welcome Page
    location / {{
        root {install_dir}/static/default_page;
        index index.html;
    }}
}}
"""
            success, msg = _stage_nginx_config(
                proxy_file,
                proxy_config,
                link_path="/etc/nginx/sites-enabled/limristem-web",
            )
            if not success:
                return False, msg

        return True, "Nginx config regenerated with new hostname"
    except Exception as e:
        return False, str(e)
