import multiprocessing
import os
import runpy
import sys
from pathlib import Path
from typing import Any, Callable, Literal, Optional, TypedDict, Union

from fastapi.middleware.gzip import GZipMiddleware
from starlette.routing import Route
from starlette.types import ASGIApp
from uvicorn.main import STARTUP_FAILURE
from uvicorn.supervisors import ChangeReload, Multiprocess

import __main__

from . import core, helpers
from . import native as native_module
from .air import Air
from .client import Client
from .language import Language
from .logging import log
from .middlewares import RedirectWithPrefixMiddleware, SetCacheControlMiddleware
from .server import CustomServerConfig, Server
from .storage import set_storage_secret

APP_IMPORT_STRING = 'nicegui:app'


class ContactDict(TypedDict):
    name: Optional[str]
    url: Optional[str]
    email: Optional[str]


class LicenseInfoDict(TypedDict):
    name: str
    identifier: Optional[str]
    url: Optional[str]


class DocsConfig(TypedDict):
    title: Optional[str]
    summary: Optional[str]
    description: Optional[str]
    version: Optional[str]
    terms_of_service: Optional[str]
    contact: Optional[ContactDict]
    license_info: Optional[LicenseInfoDict]


def run(root: Optional[Callable] = None, *,
        host: Optional[str] = None,
        port: Optional[int] = None,
        title: str = 'NiceGUI',
        viewport: str = 'width=device-width, initial-scale=1',
        favicon: Optional[Union[str, Path]] = None,
        dark: Optional[bool] = False,
        language: Language = 'en-US',
        binding_refresh_interval: Optional[float] = 0.1,
        reconnect_timeout: float = 3.0,
        message_history_length: int = 1000,
        cache_control_directives: str = 'public, max-age=31536000, immutable, stale-while-revalidate=31536000',
        gzip_middleware_factory: Optional[Callable[[ASGIApp], GZipMiddleware]] = GZipMiddleware,
        fastapi_docs: Union[bool, DocsConfig] = False,
        show: Union[bool, str] = True,
        on_air: Optional[Union[str, Literal[True]]] = None,
        native: bool = False,
        window_size: Optional[tuple[int, int]] = None,
        fullscreen: bool = False,
        frameless: bool = False,
        reload: bool = True,
        uvicorn_logging_level: str = 'warning',
        uvicorn_reload_dirs: str = '.',
        uvicorn_reload_includes: str = '*.py',
        uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
        tailwind: bool = True,
        prod_js: bool = True,
        endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none',
        storage_secret: Optional[str] = None,
        session_middleware_kwargs: Optional[dict[str, Any]] = None,
        show_welcome_message: bool = True,
        **kwargs: Any,
        ) -> None:
    """ui.run

    You can call `ui.run()` with optional arguments.
    Most of them only apply after stopping and fully restarting the app and do not apply with auto-reloading.

    :param root: root page function (*added in version 3.0.0*)
    :param host: start server with this host (defaults to `'127.0.0.1` in native mode, otherwise `'0.0.0.0'`)
    :param port: use this port (default: 8080 in normal mode, and an automatically determined open port in native mode)
    :param title: page title (default: `'NiceGUI'`, can be overwritten per page)
    :param viewport: page meta viewport content (default: `'width=device-width, initial-scale=1'`, can be overwritten per page)
    :param favicon: relative filepath, absolute URL to a favicon (default: `None`, NiceGUI icon will be used) or emoji (e.g. `'🚀'`, works for most browsers)
    :param dark: whether to use Quasar's dark mode (default: `False`, use `None` for "auto" mode)
    :param language: language for Quasar elements (default: `'en-US'`)
    :param binding_refresh_interval: interval for updating active links (default: 0.1 seconds, bigger is more CPU friendly, *since version 3.4.0*: can be ``None`` to disable update loop)
    :param reconnect_timeout: maximum time the server waits for the browser to reconnect (default: 3.0 seconds)
    :param message_history_length: maximum number of messages that will be stored and resent after a connection interruption (default: 1000, use 0 to disable, *added in version 2.9.0*)
    :param cache_control_directives: cache control directives for internal static files (default: `'public, max-age=31536000, immutable, stale-while-revalidate=31536000'`)
    :param gzip_middleware_factory: GZipMiddleware factory function (e.g. ``lambda app: GZipMiddleware(app, minimum_size=500, compresslevel=9)``, defaults to Starlette's ``GZipMiddleware``, can be ``None`` to disable, *added in version 3.5.0*)
    :param fastapi_docs: enable FastAPI's automatic documentation with Swagger UI, ReDoc, and OpenAPI JSON (bool or dictionary as described `here <https://fastapi.tiangolo.com/tutorial/metadata/>`_, default: `False`, *updated in version 2.9.0*)
    :param show: automatically open the UI in a browser tab (default: `True`, opens "/", *since version 3.6.0*: you can pass a specific path like "/path/to/page")
    :param on_air: tech preview: `allows temporary remote access <https://nicegui.io/documentation/section_configuration_deployment#nicegui_on_air>`_ if set to `True` (default: disabled)
    :param native: open the UI in a native window of size 800x600 (default: `False`, deactivates `show`, automatically finds an open port)
    :param window_size: open the UI in a native window with the provided size (e.g. `(1024, 786)`, default: `None`, also activates `native`)
    :param fullscreen: open the UI in a fullscreen window (default: `False`, also activates `native`)
    :param frameless: open the UI in a frameless window (default: `False`, also activates `native`)
    :param reload: automatically reload the UI on file changes (default: `True`)
    :param uvicorn_logging_level: logging level for uvicorn server (default: `'warning'`)
    :param uvicorn_reload_dirs: string with comma-separated list for directories to be monitored (default is current working directory only)
    :param uvicorn_reload_includes: string with comma-separated list of glob-patterns which trigger reload on modification (default: `'*.py'`)
    :param uvicorn_reload_excludes: string with comma-separated list of glob-patterns which should be ignored for reload (default: `'.*, .py[cod], .sw.*, ~*'`)
    :param tailwind: whether to use Tailwind (experimental, default: `True`)
    :param prod_js: whether to use the production version of Vue and Quasar dependencies (default: `True`)
    :param endpoint_documentation: control what endpoints appear in the autogenerated OpenAPI docs (default: 'none', options: 'none', 'internal', 'page', 'all')
    :param storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.individual and ui.storage.browser)
    :param session_middleware_kwargs: additional keyword arguments passed to SessionMiddleware that creates the session cookies used for browser-based storage
    :param show_welcome_message: whether to show the welcome message (default: `True`)
    :param kwargs: additional keyword arguments are passed to `uvicorn.run`
    """
    if core.script_mode:
        if Client.page_routes:
            if core.script_client and not core.script_client.content.default_slot.children and (
                core.script_client._head_html or core.script_client._body_html  # pylint: disable=protected-access
            ):
                raise RuntimeError(
                    'ui.add_head_html, ui.add_body_html, or ui.add_css has been called inside the global scope while using ui.page.\n'
                    'Consider using shared=True for this call to add the code to all pages.\n'
                    'Alternatively, to add the code to a specific page, move the call into the page function.'
                )
            raise RuntimeError(
                'ui.page cannot be used in NiceGUI scripts when UI is defined in the global scope.\n'
                'To use multiple pages, either move all UI into page functions or use ui.sub_pages.'
            )

        if helpers.is_pytest():
            raise RuntimeError('Script mode is not supported in pytest. '
                               'Please pass a root function to ui.run() or use page decorators.')
        if core.app.is_started:
            return

        def run_script() -> None:
            if not sys.argv or not sys.argv[0] or not helpers.is_file(sys.argv[0]):
                raise RuntimeError(
                    'Script mode requires a valid script file to re-execute.\n'
                    'This error occurs when running code interactively (e.g., Shift-Enter in an IDE).\n'
                    'To fix this, either:\n'
                    '  1. Run the complete file instead of a selection (e.g., "python script.py")\n'
                    '  2. Use a root function: wrap your UI code in a function and pass it to ui.run(root=my_function)'
                )
            runpy.run_path(sys.argv[0], run_name='__main__')
        root = run_script
        assert core.script_client is not None
        core.script_client.delete()

    core.app.config.add_run_config(
        reload=reload,
        title=title,
        viewport=viewport,
        favicon=favicon,
        dark=dark,
        language=language,
        binding_refresh_interval=binding_refresh_interval,
        reconnect_timeout=reconnect_timeout,
        message_history_length=message_history_length,
        cache_control_directives=cache_control_directives,
        tailwind=tailwind,
        prod_js=prod_js,
        show_welcome_message=show_welcome_message,
    )
    core.root = root
    core.app.config.endpoint_documentation = endpoint_documentation
    if not helpers.is_pytest() and gzip_middleware_factory is not None:
        core.app.add_middleware(gzip_middleware_factory)
    core.app.add_middleware(RedirectWithPrefixMiddleware)
    core.app.add_middleware(SetCacheControlMiddleware)

    for route in core.app.routes:
        if not isinstance(route, Route):
            continue
        if route.path.startswith('/_nicegui') and hasattr(route, 'methods'):
            route.include_in_schema = endpoint_documentation in {'internal', 'all'}
        if route.path == '/' or route.path in Client.page_routes.values():
            route.include_in_schema = endpoint_documentation in {'page', 'all'}

    if fastapi_docs:
        if not core.app.docs_url:
            core.app.docs_url = '/docs'
        if not core.app.redoc_url:
            core.app.redoc_url = '/redoc'
        if not core.app.openapi_url:
            core.app.openapi_url = '/openapi.json'
        if isinstance(fastapi_docs, dict):
            core.app.title = fastapi_docs.get('title') or title
            core.app.summary = fastapi_docs.get('summary')
            core.app.description = fastapi_docs.get('description') or ''
            core.app.version = fastapi_docs.get('version') or '0.1.0'
            core.app.terms_of_service = fastapi_docs.get('terms_of_service')
            contact = fastapi_docs.get('contact')
            license_info = fastapi_docs.get('license_info')
            core.app.contact = dict(contact) if contact else None
            core.app.license_info = dict(license_info) if license_info else None
        core.app.setup()

    if helpers.is_user_simulation():
        set_storage_secret(storage_secret, session_middleware_kwargs)
        return

    if on_air:
        core.air = Air('' if on_air is True else on_air)

    if multiprocessing.current_process().name != 'MainProcess':
        return

    if reload and not hasattr(__main__, '__file__'):
        log.warning('disabling auto-reloading because is is only supported when running from a file')
        core.app.config.reload = reload = False

    if fullscreen:
        native = True
    if frameless:
        native = True
    if window_size:
        native = True
    if native:
        show = False
        host = host or '127.0.0.1'
        port = port or native_module.find_open_port()
        width, height = window_size or (800, 600)
        native_host = '127.0.0.1' if host == '0.0.0.0' else host
        native_module.activate(native_host, port, title, width, height, fullscreen, frameless)
    else:
        port = port or 8080
        host = host or '0.0.0.0'
    assert host is not None
    assert port is not None

    if helpers.is_pytest():
        port = int(os.environ['NICEGUI_SCREEN_TEST_PORT'])
        show = False
        reload = False
        native = False
        show_welcome_message = False

    if kwargs.get('ssl_certfile') and kwargs.get('ssl_keyfile'):
        protocol = 'https'
    else:
        protocol = 'http'

    # NOTE: We save host and port in environment variables so the subprocess started in reload mode can access them.
    os.environ['NICEGUI_HOST'] = host
    os.environ['NICEGUI_PORT'] = str(port)
    os.environ['NICEGUI_PROTOCOL'] = protocol

    if show:
        helpers.schedule_browser(protocol, host, port, show if isinstance(show, str) else '/')

    def split_args(args: str) -> list[str]:
        return [a.strip() for a in args.split(',')]

    if kwargs.get('workers', 1) > 1:
        raise ValueError('NiceGUI does not support multiple workers yet.')

    # NOTE: The following lines are basically a copy of `uvicorn.run`, but keep a reference to the `server`.

    config = CustomServerConfig(
        APP_IMPORT_STRING if reload else core.app,
        host=host,
        port=port,
        reload=reload,
        reload_includes=split_args(uvicorn_reload_includes) if reload else None,
        reload_excludes=split_args(uvicorn_reload_excludes) if reload else None,
        reload_dirs=split_args(uvicorn_reload_dirs) if reload else None,
        log_level=uvicorn_logging_level,
        ws='wsproto',
        **kwargs,
    )
    config.storage_secret = storage_secret
    config.method_queue = native_module.native.method_queue if native else None
    config.response_queue = native_module.native.response_queue if native else None
    config.session_middleware_kwargs = session_middleware_kwargs
    Server.create_singleton(config)

    if (reload or config.workers > 1) and not isinstance(config.app, str):
        log.warning('You must pass the application as an import string to enable "reload" or "workers".')
        sys.exit(1)

    if config.should_reload:
        sock = config.bind_socket()
        ChangeReload(config, target=Server.instance.run, sockets=[sock]).run()
    elif config.workers > 1:
        sock = config.bind_socket()
        Multiprocess(config, target=Server.instance.run, sockets=[sock]).run()
    else:
        Server.instance.run()
    if config.uds:
        os.remove(config.uds)  # pragma: py-win32

    if not Server.instance.started and not config.should_reload and config.workers == 1:
        sys.exit(STARTUP_FAILURE)
