"""
Webapp Server - Serves the React webapp with integrated WebSocket backend.

This module provides a simple HTTP server for the React static files
and starts the WebSocket server for API communication.
"""

import asyncio
import logging
import os
import threading
import time
import webbrowser
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from functools import partial
from typing import Optional, Callable
from subprocess import Popen

from pwa_launcher import open_pwa, ChromiumNotFoundError

from lanscape.ui.react_proxy.manager import WebappManager
from lanscape.ui.react_proxy.version_compat import is_version_compatible
from lanscape.ui.ws.server import WebSocketServer

log = logging.getLogger('WebappServer')


class SPAHandler(SimpleHTTPRequestHandler):
    """
    HTTP handler for Single Page Application.

    Serves static files and falls back to index.html for client-side routing.
    """

    def __init__(self, *args, directory: str = None, **kwargs):
        self.spa_directory = directory
        super().__init__(*args, directory=directory, **kwargs)

    def do_GET(self):
        """Handle GET requests with SPA fallback."""
        # Get the requested file path
        path = self.translate_path(self.path)

        # If the path doesn't exist and isn't a file request, serve index.html
        if not os.path.exists(path) and not self.path.startswith('/assets'):
            self.path = '/index.html'  # pylint: disable=attribute-defined-outside-init

        return super().do_GET()

    def log_message(self, fmt, *args):  # pylint: disable=arguments-renamed,arguments-differ
        """Override to use our logger instead of stderr."""
        log.debug(f'{self.address_string()} - {fmt % args}')

    def end_headers(self):
        """Add CORS headers for local development."""
        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 start_static_server(directory: Path, port: int, host: str = '127.0.0.1') -> HTTPServer:
    """
    Start the static file HTTP server.

    Args:
        directory: Directory containing static files to serve
        port: Port to bind to
        host: Host to bind to (default: 127.0.0.1)

    Returns:
        The HTTPServer instance
    """
    handler = partial(SPAHandler, directory=str(directory))
    server = HTTPServer((host, port), handler)
    log.info(f'Static server starting on http://{host}:{port}')
    return server


class WebappServerController:
    """
    Controller for the webapp server with auto-shutdown support.

    Monitors WebSocket connections and shuts down when all clients disconnect
    (unless persistent mode is enabled).
    """

    def __init__(
        self,
        http_port: int = 5001,
        ws_port: int = 8766,
        host: str = '127.0.0.1',
        persistent: bool = False
    ):
        """
        Initialize the webapp server controller.

        Args:
            http_port: Port for HTTP static file server
            ws_port: Port for WebSocket server
            host: Host to bind to
            persistent: If True, don't auto-shutdown when clients disconnect
        """
        self.http_port = http_port
        self.ws_port = ws_port
        self.host = host
        self.persistent = persistent

        self._http_server: Optional[HTTPServer] = None
        self._ws_server: Optional[WebSocketServer] = None
        self._shutdown_event = threading.Event()
        self._had_connection = False
        self._ws_loop: Optional[asyncio.AbstractEventLoop] = None

    def _run_ws_server(self, on_client_change: Callable[[int], None]) -> None:
        """
        Run the WebSocket server in its own event loop.

        Args:
            on_client_change: Callback when client count changes
        """
        self._ws_loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self._ws_loop)

        self._ws_server = WebSocketServer(
            host='0.0.0.0',
            port=self.ws_port,
            on_client_change=on_client_change
        )

        try:
            self._ws_loop.run_until_complete(self._ws_server.serve_forever())
        except asyncio.CancelledError:
            pass
        finally:
            self._ws_loop.close()

    def _on_client_change(self, client_count: int) -> None:
        """
        Handle WebSocket client count changes.

        Args:
            client_count: Current number of connected clients
        """
        log.debug(f'WebSocket clients: {client_count}')

        if client_count > 0:
            self._had_connection = True
        elif self._had_connection and not self.persistent:
            # Had connections before, now zero - time to shutdown
            log.info('All clients disconnected, shutting down...')
            self._shutdown_event.set()

    def start(
        self,
        webapp_dir: Path,
        open_browser: bool = True
    ) -> None:
        """
        Start the webapp server.

        Args:
            webapp_dir: Directory containing the webapp static files
            open_browser: Whether to open a browser window
        """
        # Start WebSocket server in a thread
        ws_thread = threading.Thread(
            target=self._run_ws_server,
            args=(self._on_client_change,),
            daemon=True
        )
        ws_thread.start()
        log.info(f'WebSocket server started on ws://{self.host}:{self.ws_port}')

        # Start HTTP server
        self._http_server = start_static_server(webapp_dir, self.http_port, '0.0.0.0')

        url = f'http://{self.host}:{self.http_port}?ws-server=localhost:{self.ws_port}'

        # Open browser
        if open_browser:
            threading.Thread(
                target=_open_browser,
                args=(url,),
                daemon=True
            ).start()

        log.info(f'Webapp available at {url}')
        if self.persistent:
            log.info('Running in persistent mode. Press Ctrl+C to stop.')
        else:
            log.info('Will shutdown when all clients disconnect. Press Ctrl+C to stop.')

        # Run HTTP server with shutdown check
        self._http_server.timeout = 1.0  # Check shutdown every second
        try:
            while not self._shutdown_event.is_set():
                self._http_server.handle_request()
        except KeyboardInterrupt:
            log.info('Interrupted by user')
        finally:
            self._shutdown()

    def _shutdown(self) -> None:
        """Shutdown all servers."""
        log.info('Shutting down webapp server...')

        # Stop HTTP server
        if self._http_server:
            self._http_server.server_close()

        # Stop WebSocket server
        if self._ws_server and self._ws_loop:
            # Schedule stop on the WS event loop
            asyncio.run_coroutine_threadsafe(
                self._ws_server.stop(),
                self._ws_loop
            )

        log.info('Webapp server stopped')


# pylint: disable=too-many-arguments,too-many-positional-arguments
def start_webapp_server(
    http_port: int = 5001,
    ws_port: int = 8766,
    host: str = '127.0.0.1',
    open_browser: bool = True,
    force_download: bool = False,
    persistent: bool = False
) -> None:
    """
    Start the webapp server with both HTTP (static files) and WebSocket (API).

    This is the main entry point for running the React webapp with Python backend.

    Args:
        http_port: Port for the HTTP static file server (default: 5001)
        ws_port: Port for the WebSocket server (default: 8766)
        host: Host to bind to (default: 127.0.0.1)
        open_browser: Whether to open a browser window (default: True)
        force_download: Force re-download of webapp even if cached (default: False)
        persistent: Don't auto-shutdown when clients disconnect (default: False)
    """
    # Ensure webapp is available
    manager = WebappManager()

    webapp_dir = manager.get_webapp_dir()

    # Check if we need to download (not cached, force download, or incompatible cached version)
    need_download = force_download or not manager.is_cached()

    if not need_download:
        # Check if cached version is compatible
        cached_version = manager.get_cached_version()
        if cached_version and not is_version_compatible(cached_version):
            log.warning(
                f'Cached webapp v{cached_version} is not compatible with this backend. '
                'Downloading compatible version...'
            )
            need_download = True

    if need_download:
        log.info('Downloading webapp...')
        if not manager.download_webapp(force=force_download):
            # Download failed - check if we can fall back to cached version
            if manager.is_cached():
                cached_version = manager.get_cached_version()
                log.warning(
                    f'Download failed. Falling back to cached webapp v{cached_version} '
                    '(may have compatibility issues)'
                )
            else:
                raise RuntimeError(
                    'Failed to download webapp and no cached version available. '
                    'Check your internet connection.'
                )
    else:
        info = manager.get_info()
        if info:
            log.info(f'Using cached webapp v{info.version}')
            # Check for updates in background (for next run)
            threading.Thread(
                target=_check_for_update_background,
                args=(manager,),
                daemon=True
            ).start()

    if not webapp_dir.exists():
        raise RuntimeError(f'Webapp directory not found: {webapp_dir}')

    # Create and start the controller
    controller = WebappServerController(
        http_port=http_port,
        ws_port=ws_port,
        host=host,
        persistent=persistent
    )
    controller.start(webapp_dir, open_browser=open_browser)


def _check_for_update_background(manager: WebappManager) -> None:
    """
    Check for webapp updates in background.

    Downloads a new version if available, for use on next startup.

    Args:
        manager: WebappManager instance
    """
    try:
        if manager.is_update_available():
            log.info('Webapp update available, downloading for next run...')
            manager.download_webapp(force=True)
    except Exception as e:  # pylint: disable=broad-exception-caught
        log.debug(f'Background update check failed: {e}')


def _open_browser(url: str, wait: float = 1.5) -> Optional[Popen]:
    """
    Open a browser window to the specified URL.

    Args:
        url: URL to open
        wait: Seconds to wait before opening (default: 1.5)

    Returns:
        Popen instance if PWA was opened, None otherwise
    """
    time.sleep(wait)
    try:
        log.info(f'Opening browser: {url}')
        return open_pwa(url, auto_profile=False)
    except ChromiumNotFoundError:
        success = webbrowser.open(url)
        if success:
            log.warning('Chromium not found, using default browser')
        else:
            log.warning(f'Could not open browser. Webapp running at {url}')
    except Exception as e:
        log.debug(f'Browser open failed: {e}')
        log.info(f'Open your browser to: {url}')
    return None
