import os
import socket
import urllib.request
import webbrowser
from typing import Optional

import typer
import uvicorn

from remotivelabs.cli.search import SearchIndex
from remotivelabs.cli.studio.api import api
from remotivelabs.cli.studio.api.environment import parse_topology_version_output
from remotivelabs.cli.studio.studio_mount import mount_studio
from remotivelabs.cli.topology.context import TopologyContext
from remotivelabs.cli.utils.console import print_generic_error, print_hint


def _is_port_in_use(host: str, port: int) -> bool:
    """Check both IPv4 and IPv6 since servers may listen on either."""
    for family, addr in ((socket.AF_INET, host), (socket.AF_INET6, "::1")):
        try:
            with socket.socket(family, socket.SOCK_STREAM) as s:
                s.settimeout(1)
                if s.connect_ex((addr, port)) == 0:
                    return True
        except OSError:
            continue
    return False


def _get_studio_version_if_studio(host: str, port: int) -> Optional[str]:
    """Probe the occupied port for a running RemotiveStudio instance and exit with an appropriate message."""
    url = f"http://{host}:{port}/api/studio/v1/environment"
    try:
        with urllib.request.urlopen(url, timeout=2) as resp:
            data = parse_topology_version_output(resp.read().decode("utf-8"))
            print
            return next((v.version for v in data.versions if v.name == "RemotiveCLI"), None)
    except Exception as e:
        print(e)
        pass
    return None


def ensure_studio_can_start(host: str, port: int) -> None:
    """Ensure the studio can start by checking if the port is in use and if a studio is already running on the port."""
    if port == 0:
        return
    if _is_port_in_use(host, port):
        version = _get_studio_version_if_studio(host, port)
        if version:
            print_hint(f"RemotiveStudio is already running on http://{host}:{port} (version {version})")
            raise typer.Exit(1)
        print_generic_error(f"Port {port} is already in use by another process. Use --port to specify a different port.")
        raise typer.Exit(1)


def start_studio(  # noqa: PLR0913
    ctx: TopologyContext,
    broker_url: str,
    host: str,
    port: int,
    browser: bool,
    log_level: str,
    search_db: str = ":memory:",
) -> None:
    ensure_studio_can_start(host, port)

    found_studio_app = mount_studio(api)
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((host, port))
    sock.listen(2048)
    _, port = sock.getsockname()

    def startup() -> None:
        print(f"🚀 RemotiveStudio is running: http://localhost:{port}")
        if browser:
            if not found_studio_app:
                # hint is mainly info for local development
                print_hint("RemotiveStudio files not found, react-app not included?")
            else:
                webbrowser.open(f"http://localhost:{port}/")

    api.state.startup = startup
    api.state.topology = ctx
    api.state.broker_url = broker_url
    api.state.search_index = SearchIndex(db_path=search_db)

    os.putenv("REMOTIVE_STUDIO", "true")

    uvicorn.run(
        api,
        fd=sock.fileno(),
        log_level=log_level,
        lifespan="on",
    )
