#!/usr/bin/env python3
"""
Development HTTP server with cache-busting headers
Supports start, status, and stop commands for process management.
"""

import argparse
import os
import signal
import socketserver
import sys
import time
import urllib.request
import urllib.error
from http.server import SimpleHTTPRequestHandler
from pathlib import Path

class NoCacheHTTPRequestHandler(SimpleHTTPRequestHandler):
    def end_headers(self):
        self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
        self.send_header('Pragma', 'no-cache')
        self.send_header('Expires', '0')
        super().end_headers()

class DevServer:
    def __init__(self, port=8000, directory=None):
        self.port = port
        self.directory = directory or os.getcwd()
        self.runtime_dir = self._get_runtime_dir()
        # Port-specific PID files to allow multiple servers
        self.pidfile = self.runtime_dir / f"zddc-dev-server-{port}.pid"
    
    def _get_runtime_dir(self):
        """Get appropriate runtime directory for PID files"""
        # Try user runtime directory first (systemd)
        import getpass
        uid = os.getuid()
        user_runtime = Path(f"/run/user/{uid}")
        if user_runtime.exists() and user_runtime.is_dir():
            runtime_dir = user_runtime / "zddc"
            runtime_dir.mkdir(exist_ok=True)
            return runtime_dir
        
        # Fall back to user's cache directory
        home = Path.home()
        cache_dir = home / ".cache" / "zddc"
        cache_dir.mkdir(parents=True, exist_ok=True)
        return cache_dir
    
    def get_server_pid(self):
        """Get the PID of running server from pidfile"""
        pid, _ = self._get_server_info()
        return pid
    
    def _get_server_info(self):
        """Get PID and serving directory from pidfile"""
        if not self.pidfile.exists():
            return None, None
        
        try:
            with open(self.pidfile, 'r') as f:
                content = f.read().strip()
            
            # Handle different formats: PID, PID:PORT, PID:DIRECTORY
            if ':' in content:
                pid_str, rest = content.split(':', 1)
                pid = int(pid_str)
                # If rest is numeric, it's old PID:PORT format
                try:
                    int(rest)
                    serving_dir = None  # Old format, directory unknown
                except ValueError:
                    serving_dir = rest  # New PID:DIRECTORY format
            else:
                pid = int(content)
                serving_dir = None  # Old format, directory unknown
            
            # Check if process is actually running using /proc
            if self._is_process_running(pid):
                return pid, serving_dir
            else:
                # Clean up stale pidfile
                print(f"  Cleaning up stale PID file for process {pid}")
                self._cleanup()
                return None, None
            
        except (ValueError, IOError) as e:
            print(f"  Error reading PID file: {e}")
            self._cleanup()
            return None, None
    
    def _is_process_running(self, pid):
        """Check if a process with given PID is running and is our process"""
        try:
            # Check if process exists
            with open(f"/proc/{pid}/comm", 'r') as f:
                comm = f.read().strip()
            
            # Verify it's a python process
            if 'python' not in comm:
                return False
            
            # Check command line to verify it's our dev-server
            try:
                with open(f"/proc/{pid}/cmdline", 'r') as f:
                    cmdline = f.read()
                    return 'dev-server.py' in cmdline
            except (FileNotFoundError, PermissionError):
                # If we can't read cmdline, assume it's our process if it's python
                return True
                
        except (FileNotFoundError, PermissionError):
            return False
    
    def is_server_running(self):
        """Check if server is running by making HTTP request"""
        try:
            with urllib.request.urlopen(f"http://localhost:{self.port}/", timeout=2) as response:
                return response.status == 200 or response.status == 403  # 403 for directory listing disabled
        except (urllib.error.URLError, urllib.error.HTTPError, OSError):
            return False
    
    def start(self, daemon=False):
        """Start the development server"""
        # Check if server is already running on this port
        existing_pid = self.get_server_pid()
        if existing_pid:
            print(f"Server is already running (PID: {existing_pid}) at http://localhost:{self.port}")
            return True  # Exit without error as requested
        
        # Check if port is in use by another process
        if self.is_server_running():
            print(f"Port {self.port} is already in use by another process")
            return False
        
        if daemon:
            self._start_daemon()
        else:
            self._start_foreground()
        
        return True
    
    def _start_foreground(self):
        """Start server in foreground mode"""
        print(f"Starting dev server on port {self.port}...")
        print(f"Serving directory: {self.directory}")
        print("Press Ctrl+C to stop")
        
        # Change to the specified directory
        try:
            os.chdir(self.directory)
        except OSError as e:
            print(f"Failed to change to directory {self.directory}: {e}")
            return False
        
        # Set up signal handler for graceful shutdown
        def signal_handler(signum, frame):
            print("\nShutting down server...")
            sys.exit(0)
        
        signal.signal(signal.SIGINT, signal_handler)
        signal.signal(signal.SIGTERM, signal_handler)
        
        try:
            httpd = socketserver.TCPServer(("", self.port), NoCacheHTTPRequestHandler)
            httpd.serve_forever()
        except OSError as e:
            if e.errno == 98:  # Address already in use
                print(f"Port {self.port} is already in use")
            else:
                print(f"Error starting server: {e}")
            return False
        except KeyboardInterrupt:
            print("\nServer stopped")
            return True
        except Exception as e:
            print(f"Error starting server: {e}")
        finally:
            self._cleanup()
    
    def _start_daemon(self):
        """Start server in daemon mode (background)"""
        try:
            # Fork the first time (detach from parent)
            pid = os.fork()
            if pid > 0:
                # Parent process - wait a moment then exit
                time.sleep(0.5)
                return True
        except OSError as e:
            print(f"Fork #1 failed: {e}")
            return False
        
        # Change to the specified directory before daemonizing
        try:
            os.chdir(self.directory)
        except OSError as e:
            print(f"Failed to change to directory {self.directory}: {e}")
            return False
        
        # Decouple from parent environment
        os.setsid()
        os.umask(0)
        
        # Fork the second time (prevent zombie processes)
        try:
            pid = os.fork()
            if pid > 0:
                # Parent process - exit immediately
                os._exit(0)
        except OSError as e:
            print(f"Fork #2 failed: {e}")
            os._exit(1)
        
        # Redirect standard file descriptors
        sys.stdout.flush()
        sys.stderr.flush()
        
        with open('/dev/null', 'r') as si:
            os.dup2(si.fileno(), sys.stdin.fileno())
        with open('/dev/null', 'w') as so:
            os.dup2(so.fileno(), sys.stdout.fileno())
        with open('/dev/null', 'w') as se:
            os.dup2(se.fileno(), sys.stderr.fileno())
        
        # Write PID to file with directory information
        try:
            with open(self.pidfile, 'w') as f:
                f.write(f"{os.getpid()}:{self.directory}")
            print(f"Dev server started on port {self.port} serving {self.directory}")
            return True
        except IOError as e:
            print(f"Failed to write PID file: {e}")
            return False
        
        # Start the server
        try:
            with socketserver.TCPServer(("", self.port), NoCacheHTTPRequestHandler) as httpd:
                httpd.serve_forever()
        except Exception:
            # In daemon mode, errors just cause the process to exit
            pass
        finally:
            self._cleanup()
    
    def status(self):
        """Check server status by testing HTTP connection"""
        # Check for stale PID files first
        self._cleanup_if_stale()
        
        pid, serving_dir = self._get_server_info()
        if pid:
            # Test if the server is actually responding on its port
            server_responding = self.is_server_running()
            
            print(f"Dev server is running")
            print(f"  PID: {pid}")
            print(f"  Port: {self.port}")
            print(f"  URL: http://localhost:{self.port}")
            print(f"  Directory: {serving_dir or 'Unknown (old PID file format)'}")
            print(f"  PID file: {self.pidfile}")
            
            if server_responding:
                print(f"  Status: Responding to HTTP requests")
            else:
                print(f"  Status: Process running but not responding to HTTP (may be starting up)")
            
            # Get process uptime
            try:
                uptime_seconds = self._get_process_uptime(pid)
                print(f"  Uptime: {self._format_uptime(uptime_seconds)}")
            except (FileNotFoundError, IndexError, ValueError):
                print(f"  Uptime: Unable to determine")
            
            return True
        else:
            print("Dev server is not running")
            return False
    
    def _get_process_uptime(self, pid):
        """Get the actual uptime of a process in seconds"""
        with open(f"/proc/{pid}/stat", 'r') as f:
            stat_data = f.read().split()
            starttime_ticks = int(stat_data[21])  # Process start time in ticks since boot
        
        # Get system clock ticks per second
        clock_ticks = os.sysconf(os.sysconf_names['SC_CLK_TCK'])
        
        # Get system boot time
        with open("/proc/stat", 'r') as f:
            for line in f:
                if line.startswith('btime '):
                    boot_time = int(line.split()[1])
                    break
        
        # Calculate process start time in seconds since epoch
        process_start_time = boot_time + (starttime_ticks / clock_ticks)
        
        # Calculate uptime
        return time.time() - process_start_time
    

    
    def _cleanup_if_stale(self):
        """Check for and clean up stale PID files"""
        if self.pidfile.exists():
            try:
                with open(self.pidfile, 'r') as f:
                    content = f.read().strip()
                
                # Handle different formats: PID, PID:PORT, PID:DIRECTORY
                if ':' in content:
                    pid = int(content.split(':', 1)[0])
                else:
                    pid = int(content)
                
                if not self._is_process_running(pid):
                    print(f"  Found stale PID file for process {pid}, cleaning up")
                    self._cleanup()
            except (ValueError, IOError):
                print(f"  Found corrupted PID file, cleaning up")
                self._cleanup()
    
    def stop(self):
        """Stop the development server"""
        pid = self.get_server_pid()
        if not pid:
            if self.is_server_running():
                print("Server is running but not managed by this script")
                print("Cannot stop server started by another process")
                return False
            else:
                print("Dev server is not running")
                return False
        
        try:
            print(f"Stopping dev server (PID: {pid})...")
            os.kill(pid, signal.SIGTERM)
            
            # Wait for process to stop
            for i in range(30):  # Wait up to 3 seconds
                time.sleep(0.1)
                try:
                    os.kill(pid, 0)  # Test if process still exists
                except OSError:
                    # Process no longer exists
                    break
            else:
                # Force kill if still running
                print("Process didn't stop gracefully, force killing...")
                try:
                    os.kill(pid, signal.SIGKILL)
                except OSError:
                    pass  # Process might have died already
            
            self._cleanup()
            print("Dev server stopped")
            return True
            
        except OSError as e:
            print(f"Error stopping server: {e}")
            self._cleanup()
            return False
    
    def _cleanup(self):
        """Clean up PID file"""
        if self.pidfile.exists():
            try:
                self.pidfile.unlink()
            except OSError as e:
                print(f"Warning: Could not remove PID file {self.pidfile}: {e}")
    
    def _format_uptime(self, seconds):
        """Format uptime in human readable format"""
        if seconds < 60:
            return f"{int(seconds)}s"
        elif seconds < 3600:
            return f"{int(seconds // 60)}m {int(seconds % 60)}s"
        else:
            hours = int(seconds // 3600)
            minutes = int((seconds % 3600) // 60)
            return f"{hours}h {minutes}m"

def main():
    parser = argparse.ArgumentParser(
        description="ZDDC Development Server",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""Commands:
  start     Start the development server (default)
  status    Show server status
  stop      Stop the development server

Examples:
  %(prog)s                # Start server in foreground (default)
  %(prog)s start -d       # Start server in background
  %(prog)s status         # Check if server is running
  %(prog)s stop           # Stop the server
  %(prog)s -p 8080 start  # Start on port 8080
  %(prog)s start ~/docs   # Start serving ~/docs directory
  %(prog)s -p 9000 ~/src  # Start serving ~/src on port 9000"""
    )
    
    parser.add_argument('command', nargs='?', default='start',
                       choices=['start', 'status', 'stop'],
                       help='Command to execute (default: start)')
    parser.add_argument('-p', '--port', type=int, default=8000,
                       help='Port to run server on (default: 8000)')
    parser.add_argument('-d', '--daemon', action='store_true',
                       help='Run server in background (daemon mode)')
    parser.add_argument('directory', nargs='?', default=None,
                       help='Directory to serve (default: current directory)')
    
    args = parser.parse_args()
    
    server = DevServer(port=args.port, directory=args.directory)
    
    if args.command == 'start':
        if not server.start(daemon=args.daemon):
            sys.exit(1)
        # After starting (or if already running), show status
        if not server.status():
            sys.exit(1)
    elif args.command == 'status':
        if not server.status():
            sys.exit(1)
    elif args.command == 'stop':
        if not server.stop():
            sys.exit(1)

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nOperation cancelled")
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)
