"""Main entry point for the LANscape application when running as a module."""
import socket

import time
import logging
import traceback
import os
from subprocess import Popen
import webbrowser
import requests

from pwa_launcher import open_pwa, ChromiumNotFoundError


from lanscape.core.logger import configure_logging
from lanscape.core.runtime_args import parse_args, was_port_explicit, was_ws_port_explicit
from lanscape.core.version_manager import get_installed_version, is_update_available
from lanscape.ui.app import start_webserver_daemon, start_webserver
from lanscape.ui.ws.server import run_server
# do this so any logs generated on import are displayed
args = parse_args()
configure_logging(args.loglevel, args.logfile, args.flask_logging)


log = logging.getLogger('core')
# determine if the execution is an instance of a flask reload
# happens on file change with reloader enabled
IS_FLASK_RELOAD = os.environ.get("WERKZEUG_RUN_MAIN")


def main():
    """core entry point for running lanscape as a module."""
    try:
        _main()
    except KeyboardInterrupt:
        log.info('Keyboard interrupt received, terminating...')
        terminate()
    except Exception as e:
        log.critical(f'Unexpected error: {e}')
        log.debug(traceback.format_exc())
        terminate()


def _main():
    if not IS_FLASK_RELOAD:
        log.info(f'LANscape v{get_installed_version()}')
        try_check_update()

    else:
        log.info('Flask reloaded app.')

    # Check if WebSocket server mode is requested
    if args.ws_server:
        start_websocket_server()
        return

    if was_port_explicit():
        # Explicit port specified - validate it's available or error
        validate_port_available(args.port, '--port')
    else:
        # No explicit port - auto-find an available one
        args.port = get_valid_port(args.port)

    try:
        start_webserver_ui()
        log.info('Exiting...')
    except Exception as e:
        # showing error in debug only because this is handled gracefully
        log.critical(f'Failed to start app. Error: {e}')
        log.debug('Failed to start. Traceback below')
        log.debug(traceback.format_exc())


def try_check_update():
    """Check for updates and log if available."""
    try:
        if is_update_available():
            log.info('An update is available!')
            log.info(
                'Run "pip install --upgrade lanscape --no-cache" to suppress this message.')
    except BaseException:
        log.debug(traceback.format_exc())
        log.warning('Unable to check for updates.')


def start_websocket_server():
    """Start the WebSocket server."""
    if was_ws_port_explicit():
        # Explicit port specified - validate it's available or error
        validate_port_available(args.ws_port, '--ws-port')
    else:
        # No explicit port - auto-find an available one
        args.ws_port = get_valid_port(args.ws_port)
    log.info(f'Starting WebSocket server on port {args.ws_port}')
    log.info(f'React UI should connect to ws://localhost:{args.ws_port}')

    try:
        run_server(host='0.0.0.0', port=args.ws_port)
    except KeyboardInterrupt:
        log.info('WebSocket server stopped by user')
    except Exception as e:
        log.critical(f'WebSocket server failed: {e}')
        log.debug(traceback.format_exc())
        raise


def open_browser(url: str, wait=2) -> Popen | None:
    """
    Open a browser window to the specified
    url after waiting for the server to start
    """
    try:
        time.sleep(wait)
        log.info(f'Starting UI - http://127.0.0.1:{args.port}')
        return open_pwa(url, auto_profile=False)

    except ChromiumNotFoundError:
        success = webbrowser.open(url)
        if success:
            log.warning("Chromium browser not found. Falling back to default web browser.")
        else:
            log.warning(f"Cannot find any web browser. LANScape UI running on {url}")
    except BaseException:
        log.debug(traceback.format_exc())
        log.info(f'Unable to open web browser, server running on {url}')
    return None


def start_webserver_ui():
    """Start the web server and open the UI in a browser."""
    uri = f'http://127.0.0.1:{args.port}'

    # running reloader requires flask to run in main thread
    # this decouples UI from main process
    if args.reloader:
        # determine if it was reloaded by flask debug reloader
        # if it was, dont open the browser again
        log.info('Opening UI as daemon')
        if not IS_FLASK_RELOAD:
            open_browser(uri)
        start_webserver(args)
    else:
        flask_thread = start_webserver_daemon(args)
        proc = open_browser(uri)
        if proc:
            app_closed = proc.wait()
        else:
            app_closed = False

        if not app_closed or args.persistent:
            # not doing a direct join so i can still
            # terminate the app with ctrl+c
            while flask_thread.is_alive():
                time.sleep(1)


def is_port_available(port: int) -> bool:
    """Check if a port is available for binding."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        return s.connect_ex(('localhost', port)) != 0


def validate_port_available(port: int, flag_name: str) -> None:
    """
    Validate that an explicitly specified port is available.
    Raises an error if the port is already in use.
    """
    if not is_port_available(port):
        raise OSError(
            f"Port {port} is already in use. "
            f"Either free the port or remove the {flag_name} flag to auto-select an available port."
        )


def get_valid_port(port: int) -> int:
    """
    Get the first available port starting from the specified port.
    Used when no explicit port is specified.

    Args:
        port: Starting port number to check

    Returns:
        First available port

    Raises:
        RuntimeError: If no available port found between port and 65535
    """
    max_port = 65535
    start_port = port
    while port <= max_port:
        if is_port_available(port):
            return port
        port += 1
    raise RuntimeError(
        f"No available port found between {start_port} and {max_port}"
    )


def terminate():
    """Send a request to shutdown flask if it's running."""
    try:
        log.info('Attempting flask shutdown')
        requests.get(f'http://127.0.0.1:{args.port}/shutdown?type=core', timeout=2)
    except requests.exceptions.RequestException:
        # Flask server not running or already shut down - that's fine
        pass


if __name__ == "__main__":
    main()
