import functools
import os
import sys
import collections
import inspect
import importlib
import warnings
from importlib.machinery import ModuleSpec
from importlib.util import find_spec
from importlib import metadata
import pkgutil
import threading
import re
import logging
import mimetypes
import hashlib
import base64
from urllib.parse import urlparse
from typing import Any, Callable, Dict, Optional, Union, Sequence, Literal, List

import traceback

from dash import dcc
from dash import html
from dash import dash_table
from .fingerprint import build_fingerprint
from .resources import Scripts, Css
from .dependencies import (
    Input,
    Output,
    State,
)
from .development.base_component import ComponentRegistry
from .exceptions import (
    ProxyError,
    DuplicateCallback,
)
from .backends import get_backend
from .version import __version__
from ._configs import get_combined_config, pathname_configs, pages_folder_config
from ._utils import (
    AttributeDict,
    format_tag,
    generate_hash,
    inputs_to_dict,
    inputs_to_vals,
    interpolate_str,
    patch_collections_abc,
    split_callback_id,
    to_json,
    convert_to_AttributeDict,
    gen_salt,
    hooks_to_js_object,
    get_caller_name,
    get_root_path,
    alias_main_module,
)
from . import _callback
from . import _callback_signing
from . import _get_paths
from . import _dash_renderer
from . import _validate
from . import _watch
from . import _get_app
from . import backends

from ._get_app import with_app_context, with_app_context_factory
from ._grouping import map_grouping, grouping_len, update_args_group
from ._obsolete import ObsoleteChecker
from ._callback_context import callback_context

from . import _pages
from ._pages import (
    _parse_query_string,
    _page_meta_tags,
    _path_to_page,
    _import_layouts_from_pages,
)
from ._jupyter import jupyter_dash, JupyterDisplayMode
from .types import CallbackExecutionBody, RendererHooks

RouteCallable = Callable[..., Any]

# If dash_design_kit is installed, check for version
ddk_version = None
if find_spec("dash_design_kit"):
    ddk_version = metadata.version("dash_design_kit")

plotly_version = None
if find_spec("plotly"):
    plotly_version = metadata.version("plotly")

# Add explicit mapping for map files
mimetypes.add_type("application/json", ".map", True)

_default_index = """<!DOCTYPE html>
<html>
    <head>
        {%metas%}
        <title>{%title%}</title>
        {%favicon%}
        {%css%}
    </head>
    <body>
        <!--[if IE]><script>
        alert("Dash v2.7+ does not support Internet Explorer. Please use a newer browser.");
        </script><![endif]-->
        {%app_entry%}
        <footer>
            {%config%}
            {%scripts%}
            {%renderer%}
        </footer>
    </body>
</html>"""

_app_entry = """
<div id="react-entry-point">
    <div class="_dash-loading">
        Loading...
    </div>
</div>
"""

_re_index_entry = "{%app_entry%}", "{%app_entry%}"
_re_index_config = "{%config%}", "{%config%}"
_re_index_scripts = "{%scripts%}", "{%scripts%}"

_re_index_entry_id = 'id="react-entry-point"', "#react-entry-point"
_re_index_config_id = 'id="_dash-config"', "#_dash-config"
_re_index_scripts_id = 'src="[^"]*dash[-_]renderer[^"]*"', "dash-renderer"
_re_renderer_scripts_id = 'id="_dash-renderer', "new DashRenderer"


_ID_CONTENT = "_pages_content"
_ID_LOCATION = "_pages_location"
_ID_STORE = "_pages_store"
_ID_DUMMY = "_pages_dummy"

_UNINITIALIZED = object()  # Sentinel for tracking init_app state

DASH_VERSION_URL = "https://dash-version.plotly.com:8080/current_version"

# Handles the case in a newly cloned environment where the components are not yet generated.
try:
    page_container = html.Div(  # type: ignore[operator]
        [
            dcc.Location(id=_ID_LOCATION, refresh="callback-nav"),  # type: ignore[operator]
            html.Div(id=_ID_CONTENT, disable_n_clicks=True),  # type: ignore[operator]
            dcc.Store(id=_ID_STORE),  # type: ignore[operator]
            html.Div(id=_ID_DUMMY, disable_n_clicks=True),  # type: ignore[operator]
        ]
    )
# pylint: disable-next=bare-except
except:  # noqa: E722
    page_container = None


def _get_traceback(secret, error: Exception):
    try:
        # pylint: disable=import-outside-toplevel
        from werkzeug.debug import tbtools
    except ImportError:
        tbtools = None  # type: ignore[assignment]

    def _get_skip(error):
        from dash._callback import (  # pylint: disable=import-outside-toplevel
            _invoke_callback,
            _async_invoke_callback,
        )

        tb = error.__traceback__
        skip = 1
        while tb.tb_next is not None:
            skip += 1
            tb = tb.tb_next
            if tb.tb_frame.f_code in [
                _invoke_callback.__code__,
                _async_invoke_callback.__code__,
            ]:
                return skip

        return skip

    def _do_skip(error):
        from dash._callback import (  # pylint: disable=import-outside-toplevel
            _invoke_callback,
            _async_invoke_callback,
        )

        tb = error.__traceback__
        while tb.tb_next is not None:
            if tb.tb_frame.f_code in [
                _invoke_callback.__code__,
                _async_invoke_callback.__code__,
            ]:
                return tb.tb_next
            tb = tb.tb_next
        return error.__traceback__

    # werkzeug<2.1.0
    if hasattr(tbtools, "get_current_traceback"):
        return tbtools.get_current_traceback(  # type: ignore
            skip=_get_skip(error)
        ).render_full()

    if hasattr(tbtools, "DebugTraceback"):
        # pylint: disable=no-member
        return tbtools.DebugTraceback(  # type: ignore
            error, skip=_get_skip(error)
        ).render_debugger_html(True, secret, True)

    return "".join(traceback.format_exception(type(error), error, _do_skip(error)))


# Singleton signal to not update an output, alternative to PreventUpdate
no_update = _callback.NoUpdate()  # pylint: disable=protected-access


async def execute_async_function(func, *args, **kwargs):
    # Check if the function is a coroutine function
    if inspect.iscoroutinefunction(func):
        return await func(*args, **kwargs)
    # If the function is not a coroutine, call it directly
    return func(*args, **kwargs)


# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments, too-many-locals
class Dash(ObsoleteChecker):
    """Dash is a framework for building analytical web applications.
    No JavaScript required.

    If a parameter can be set by an environment variable, that is listed as:
        env: ``DASH_****``
    Values provided here take precedence over environment variables.

    :param name: The name Flask should use for your app. Even if you provide
        your own ``server``, ``name`` will be used to help find assets.
        Typically ``__name__`` (the magic global var, not a string) is the
        best value to use. Default ``'__main__'``, env: ``DASH_APP_NAME``
    :type name: string

    :param server: Sets the server for your app. There are three options:
        ``True`` (default): Dash will create a new server using the specified backend
        ``False``: The server will be added later via ``app.init_app(server)``
        A server instance: Use a pre-existing server (Flask, Quart, or FastAPI)
    :type server: boolean or server instance

    :param backend: The backend to use for the Dash app. Can be a string
        (name of the backend) or a backend class. Default is None, which
        selects the Flask backend. Currently, "flask", "fastapi", and "quart" backends
        are supported.
    :type backend: string or type

    :param assets_folder: a path, relative to the current working directory,
        for extra files to be used in the browser. Default ``'assets'``.
        All .js and .css files will be loaded immediately unless excluded by
        ``assets_ignore``, and other files such as images will be served if
        requested.
    :type assets_folder: string

    :param pages_folder: a relative or absolute path for pages of a multi-page app.
        Default ``'pages'``.
    :type pages_folder: string or pathlib.Path

    :param use_pages: When True, the ``pages`` feature for multi-page apps is
        enabled. If you set a non-default ``pages_folder`` this will be inferred
        to be True. Default `None`.
    :type use_pages: boolean

    :param include_pages_meta: Include the page meta tags for twitter cards.
    :type include_pages_meta: bool

    :param assets_url_path: The local urls for assets will be:
        ``requests_pathname_prefix + assets_url_path + '/' + asset_path``
        where ``asset_path`` is the path to a file inside ``assets_folder``.
        Default ``'assets'``.
    :type asset_url_path: string

    :param assets_ignore: A regex, as a string to pass to ``re.compile``, for
        assets to omit from immediate loading. Ignored files will still be
        served if specifically requested. You cannot use this to prevent access
        to sensitive files.
    :type assets_ignore: string

    :param assets_path_ignore: A list of regex, each regex as a string to pass to ``re.compile``, for
        assets path to omit from immediate loading. The files in these ignored paths will still be
        served if specifically requested. You cannot use this to prevent access
        to sensitive files.
    :type assets_path_ignore: list of strings

    :param assets_external_path: an absolute URL from which to load assets.
        Use with ``serve_locally=False``. assets_external_path is joined
        with assets_url_path to determine the absolute url to the
        asset folder. Dash can still find js and css to automatically load
        if you also keep local copies in your assets folder that Dash can index,
        but external serving can improve performance and reduce load on
        the Dash server.
        env: ``DASH_ASSETS_EXTERNAL_PATH``
    :type assets_external_path: string

    :param include_assets_files: Default ``True``, set to ``False`` to prevent
        immediate loading of any assets. Assets will still be served if
        specifically requested. You cannot use this to prevent access
        to sensitive files. env: ``DASH_INCLUDE_ASSETS_FILES``
    :type include_assets_files: boolean

    :param url_base_pathname: A local URL prefix to use app-wide.
        Default ``'/'``. Both `requests_pathname_prefix` and
        `routes_pathname_prefix` default to `url_base_pathname`.
        env: ``DASH_URL_BASE_PATHNAME``
    :type url_base_pathname: string

    :param requests_pathname_prefix: A local URL prefix for file requests.
        Defaults to `url_base_pathname`, and must end with
        `routes_pathname_prefix`. env: ``DASH_REQUESTS_PATHNAME_PREFIX``
    :type requests_pathname_prefix: string

    :param routes_pathname_prefix: A local URL prefix for JSON requests.
        Defaults to ``url_base_pathname``, and must start and end
        with ``'/'``. env: ``DASH_ROUTES_PATHNAME_PREFIX``
    :type routes_pathname_prefix: string

    :param serve_locally: If ``True`` (default), assets and dependencies
        (Dash and Component js and css) will be served from local URLs.
        If ``False`` we will use CDN links where available.
    :type serve_locally: boolean

    :param compress: Use gzip to compress files and data served by Flask.
        To use this option, you need to install dash[compress]
        Default ``False``
    :type compress: boolean

    :param meta_tags: html <meta> tags to be added to the index page.
        Each dict should have the attributes and values for one tag, eg:
        ``{'name': 'description', 'content': 'My App'}``
    :type meta_tags: list of dicts

    :param index_string: Override the standard Dash index page.
        Must contain the correct insertion markers to interpolate various
        content into it depending on the app config and components used.
        See https://dash.plotly.com/external-resources for details.
    :type index_string: string

    :param external_scripts: Additional JS files to load with the page.
        Each entry can be a string (the URL) or a dict with ``src`` (the URL)
        and optionally other ``<script>`` tag attributes such as ``integrity``
        and ``crossorigin``.
    :type external_scripts: list of strings or dicts

    :param external_stylesheets: Additional CSS files to load with the page.
        Each entry can be a string (the URL) or a dict with ``href`` (the URL)
        and optionally other ``<link>`` tag attributes such as ``rel``,
        ``integrity`` and ``crossorigin``.
    :type external_stylesheets: list of strings or dicts

    :param suppress_callback_exceptions: Default ``False``: check callbacks to
        ensure referenced IDs exist and props are valid. Set to ``True``
        if your layout is dynamic, to bypass these checks.
        env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
    :type suppress_callback_exceptions: boolean

    :param prevent_initial_callbacks: Default ``False``: Sets the default value
        of ``prevent_initial_call`` for all callbacks added to the app.
        Normally all callbacks are fired when the associated outputs are first
        added to the page. You can disable this for individual callbacks by
        setting ``prevent_initial_call`` in their definitions, or set it
        ``True`` here in which case you must explicitly set it ``False`` for
        those callbacks you wish to have an initial call. This setting has no
        effect on triggering callbacks when their inputs change later on.

    :param show_undo_redo: Default ``False``, set to ``True`` to enable undo
        and redo buttons for stepping through the history of the app state.
    :type show_undo_redo: boolean

    :param extra_hot_reload_paths: A list of paths to watch for changes, in
        addition to assets and known Python and JS code, if hot reloading is
        enabled.
    :type extra_hot_reload_paths: list of strings

    :param plugins: Extend Dash functionality by passing a list of objects
        with a ``plug`` method, taking a single argument: this app, which will
        be called after the Flask server is attached.
    :type plugins: list of objects

    :param title: Default ``Dash``. Configures the document.title
    (the text that appears in a browser tab).

    :param update_title: Default ``Updating...``. Configures the document.title
    (the text that appears in a browser tab) text when a callback is being run.
    Set to None or '' if you don't want the document.title to change or if you
    want to control the document.title through a separate component or
    clientside callback.

    :param background_callback_manager: Background callback manager instance
        to support the ``@callback(..., background=True)`` decorator.
        One of ``DiskcacheManager`` or ``CeleryManager`` currently supported.

    :param add_log_handler: Automatically add a StreamHandler to the app logger
        if not added previously.

    :param hooks: Extend Dash renderer functionality by passing a dictionary of
    javascript functions. To hook into the layout, use dict keys "layout_pre" and
    "layout_post". To hook into the callbacks, use keys "request_pre" and "request_post"

    :param routing_callback_inputs: When using Dash pages (use_pages=True), allows to
    add new States to the routing callback, to pass additional data to the layout
    functions. The syntax for this parameter is a dict of State objects:
    `routing_callback_inputs={"language": Input("language", "value")}`
    NOTE: the keys "pathname_" and "search_" are reserved for internal use.

    :param description:  Sets a default description for meta tags on Dash pages (use_pages=True).

    :param on_error: Global callback error handler to call when
        an exception is raised. Receives the exception object as first argument.
        The callback_context can be used to access the original callback inputs,
        states and output.

    :param use_async: When True, the app will create async endpoints, as a dev,
        they will be responsible for installing the `flask[async]` dependency.
    :type use_async: boolean

    :param health_endpoint: Path for the health check endpoint. Set to None to
        disable the health endpoint. Default is None.
    :type health_endpoint: string or None

    :param csrf_token_name: Name of the cookie to read the CSRF token from.
        Default ``'_csrf_token'``. Set this to match the CSRF cookie name
        used by your server framework (e.g. ``'csrftoken'`` for Django).
    :type csrf_token_name: string

    :param csrf_header_name: Name of the HTTP header to send the CSRF token in.
        Default ``'X-CSRFToken'``.
    :type csrf_header_name: string
    """

    _plotlyjs_url: str
    STARTUP_ROUTES: list = []

    server: Any

    # Layout is a complex type which can be many things
    _layout: Any
    _extra_components: Any

    def __init__(  # pylint: disable=too-many-statements, too-many-branches
        self,
        name: Optional[str] = None,
        server: Union[bool, Callable[[], Any]] = True,
        backend: Union[str, type, None] = None,
        assets_folder: str = "assets",
        pages_folder: str = "pages",
        use_pages: Optional[bool] = None,
        assets_url_path: str = "assets",
        assets_ignore: str = "",
        assets_path_ignore: Optional[List[str]] = None,
        assets_external_path: Optional[str] = None,
        eager_loading: bool = False,
        include_assets_files: bool = True,
        include_pages_meta: bool = True,
        url_base_pathname: Optional[str] = None,
        requests_pathname_prefix: Optional[str] = None,
        routes_pathname_prefix: Optional[str] = None,
        serve_locally: bool = True,
        compress: Optional[bool] = None,
        meta_tags: Optional[Sequence[Dict[str, Any]]] = None,
        index_string: str = _default_index,
        external_scripts: Optional[Sequence[Union[str, Dict[str, Any]]]] = None,
        external_stylesheets: Optional[Sequence[Union[str, Dict[str, Any]]]] = None,
        suppress_callback_exceptions: Optional[bool] = None,
        prevent_initial_callbacks: bool = False,
        show_undo_redo: bool = False,
        extra_hot_reload_paths: Optional[Sequence[str]] = None,
        plugins: Optional[list] = None,
        title: str = "Dash",
        update_title: str = "Updating...",
        background_callback_manager: Optional[
            Any
        ] = None,  # Type should be specified if possible
        add_log_handler: bool = True,
        hooks: Optional[RendererHooks] = None,
        routing_callback_inputs: Optional[Dict[str, Union[Input, State]]] = None,
        description: Optional[str] = None,
        on_error: Optional[Callable[[Exception], Any]] = None,
        use_async: Optional[bool] = None,
        health_endpoint: Optional[str] = None,
        csrf_token_name: str = "_csrf_token",
        csrf_header_name: str = "X-CSRFToken",
        websocket_callbacks: Optional[bool] = False,
        websocket_allowed_origins: Optional[List[str]] = None,
        websocket_inactivity_timeout: Optional[int] = 300000,
        websocket_heartbeat_interval: Optional[int] = 30000,
        websocket_batch_delay: Optional[float] = 0.005,
        websocket_max_workers: Optional[int] = 4,
        enable_mcp: Optional[bool] = None,
        mcp_path: Optional[str] = None,
        **obsolete,
    ):

        use_async = _validate.check_async(use_async)
        _validate.check_obsolete(obsolete)

        if not csrf_token_name or not csrf_token_name.strip():
            raise ValueError("csrf_token_name must be a non-empty string")
        if not csrf_header_name or not csrf_header_name.strip():
            raise ValueError("csrf_header_name must be a non-empty string")

        caller_name: str = name if name is not None else get_caller_name()

        alias_main_module(caller_name)

        # Determine backend
        if backend is None:
            backend_cls = get_backend("flask")
        elif isinstance(backend, str):
            backend_cls = get_backend(backend)
        elif isinstance(backend, type):
            backend_cls = backend
        else:
            raise ValueError("Invalid backend argument")

        # Determine server and backend instance
        if server not in (None, True, False):
            # User provided a server instance (e.g., Flask, Quart, FastAPI)
            inferred_backend = backends.get_server_type(server)
            _validate.check_backend(backend, inferred_backend)
            backend_cls = get_backend(inferred_backend)
            if name is None:
                caller_name = getattr(server, "name", caller_name)

            self.backend = backend_cls(server)
            self.server = server
        else:
            # No server instance provided, create backend and let backend create server
            self.server = backend_cls.create_app(caller_name)  # type: ignore
            self.backend = backend_cls(self.server)

        base_prefix, routes_prefix, requests_prefix = pathname_configs(
            url_base_pathname, routes_pathname_prefix, requests_pathname_prefix
        )

        self.config = AttributeDict(
            name=caller_name,
            assets_folder=os.path.join(
                get_root_path(caller_name), assets_folder
            ),  # type: ignore
            assets_url_path=assets_url_path,
            assets_ignore=assets_ignore,
            assets_path_ignore=assets_path_ignore,
            assets_external_path=get_combined_config(
                "assets_external_path", assets_external_path, ""
            ),
            pages_folder=pages_folder_config(caller_name, pages_folder, use_pages),
            eager_loading=eager_loading,
            include_assets_files=get_combined_config(
                "include_assets_files", include_assets_files, True
            ),
            url_base_pathname=base_prefix,
            routes_pathname_prefix=routes_prefix,
            requests_pathname_prefix=requests_prefix,
            serve_locally=serve_locally,
            compress=get_combined_config("compress", compress, False),
            meta_tags=meta_tags or [],
            external_scripts=external_scripts or [],
            external_stylesheets=external_stylesheets or [],
            suppress_callback_exceptions=get_combined_config(
                "suppress_callback_exceptions", suppress_callback_exceptions, False
            ),
            prevent_initial_callbacks=prevent_initial_callbacks,
            show_undo_redo=show_undo_redo,
            extra_hot_reload_paths=extra_hot_reload_paths or [],
            title=title,
            update_title=update_title,
            include_pages_meta=include_pages_meta,
            description=description,
            health_endpoint=health_endpoint,
            hide_all_callbacks=False,
            csrf_token_name=csrf_token_name,
            csrf_header_name=csrf_header_name,
        )
        self.config.set_read_only(
            [
                "name",
                "assets_folder",
                "assets_url_path",
                "eager_loading",
                "serve_locally",
                "compress",
                "pages_folder",
                "csrf_token_name",
                "csrf_header_name",
            ],
            "Read-only: can only be set in the Dash constructor",
        )
        self.config.finalize(
            "Invalid config key. Some settings are only available "
            "via the Dash constructor"
        )

        _get_paths.CONFIG = self.config
        _pages.CONFIG = self.config

        self.pages_folder = str(pages_folder)
        self.use_pages = (pages_folder != "pages") if use_pages is None else use_pages
        self.routing_callback_inputs = routing_callback_inputs or {}

        # keep title as a class property for backwards compatibility
        self.title = title

        # MCP (Model Context Protocol) configuration
        self._enable_mcp = get_combined_config("mcp_enabled", enable_mcp, False)
        _mcp_path = get_combined_config("mcp_path", mcp_path, "_mcp")
        self._mcp_path = (
            _mcp_path.lstrip("/") if isinstance(_mcp_path, str) else _mcp_path
        )

        # list of dependencies - this one is used by the back end for dispatching
        self.callback_map: dict = {}
        # same deps as a list to catch duplicate outputs, and to send to the front end
        self._callback_list: list = []
        self.callback_api_paths: dict = {}
        self.mcp_decorated_functions: dict = {}
        self.mcp_callback_map: Any = None

        # list of inline scripts
        self._inline_scripts: list = []

        # index_string has special setter so can't go in config
        self._index_string: str = ""
        self.index_string = index_string
        self._favicon = None

        # default renderer string
        self.renderer = f"var renderer = new DashRenderer({hooks_to_js_object(hooks)});"

        # static files from the packages
        self.css = Css(serve_locally)
        self.scripts = Scripts(serve_locally, eager_loading)

        self.registered_paths: Any = collections.defaultdict(set)

        # urls
        self.routes: Any = []

        self._layout = None
        self._layout_is_function = False
        self.validation_layout = None
        self._on_error = on_error
        self._extra_components = []
        self._use_async = use_async

        self._setup_dev_tools()
        self._hot_reload = AttributeDict(
            hash=None,
            hard=False,
            lock=threading.RLock(),
            watch_thread=None,
            changed_assets=[],
        )

        self._assets_files: list = []

        self._background_manager = background_callback_manager
        self._websocket_callbacks = websocket_callbacks
        self._websocket_allowed_origins = websocket_allowed_origins or []
        self._websocket_inactivity_timeout = websocket_inactivity_timeout
        self._websocket_heartbeat_interval = websocket_heartbeat_interval
        self._websocket_batch_delay = websocket_batch_delay
        self._websocket_max_workers = websocket_max_workers

        self.logger = logging.getLogger(__name__)

        if not self.logger.handlers and add_log_handler:
            self.logger.addHandler(logging.StreamHandler(stream=sys.stdout))

        if plugins is not None and isinstance(
            plugins, patch_collections_abc("Iterable")
        ):
            for plugin in plugins:
                plugin.plug(self)

        self._setup_hooks()

        # tracks internally if a function already handled at least one request.
        self._got_first_request = {"pages": False, "setup_server": False}

        # Secret used to sign background-callback handles (see _callback_signing).
        # Prefer the Flask/Quart secret_key (shared across workers when the
        # operator sets one); otherwise fall back to a per-process random secret.
        self._generated_signing_secret: Optional[bytes] = None

        if server:
            self.init_app()

        self.logger.setLevel(logging.INFO)

        if self.__class__.__name__ == "JupyterDash":
            warnings.warn(
                "JupyterDash is deprecated, use Dash instead.\n"
                "See https://dash.plotly.com/dash-in-jupyter for more details.",
                stacklevel=2,
            )
        self.setup_startup_routes()

        self._plotly_cloud = None

    def _setup_hooks(self):
        # pylint: disable=import-outside-toplevel,protected-access
        from ._hooks import HooksManager

        self._hooks = HooksManager
        self._hooks.register_setuptools()

        for setup in self._hooks.get_hooks("setup"):
            setup(self)

        for hook in self._hooks.get_hooks("callback"):
            callback_args, callback_kwargs = hook.data  # type: ignore
            self.callback(*callback_args, **callback_kwargs)(hook.func)

        for (
            clientside_function,
            args,
            kwargs,
        ) in self._hooks.hooks._clientside_callbacks:
            _callback.register_clientside_callback(
                self._callback_list,
                self.callback_map,
                self.config.prevent_initial_callbacks,
                self._inline_scripts,
                clientside_function,
                *args,
                **kwargs,
            )

        if self._hooks.get_hooks("error"):
            self._on_error = self._hooks.HookErrorHandler(self._on_error)

    def init_app(self, app: Optional[Any] = None, **kwargs) -> None:
        config = self.config
        config.unset_read_only(
            [
                "url_base_pathname",
                "routes_pathname_prefix",
                "requests_pathname_prefix",
            ]
        )
        config.update(kwargs)
        config.set_read_only(
            [
                "url_base_pathname",
                "routes_pathname_prefix",
                "requests_pathname_prefix",
            ],
            "Read-only: can only be set in the Dash constructor or during init_app()",
        )
        if app is not None:
            self.server = app
            # Also update the backend's server reference so routes are registered
            # on the correct server (important when using server=False pattern)
            self.backend.server = app

        # Skip registration if already initialized on this server
        # This prevents double registration when init_app() is called multiple times
        # (e.g., with flask run pattern where __init__ calls init_app, then user does too)
        if getattr(self, "_initialized_server", _UNINITIALIZED) is self.server:
            return
        self._initialized_server = self.server

        bp_prefix = config.routes_pathname_prefix.replace("/", "_").replace(".", "_")
        assets_blueprint_name = f"{bp_prefix}dash_assets"
        self.backend.register_assets_blueprint(
            assets_blueprint_name,
            config.routes_pathname_prefix + self.config.assets_url_path.lstrip("/"),
            self.config.assets_folder,
        )
        if config.compress:
            self.backend.enable_compression()  # type: ignore

        def _handle_error(_):
            """Handle a halted callback and return an empty 204 response."""
            return "", 204

        # To-Do add error handlers for these two scenarios
        # add handler for halted callbacks
        # self.backend.before_request(_handle_error)
        # add a handler for components suites errors to return 404
        # self.server.errorhandler(InvalidResourceError)(self._invalid_resources_handler)

        self.backend.register_error_handlers()
        self.backend.before_request(self._setup_server)
        self.backend.setup_backend(self)
        self._setup_routes()
        _get_app.APP = self
        self.enable_pages()
        self._setup_plotlyjs()

    def _add_url(self, name: str, view_func: RouteCallable, methods=("GET",)) -> None:
        full_name = self.config.routes_pathname_prefix + name
        self.backend.add_url_rule(
            full_name,
            view_func=view_func,
            endpoint=full_name,
            methods=list(methods),
        )
        self.routes.append(full_name)

    def _serve_default_favicon(self):
        return self.backend.make_response(
            pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon"
        )

    def _setup_routes(self):
        self.backend.setup_component_suites(self)
        self._add_url("_dash-layout", self.serve_layout)
        self._add_url("_dash-dependencies", self.dependencies)
        self._add_url(
            "_dash-update-component",
            self.backend.serve_callback(self),
            ["POST"],
        )
        self._add_url("_reload-hash", self.serve_reload_hash)
        self._add_url(
            "_favicon.ico",
            self._serve_default_favicon,  # pylint: disable=protected-access
        )
        if self.config.health_endpoint is not None:
            self._add_url(self.config.health_endpoint, self.serve_health)

        # Set up WebSocket callback route if backend supports it
        # This enables both global websocket_callbacks and per-callback websocket=True
        if self.backend.websocket_capability:
            self.backend.serve_websocket_callback(self)

        self.backend.setup_index(self)
        self.backend.setup_catchall(self)

        if jupyter_dash.active:
            self._add_url(
                "_alive_" + jupyter_dash.alive_token, jupyter_dash.serve_alive
            )

        for hook in self._hooks.get_hooks("routes"):
            self._add_url(
                hook.data["name"],
                with_app_context_factory(hook.func, self),
                hook.data["methods"],
            )

        if self._enable_mcp:
            from .mcp import (  # pylint: disable=import-outside-toplevel
                enable_mcp_server,
            )

            try:
                enable_mcp_server(self, self._mcp_path)
            except Exception as e:  # pylint: disable=broad-exception-caught
                self._enable_mcp = False
                self.logger.warning(
                    "MCP server could not be started at '%s': %s",
                    self._mcp_path,
                    e,
                )

    def setup_apis(self):
        """
        Register API endpoints for all callbacks defined using `dash.callback`.

        This method must be called after all callbacks are registered and before the app is served.
        It ensures that all callback API routes are available for the Dash app to function correctly.

        Typical usage:
            app = Dash(__name__)
            # Register callbacks here
            app.setup_apis()
            app.run()

        If not called, callback endpoints will not be available and the app will not function as expected.
        """
        for k in list(_callback.GLOBAL_API_PATHS):
            if k in self.callback_api_paths:
                raise DuplicateCallback(
                    f"The callback `{k}` provided with `dash.callback` was already "
                    "assigned with `app.callback`."
                )
            self.callback_api_paths[k] = _callback.GLOBAL_API_PATHS.pop(k)

        # Delegate to the server factory for route registration
        self.backend.register_callback_api_routes(self.callback_api_paths)

    def _setup_plotlyjs(self):
        # pylint: disable=import-outside-toplevel
        from plotly.offline import get_plotlyjs_version  # type: ignore[import-untyped]

        url = f"https://cdn.plot.ly/plotly-{get_plotlyjs_version()}.min.js"

        # pylint: disable=protected-access
        dcc._js_dist.extend(
            [
                {
                    "relative_package_path": "package_data/plotly.min.js",
                    "external_url": url,
                    "namespace": "plotly",
                    "async": "eager",
                }
            ]
        )
        self._plotlyjs_url = url

    @property
    def layout(self) -> Any:
        return self._layout

    @layout.setter
    def layout(self, value: Any):
        _validate.validate_layout_type(value)
        self._layout_is_function = callable(value)
        self._layout = value

        # for using flask.has_request_context() to deliver a full layout for
        # validation inside a layout function - track if a user might be doing this.
        if (
            self._layout_is_function
            and not self.validation_layout
            and not self.config.suppress_callback_exceptions
        ):
            layout_value = self._layout_value()
            _validate.validate_layout(value, layout_value)
            self.validation_layout = layout_value

    def _layout_value(self):
        if self._layout_is_function:
            layout = self._layout()  # type: ignore[reportOptionalCall]
        else:
            layout = self._layout

        # Add any extra components
        if self._extra_components:
            layout = html.Div(children=[layout] + self._extra_components)  # type: ignore[reportArgumentType]

        return layout

    @property
    def index_string(self) -> str:
        return self._index_string

    @index_string.setter
    def index_string(self, value: str) -> None:
        checks = (_re_index_entry, _re_index_config, _re_index_scripts)
        _validate.validate_index("index string", checks, value)
        self._index_string = value

    @with_app_context
    def get_layout(self):
        """Return the resolved layout with all hooks applied.

        This is the canonical way to obtain the app's layout — it
        calls the layout function (if callable), includes extra
        components, and runs layout hooks.
        """
        layout = self._layout_value()
        for hook in self._hooks.get_hooks("layout"):
            layout = hook(layout)
        return layout

    def serve_layout(self):
        # TODO - Set browser cache limit - pass hash into frontend
        return self.backend.make_response(
            to_json(self.get_layout()),
            mimetype="application/json",
        )

    def _get_signing_secret(self) -> bytes:
        """Return the secret used to sign background-callback handles.

        Resolution order:

        1. The server's ``secret_key`` if set (shared across workers when the
           operator configures one, e.g. for Flask-Login).
        2. Otherwise a random secret persisted in the background-callback result
           store, so every worker reads back the same value. This is exactly as
           shared as the callback results themselves, so it works cross-worker
           whenever the deployment is set up for multi-worker background
           callbacks (an explicitly shared cache / broker).
        3. Finally, if no background manager is available, a per-process random
           secret (there are no background handles to verify in that case).
        """
        key = getattr(self.server, "secret_key", None)
        if key:
            return key.encode("utf-8") if isinstance(key, str) else key
        if self._generated_signing_secret is None:
            self._generated_signing_secret = self._resolve_fallback_signing_secret()
        return self._generated_signing_secret

    def _background_managers(self):
        """The background-callback managers this app uses, deterministically
        ordered and de-duplicated by identity."""
        managers = []
        seen = set()
        for candidate in [self._background_manager] + [
            cb.get("manager") for cb in self.callback_map.values()
        ]:
            if candidate is not None and id(candidate) not in seen:
                seen.add(id(candidate))
                managers.append(candidate)
        return managers

    def _resolve_fallback_signing_secret(self) -> bytes:
        def _generate() -> bytes:
            return gen_salt(64).encode("utf-8")

        def _coerce(value) -> bytes:
            if isinstance(value, bytes):
                return value
            if isinstance(value, str):
                return value.encode("utf-8")
            return bytes(value)

        # Persist the secret in the (shared) result store this app's manager
        # uses so all workers read back the same value. Every worker runs the
        # same code, so they resolve the same manager and the same store.
        for manager in self._background_managers():
            try:
                secret = manager.get_or_create_signing_secret(_generate)
            except NotImplementedError:
                continue
            except Exception:  # pylint: disable=broad-except
                # A misbehaving/unreachable store must not break app startup;
                # try the next manager, then fall back to a per-process secret.
                continue
            if secret:
                return _coerce(secret)

        return _generate()

    def _config(self):
        # pieces of config needed by the front end
        config = {
            "url_base_pathname": self.config.url_base_pathname,
            "requests_pathname_prefix": self.config.requests_pathname_prefix,
            "ui": self._dev_tools.ui,
            "props_check": self._dev_tools.props_check,
            "disable_version_check": self._dev_tools.disable_version_check,
            "show_undo_redo": self.config.show_undo_redo,
            "suppress_callback_exceptions": self.config.suppress_callback_exceptions,
            "update_title": self.config.update_title,
            "children_props": ComponentRegistry.children_props,
            "serve_locally": self.config.serve_locally,
            "dash_version": __version__,
            "python_version": sys.version,
            "dash_version_url": DASH_VERSION_URL,
            "ddk_version": ddk_version,
            "plotly_version": plotly_version,
            "validate_callbacks": self._dev_tools.validate_callbacks,
            "csrf_token_name": self.config.csrf_token_name,
            "csrf_header_name": self.config.csrf_header_name,
        }

        # Server-issued, server-signed token for this page load. The renderer
        # echoes it on every callback request; the server binds background
        # callback handles (cacheKey/job) to it so they cannot be forged or
        # replayed from another page load. See dash/_callback_signing.py.
        end_id = gen_salt(24)
        config["end_id"] = _callback_signing.sign(
            self._get_signing_secret(),
            _callback_signing.END_SCOPE,
            end_id,
        )
        if self._plotly_cloud is None:
            if os.getenv("DASH_ENTERPRISE_ENV") == "WORKSPACE":
                # Disable the placeholder button on workspace.
                self._plotly_cloud = True
            else:
                try:
                    # pylint: disable=C0415,W0611
                    import plotly_cloud  # type: ignore  # noqa: F401

                    self._plotly_cloud = True
                except ImportError:
                    self._plotly_cloud = False

        config["plotly_cloud_installed"] = self._plotly_cloud
        if not self.config.serve_locally:
            config["plotlyjs_url"] = self._plotlyjs_url
        if self._dev_tools.hot_reload:
            config["hot_reload"] = {
                # convert from seconds to msec as used by js `setInterval`
                "interval": int(self._dev_tools.hot_reload_interval * 1000),
                "max_retry": self._dev_tools.hot_reload_max_retry,
            }
        if self.validation_layout and not self.config.suppress_callback_exceptions:
            validation_layout = self.validation_layout

            # Add extra components
            if self._extra_components:
                validation_layout = html.Div(
                    children=[validation_layout] + self._extra_components
                )

            config["validation_layout"] = validation_layout

        if self._dev_tools.ui:
            # Add custom dev tools hooks if the ui is activated.
            custom_dev_tools = []
            for hook_dev_tools in self._hooks.get_hooks("dev_tools"):
                props = hook_dev_tools.get("props", {})
                if callable(props):
                    props = props()
                custom_dev_tools.append({**hook_dev_tools, "props": props})
            config["dev_tools"] = custom_dev_tools

        # Add websocket config if backend supports it
        # This enables both global websocket_callbacks and per-callback websocket=True
        if self.backend.websocket_capability:
            config["websocket"] = {
                "enabled": bool(self._websocket_callbacks),
                "url": self.config.requests_pathname_prefix + "_dash-ws-callback",
                "worker_url": self._get_worker_url(),
                "inactivity_timeout": self._websocket_inactivity_timeout,
                "heartbeat_interval": self._websocket_heartbeat_interval,
            }

        return config

    def serve_reload_hash(self):
        _reload = self._hot_reload
        with _reload.lock:
            hard = _reload.hard
            changed = _reload.changed_assets
            _hash = _reload.hash
            _reload.hard = False
            _reload.changed_assets = []

        return self.backend.jsonify(
            {
                "reloadHash": _hash,
                "hard": hard,
                "packages": list(self.registered_paths.keys()),
                "files": list(changed),
            }
        )

    def serve_health(self):
        """
        Health check endpoint for monitoring Dash server status.
        Returns a simple "OK" response with HTTP 200 status.
        """
        return self.backend.make_response("OK", status=200, mimetype="text/plain")

    def _get_worker_url(self) -> str:
        """Get the URL for the WebSocket worker script.

        Returns:
            The fingerprinted URL for the worker script served via component suites.
        """
        relative_path = "dash-renderer/build/dash-ws-worker.js"
        namespace = "dash"

        # Register the path so it can be served
        self.registered_paths[namespace].add(relative_path)

        # Build fingerprinted URL (same pattern as _collect_and_register_resources)
        module_path = os.path.join(
            os.path.dirname(sys.modules[namespace].__file__),  # type: ignore
            relative_path,
        )

        # Use a fallback if the file doesn't exist yet (during development)
        try:
            modified = int(os.stat(module_path).st_mtime)
        except FileNotFoundError:
            modified = 0

        fingerprint = build_fingerprint(relative_path, __version__, modified)
        return f"{self.config.requests_pathname_prefix}_dash-component-suites/{namespace}/{fingerprint}"

    def get_dist(self, libraries: Sequence[str]) -> list:
        dists = []
        for dist_type in ("_js_dist", "_css_dist"):
            resources = ComponentRegistry.get_resources(dist_type, libraries)
            srcs = self._collect_and_register_resources(resources, False)
            for src in srcs:
                dists.append(dict(type=dist_type, url=src))
        return dists

    # pylint: disable=too-many-branches
    def _collect_and_register_resources(
        self, resources, include_async=True, url_attr="src"
    ):
        # now needs the app context.
        # template in the necessary component suite JS bundles
        # add the version number of the package as a query parameter
        # for cache busting
        def _relative_url_path(relative_package_path="", namespace=""):
            if any(
                relative_package_path.startswith(x + "/")
                for x in ["dcc", "html", "dash_table"]
            ):
                relative_package_path = relative_package_path.replace("dash.", "")
                version = importlib.import_module(
                    f"{namespace}.{os.path.split(relative_package_path)[0]}"
                ).__version__
            else:
                version = importlib.import_module(namespace).__version__

            module_path = os.path.join(  # type: ignore[reportCallIssue]
                os.path.dirname(sys.modules[namespace].__file__),  # type: ignore[reportCallIssue]
                relative_package_path,
            )

            modified = int(os.stat(module_path).st_mtime)

            fingerprint = build_fingerprint(relative_package_path, version, modified)
            return f"{self.config.requests_pathname_prefix}_dash-component-suites/{namespace}/{fingerprint}"

        srcs = []
        for resource in resources:
            is_dynamic_resource = resource.get("dynamic", False)
            is_async = resource.get("async") is not None
            excluded = not include_async and is_async

            if "relative_package_path" in resource:
                paths = resource["relative_package_path"]
                paths = [paths] if isinstance(paths, str) else paths

                for rel_path in paths:
                    if any(x in rel_path for x in ["dcc", "html", "dash_table"]):
                        rel_path = rel_path.replace("dash.", "")

                    self.registered_paths[resource["namespace"]].add(rel_path)

                    if not is_dynamic_resource and not excluded:
                        url = _relative_url_path(
                            relative_package_path=rel_path,
                            namespace=resource["namespace"],
                        )
                        if "attributes" in resource:
                            srcs.append({url_attr: url, **resource["attributes"]})
                        else:
                            srcs.append(url)
            elif "external_url" in resource:
                if not is_dynamic_resource and not excluded:
                    urls = (
                        [resource["external_url"]]
                        if isinstance(resource["external_url"], str)
                        else resource["external_url"]
                    )
                    for url in urls:
                        if "attributes" in resource:
                            srcs.append({url_attr: url, **resource["attributes"]})
                        else:
                            srcs.append(url)
            elif "absolute_path" in resource:
                raise Exception("Serving files from absolute_path isn't supported yet")
            elif "asset_path" in resource:
                static_url = self.get_asset_url(resource["asset_path"])
                url_with_cache = static_url + f"?m={resource['ts']}"
                # Import .mjs files with type=module script tag
                if static_url.endswith(".mjs"):
                    attrs = {url_attr: url_with_cache, "type": "module"}
                    if "attributes" in resource:
                        attrs.update(resource["attributes"])
                    srcs.append(attrs)
                else:
                    if "attributes" in resource:
                        srcs.append(
                            {url_attr: url_with_cache, **resource["attributes"]}
                        )
                    else:
                        srcs.append(url_with_cache)

        return srcs

    # pylint: disable=protected-access
    def _generate_css_dist_html(self):
        external_links = self.config.external_stylesheets
        links = self._collect_and_register_resources(
            self.css.get_all_css()
            + self.css._resources._filter_resources(self._hooks.hooks._css_dist),
            url_attr="href",
        )

        return "\n".join(
            [
                (
                    format_tag("link", link, opened=True)
                    if isinstance(link, dict)
                    else f'<link rel="stylesheet" href="{link}">'
                )
                for link in (external_links + links)
            ]
        )

    def _generate_scripts_html(self) -> str:
        # Dash renderer has dependencies like React which need to be rendered
        # before every other script. However, the dash renderer bundle
        # itself needs to be rendered after all of the component's
        # scripts have rendered.
        # The rest of the scripts can just be loaded after React but before
        # dash renderer.
        # pylint: disable=protected-access

        mode = "dev" if self._dev_tools["props_check"] is True else "prod"

        deps = [
            {
                key: value[mode] if isinstance(value, dict) else value
                for key, value in js_dist_dependency.items()
            }
            for js_dist_dependency in _dash_renderer._js_dist_dependencies
        ]
        dev = self._dev_tools.serve_dev_bundles
        srcs = (
            self._collect_and_register_resources(
                self.scripts._resources._filter_resources(deps, dev_bundles=dev)  # type: ignore[reportArgumentType]
            )
            + self.config.external_scripts
            + self._collect_and_register_resources(
                self.scripts.get_all_scripts(dev_bundles=dev)
                + self.scripts._resources._filter_resources(
                    _dash_renderer._js_dist, dev_bundles=dev
                )
                + self.scripts._resources._filter_resources(
                    dcc._js_dist, dev_bundles=dev
                )
                + self.scripts._resources._filter_resources(
                    html._js_dist, dev_bundles=dev
                )
                + self.scripts._resources._filter_resources(
                    dash_table._js_dist, dev_bundles=dev  # type: ignore
                )
                + self.scripts._resources._filter_resources(
                    self._hooks.hooks._js_dist, dev_bundles=dev
                )
            )
        )

        self._inline_scripts.extend(_callback.GLOBAL_INLINE_SCRIPTS)
        _callback.GLOBAL_INLINE_SCRIPTS.clear()

        return "\n".join(
            [
                (
                    format_tag("script", src)
                    if isinstance(src, dict)
                    else f'<script src="{src}"></script>'
                )
                for src in srcs
            ]
            + [f"<script>{src}</script>" for src in self._inline_scripts]
        )

    def _generate_config_html(self) -> str:
        return f'<script id="_dash-config" type="application/json">{to_json(self._config())}</script>'

    def _generate_renderer(self) -> str:
        return f'<script id="_dash-renderer" type="application/javascript">{self.renderer}</script>'

    def _generate_meta(self):
        meta_tags = []
        has_ie_compat = any(
            x.get("http-equiv", "") == "X-UA-Compatible" for x in self.config.meta_tags
        )
        has_charset = any("charset" in x for x in self.config.meta_tags)
        has_viewport = any(x.get("name") == "viewport" for x in self.config.meta_tags)

        if not has_ie_compat:
            meta_tags.append({"http-equiv": "X-UA-Compatible", "content": "IE=edge"})
        if not has_charset:
            meta_tags.append({"charset": "UTF-8"})
        if not has_viewport:
            meta_tags.append(
                {"name": "viewport", "content": "width=device-width, initial-scale=1"}
            )

        return meta_tags + self.config.meta_tags

    def index(self, *_args, **_kwargs):
        scripts = self._generate_scripts_html()
        css = self._generate_css_dist_html()
        config = self._generate_config_html()
        metas = self._generate_meta()
        renderer = self._generate_renderer()
        title = self.title
        # Refactored: direct access to global request adapter
        request = self.backend.request_adapter()

        if self.use_pages and self.config.include_pages_meta and request:
            metas = _page_meta_tags(self, request) + metas

        if self._favicon:
            favicon_mod_time = os.path.getmtime(
                os.path.join(self.config.assets_folder, self._favicon)
            )
            favicon_url = f"{self.get_asset_url(self._favicon)}?m={favicon_mod_time}"
        else:
            prefix = self.config.requests_pathname_prefix
            favicon_url = f"{prefix}_favicon.ico?v={__version__}"

        favicon = format_tag(
            "link",
            {"rel": "icon", "type": "image/x-icon", "href": favicon_url},
            opened=True,
        )

        tags = "\n      ".join(
            format_tag("meta", x, opened=True, sanitize=True) for x in metas
        )

        index = self.interpolate_index(
            metas=tags,
            title=title,
            css=css,
            config=config,
            scripts=scripts,
            app_entry=_app_entry,
            favicon=favicon,
            renderer=renderer,
        )

        for hook in self._hooks.get_hooks("index"):
            index = hook(index)

        checks = (
            _re_index_entry_id,
            _re_index_config_id,
            _re_index_scripts_id,
            _re_renderer_scripts_id,
        )
        _validate.validate_index("index", checks, index)
        return index

    def interpolate_index(
        self,
        metas="",
        title="",
        css="",
        config="",
        scripts="",
        app_entry="",
        favicon="",
        renderer="",
    ):
        """Called to create the initial HTML string that is loaded on page.
        Override this method to provide you own custom HTML.

        :Example:

            class MyDash(dash.Dash):
                def interpolate_index(self, **kwargs):
                    return '''<!DOCTYPE html>
                    <html>
                        <head>
                            <title>My App</title>
                        </head>
                        <body>
                            <div id="custom-header">My custom header</div>
                            {app_entry}
                            {config}
                            {scripts}
                            {renderer}
                            <div id="custom-footer">My custom footer</div>
                        </body>
                    </html>'''.format(app_entry=kwargs.get('app_entry'),
                                      config=kwargs.get('config'),
                                      scripts=kwargs.get('scripts'),
                                      renderer=kwargs.get('renderer'))

        :param metas: Collected & formatted meta tags.
        :param title: The title of the app.
        :param css: Collected & formatted css dependencies as <link> tags.
        :param config: Configs needed by dash-renderer.
        :param scripts: Collected & formatted scripts tags.
        :param renderer: A script tag that instantiates the DashRenderer.
        :param app_entry: Where the app will render.
        :param favicon: A favicon <link> tag if found in assets folder.
        :return: The interpolated HTML string for the index.
        """
        return interpolate_str(
            self.index_string,
            metas=metas,
            title=title,
            css=css,
            config=config,
            scripts=scripts,
            favicon=favicon,
            renderer=renderer,
            app_entry=app_entry,
        )

    @with_app_context
    def dependencies(self):
        return self.backend.make_response(
            to_json(self._callback_list),
            content_type="application/json",
        )

    def clientside_callback(self, clientside_function, *args, **kwargs):
        """Create a callback that updates the output by calling a clientside
        (JavaScript) function instead of a Python function.

        Unlike `@app.callback`, `clientside_callback` is not a decorator:
        it takes either a
        `dash.dependencies.ClientsideFunction(namespace, function_name)`
        argument that describes which JavaScript function to call
        (Dash will look for the JavaScript function at
        `window.dash_clientside[namespace][function_name]`), or it may take
        a string argument that contains the clientside function source.

        For example, when using a `dash.dependencies.ClientsideFunction`:
        ```
        app.clientside_callback(
            ClientsideFunction('my_clientside_library', 'my_function'),
            Output('my-div' 'children'),
            [Input('my-input', 'value'),
             Input('another-input', 'value')]
        )
        ```

        With this signature, Dash's front-end will call
        `window.dash_clientside.my_clientside_library.my_function` with the
        current values of the `value` properties of the components `my-input`
        and `another-input` whenever those values change.

        Include a JavaScript file by including it your `assets/` folder. The
        file can be named anything but you'll need to assign the function's
        namespace to the `window.dash_clientside` namespace. For example,
        this file might look:
        ```
        window.dash_clientside = window.dash_clientside || {};
        window.dash_clientside.my_clientside_library = {
            my_function: function(input_value_1, input_value_2) {
                return (
                    parseFloat(input_value_1, 10) +
                    parseFloat(input_value_2, 10)
                );
            }
        }
        ```

        Alternatively, you can pass the JavaScript source directly to
        `clientside_callback`. In this case, the same example would look like:
        ```
        app.clientside_callback(
            '''
            function(input_value_1, input_value_2) {
                return (
                    parseFloat(input_value_1, 10) +
                    parseFloat(input_value_2, 10)
                );
            }
            ''',
            Output('my-div' 'children'),
            [Input('my-input', 'value'),
             Input('another-input', 'value')]
        )
        ```

        The last, optional argument `prevent_initial_call` causes the callback
        not to fire when its outputs are first added to the page. Defaults to
        `False` unless `prevent_initial_callbacks=True` at the app level.
        """
        return _callback.register_clientside_callback(
            self._callback_list,
            self.callback_map,
            self.config.prevent_initial_callbacks,
            self._inline_scripts,
            clientside_function,
            *args,
            **kwargs,
        )

    def callback(self, *_args, **_kwargs) -> Callable[..., Any]:
        """
        Normally used as a decorator, `@app.callback` provides a server-side
        callback relating the values of one or more `Output` items to one or
        more `Input` items which will trigger the callback when they change,
        and optionally `State` items which provide additional information but
        do not trigger the callback directly.

        The last, optional argument `prevent_initial_call` causes the callback
        not to fire when its outputs are first added to the page. Defaults to
        `False` unless `prevent_initial_callbacks=True` at the app level.


        """
        return _callback.callback(
            *_args,
            config_prevent_initial_callbacks=self.config.prevent_initial_callbacks,
            callback_list=self._callback_list,
            callback_map=self.callback_map,
            callback_api_paths=self.callback_api_paths,
            **_kwargs,
        )

    def _inputs_to_vals(self, inputs):
        return inputs_to_vals(inputs)

    # pylint: disable=R0915
    def _initialize_context(self, body: CallbackExecutionBody):
        """Initialize the global context for the request."""
        adapter = self.backend.request_adapter()
        g = AttributeDict({})
        g.inputs_list = body.get("inputs", [])
        g.states_list = body.get("state", [])
        g.outputs_list = body.get("outputs", [])
        g.input_values = inputs_to_dict(g.inputs_list)
        g.state_values = inputs_to_dict(g.states_list)
        g.triggered_inputs = [
            {"prop_id": x, "value": g.input_values.get(x)}
            for x in body.get("changedPropIds", [])
        ]
        g.dash_response = self.backend.response_adapter()
        g.cookies = dict(adapter.cookies)
        g.headers = dict(adapter.headers)
        g.args = adapter.args
        g.path = adapter.full_path
        g.remote = adapter.remote_addr
        g.origin = adapter.origin
        g.updated_props = {}
        return g

    def _prepare_callback(self, g, body: CallbackExecutionBody):
        """Prepare callback-related data."""
        output = body["output"]
        try:
            cb = self.callback_map[output]
            func = cb["callback"]
            g.background_callback_manager = (
                cb.get("manager") or self._background_manager
            )
            g.ignore_register_page = cb.get("background", False)

            # Add args_grouping
            inputs_state_indices = cb["inputs_state_indices"]
            inputs_state = convert_to_AttributeDict(g.inputs_list + g.states_list)

            if cb.get("no_output"):
                g.outputs_list = []
            elif not g.outputs_list:
                # Legacy support for older renderers
                split_callback_id(output)

            # Update args_grouping attributes
            for s in inputs_state:
                # check for pattern matching: list of inputs or state
                if isinstance(s, list):
                    for pattern_match_g in s:
                        update_args_group(
                            pattern_match_g, body.get("changedPropIds", [])
                        )
                update_args_group(s, body.get("changedPropIds", []))

            g.args_grouping, g.using_args_grouping = self._prepare_grouping(
                inputs_state, inputs_state_indices
            )
            g.outputs_grouping, g.using_outputs_grouping = self._prepare_grouping(
                g.outputs_list, cb.get("outputs_indices", [])
            )
        except KeyError as e:
            raise KeyError(f"Callback function not found for output '{output}'.") from e
        return func

    def _prepare_grouping(self, data_list, indices):
        """Prepare grouping logic for inputs or outputs."""
        if not isinstance(data_list, list):
            flat_data = [data_list]
        else:
            flat_data = data_list

        if len(flat_data) > 0:
            grouping = map_grouping(lambda ind: flat_data[ind], indices)
            using_grouping = not isinstance(indices, int) and indices != list(
                range(grouping_len(indices))
            )
        else:
            grouping, using_grouping = [], False

        return grouping, using_grouping

    def _execute_callback(self, func, args, outputs_list, g):
        """Execute the callback with the prepared arguments."""
        g.custom_data = AttributeDict({})

        for hook in self._hooks.get_hooks("custom_data"):
            g.custom_data[hook.data["namespace"]] = hook(g)

        # noinspection PyArgumentList
        partial_func = functools.partial(
            func,
            *args,
            outputs_list=outputs_list,
            background_callback_manager=g.background_callback_manager,
            callback_context=g,
            app=self,
            app_on_error=self._on_error,
            app_use_async=self._use_async,
        )
        return partial_func

    def _setup_server(self):
        if self._got_first_request["setup_server"]:
            return

        self._got_first_request["setup_server"] = True

        # Apply _force_eager_loading overrides from modules
        eager_loading = self.config.eager_loading
        for module_name in ComponentRegistry.registry:
            module = sys.modules[module_name]
            eager = getattr(module, "_force_eager_loading", False)
            eager_loading = eager_loading or eager

        # Update eager_loading settings
        self.scripts.config.eager_loading = eager_loading

        if self.config.include_assets_files:
            self._walk_assets_directory()

        if not self.layout and self.use_pages:
            self.layout = page_container

        _validate.validate_layout(self.layout, self._layout_value())

        self._generate_scripts_html()
        self._generate_css_dist_html()

        # Copy over global callback data structures assigned with `dash.callback`
        for k in list(_callback.GLOBAL_CALLBACK_MAP):
            if k in self.callback_map:
                raise DuplicateCallback(
                    f"The callback `{k}` provided with `dash.callback` was already "
                    "assigned with `app.callback`."
                )

            self.callback_map[k] = _callback.GLOBAL_CALLBACK_MAP.pop(k)

        self._callback_list.extend(_callback.GLOBAL_CALLBACK_LIST)

        # For each callback function, if the hidden parameter uses the default value None,
        # replace it with the actual value of the self.config.hide_all_callbacks.
        self._callback_list = [
            (
                {**_callback, "hidden": self.config.get("hide_all_callbacks", False)}
                if _callback.get("hidden") is None
                else _callback
            )
            for _callback in self._callback_list
        ]

        _callback.GLOBAL_CALLBACK_LIST.clear()

        _validate.validate_background_callbacks(self.callback_map)

        cancels = {}

        for callback in self.callback_map.values():
            background = callback.get("background")
            if not background:
                continue
            if "cancel_inputs" in background:
                cancel = background.pop("cancel_inputs")
                for c in cancel:
                    cancels[c] = background.get("manager")

        if cancels:
            for cancel_input, manager in cancels.items():
                # pylint: disable=cell-var-from-loop
                @self.callback(
                    Output(cancel_input.component_id, "id"),
                    cancel_input,
                    prevent_initial_call=True,
                    manager=manager,
                )
                def cancel_call(*_):
                    job_ids = callback_context.args.getlist("cancelJob")
                    executor = _callback.context_value.get().background_callback_manager
                    if job_ids:
                        secret = self._get_signing_secret()
                        end_id = _callback.get_request_end_id(secret)
                        scope = _callback_signing.job_scope(end_id)
                        for job_id in job_ids:
                            job = _callback_signing.unsign(secret, scope, job_id)
                            if job is not None:
                                executor.terminate_job(job)
                    return no_update

    def _add_assets_resource(self, url_path, file_path):
        res = {"asset_path": url_path, "filepath": file_path}
        if self.config.assets_external_path:
            res["external_url"] = self.get_asset_url(url_path.lstrip("/"))
        self._assets_files.append(file_path)
        return res

    def _walk_assets_directory(self):
        walk_dir = self.config.assets_folder
        slash_splitter = re.compile(r"[\\/]+")
        ignore_str = self.config.assets_ignore
        ignore_path_list = self.config.assets_path_ignore
        ignore_filter = re.compile(ignore_str) if ignore_str else None
        ignore_path_filters = [
            re.compile(ignore_path)
            for ignore_path in (ignore_path_list or [])
            if ignore_path
        ]

        for current, _, files in sorted(os.walk(walk_dir)):
            if current == walk_dir:
                base = ""
                s = ""
            else:
                s = current.replace(walk_dir, "").lstrip("\\").lstrip("/")
                splitted = slash_splitter.split(s)
                if len(splitted) > 1:
                    base = "/".join(slash_splitter.split(s))
                else:
                    base = splitted[0]

            # Check if any level of current path matches ignore path
            if s and any(
                ignore_path_filter.search(x)
                for ignore_path_filter in ignore_path_filters
                for x in s.split(os.path.sep)
            ):
                pass
            else:
                if ignore_filter:
                    files_gen = (x for x in files if not ignore_filter.search(x))
                else:
                    files_gen = files

                for f in sorted(files_gen):
                    path = "/".join([base, f]) if base else f

                    full = os.path.join(current, f)

                    if f.endswith("js"):
                        self.scripts.append_script(
                            self._add_assets_resource(path, full)
                        )
                    elif f.endswith("css"):
                        self.css.append_css(self._add_assets_resource(path, full))  # type: ignore[reportArgumentType]
                    elif f == "favicon.ico":
                        self._favicon = path

    @staticmethod
    def _invalid_resources_handler(err):
        return err.args[0], 404

    def csp_hashes(self, hash_algorithm="sha256") -> Sequence[str]:
        """Calculates CSP hashes (sha + base64) of all inline scripts, such that
        one of the biggest benefits of CSP (disallowing general inline scripts)
        can be utilized together with Dash clientside callbacks (inline scripts).

        Calculate these hashes after all inline callbacks are defined,
        and add them to your CSP headers before starting the server, for example
        with the flask-talisman package from PyPI:

        flask_talisman.Talisman(app.server, content_security_policy={
            "default-src": "'self'",
            "script-src": ["'self'"] + app.csp_hashes()
        })

        :param hash_algorithm: One of the recognized CSP hash algorithms ('sha256', 'sha384', 'sha512').
        :return: List of CSP hash strings of all inline scripts.
        """

        HASH_ALGORITHMS = ["sha256", "sha384", "sha512"]
        if hash_algorithm not in HASH_ALGORITHMS:
            raise ValueError(
                "Possible CSP hash algorithms: " + ", ".join(HASH_ALGORITHMS)
            )

        method = getattr(hashlib, hash_algorithm)

        def _hash(script):
            return base64.b64encode(method(script.encode("utf-8")).digest()).decode(
                "utf-8"
            )

        self._inline_scripts.extend(_callback.GLOBAL_INLINE_SCRIPTS)
        _callback.GLOBAL_INLINE_SCRIPTS.clear()

        return [
            f"'{hash_algorithm}-{_hash(script)}'"
            for script in (self._inline_scripts + [self.renderer])
        ]

    def get_asset_url(self, path: str) -> str:
        """
        Return the URL for the provided `path` in the assets directory.

        If `assets_external_path` is set, `get_asset_url` returns
        `assets_external_path` + `assets_url_path` + `path`, where
        `path` is the path passed to `get_asset_url`.

        Otherwise, `get_asset_url` returns
        `requests_pathname_prefix` + `assets_url_path` + `path`, where
        `path` is the path passed to `get_asset_url`.

        Use `get_asset_url` in an app to access assets at the correct location
        in different environments. In a deployed app on Dash Enterprise,
        `requests_pathname_prefix` is the app name. For an app called "my-app",
        `app.get_asset_url("image.png")` would return:

        ```
        /my-app/assets/image.png
        ```

        While the same app running locally, without
        `requests_pathname_prefix` set, would return:

        ```
        /assets/image.png
        ```
        """
        return _get_paths.app_get_asset_url(self.config, path)

    def get_relative_path(self, path):
        """
        Return a path with `requests_pathname_prefix` prefixed before it.
        Use this function when specifying local URL paths that will work
        in environments regardless of what `requests_pathname_prefix` is.
        In some deployment environments, like Dash Enterprise,
        `requests_pathname_prefix` is set to the application name,
        e.g. `my-dash-app`.
        When working locally, `requests_pathname_prefix` might be unset and
        so a relative URL like `/page-2` can just be `/page-2`.
        However, when the app is deployed to a URL like `/my-dash-app`, then
        `app.get_relative_path('/page-2')` will return `/my-dash-app/page-2`.
        This can be used as an alternative to `get_asset_url` as well with
        `app.get_relative_path('/assets/logo.png')`

        Use this function with `app.strip_relative_path` in callbacks that
        deal with `dcc.Location` `pathname` routing.
        That is, your usage may look like:
        ```
        app.layout = html.Div([
            dcc.Location(id='url'),
            html.Div(id='content')
        ])
        @app.callback(Output('content', 'children'), [Input('url', 'pathname')])
        def display_content(path):
            page_name = app.strip_relative_path(path)
            if not page_name:  # None or ''
                return html.Div([
                    dcc.Link(href=app.get_relative_path('/page-1')),
                    dcc.Link(href=app.get_relative_path('/page-2')),
                ])
            elif page_name == 'page-1':
                return chapters.page_1
            if page_name == "page-2":
                return chapters.page_2
        ```
        """
        return _get_paths.app_get_relative_path(
            self.config.requests_pathname_prefix, path
        )

    def strip_relative_path(self, path: str) -> Union[str, None]:
        """
        Return a path with `requests_pathname_prefix` and leading and trailing
        slashes stripped from it. Also, if None is passed in, None is returned.
        Use this function with `get_relative_path` in callbacks that deal
        with `dcc.Location` `pathname` routing.
        That is, your usage may look like:
        ```
        app.layout = html.Div([
            dcc.Location(id='url'),
            html.Div(id='content')
        ])
        @app.callback(Output('content', 'children'), [Input('url', 'pathname')])
        def display_content(path):
            page_name = app.strip_relative_path(path)
            if not page_name:  # None or ''
                return html.Div([
                    dcc.Link(href=app.get_relative_path('/page-1')),
                    dcc.Link(href=app.get_relative_path('/page-2')),
                ])
            elif page_name == 'page-1':
                return chapters.page_1
            if page_name == "page-2":
                return chapters.page_2
        ```
        Note that `chapters.page_1` will be served if the user visits `/page-1`
        _or_ `/page-1/` since `strip_relative_path` removes the trailing slash.

        Also note that `strip_relative_path` is compatible with
        `get_relative_path` in environments where `requests_pathname_prefix` set.
        In some deployment environments, like Dash Enterprise,
        `requests_pathname_prefix` is set to the application name, e.g. `my-dash-app`.
        When working locally, `requests_pathname_prefix` might be unset and
        so a relative URL like `/page-2` can just be `/page-2`.
        However, when the app is deployed to a URL like `/my-dash-app`, then
        `app.get_relative_path('/page-2')` will return `/my-dash-app/page-2`

        The `pathname` property of `dcc.Location` will return '`/my-dash-app/page-2`'
        to the callback.
        In this case, `app.strip_relative_path('/my-dash-app/page-2')`
        will return `'page-2'`

        For nested URLs, slashes are still included:
        `app.strip_relative_path('/page-1/sub-page-1/')` will return
        `page-1/sub-page-1`
        ```
        """
        return _get_paths.app_strip_relative_path(
            self.config.requests_pathname_prefix, path
        )

    @staticmethod
    def add_startup_route(
        name: str, view_func: RouteCallable, methods: Sequence[Literal["POST", "GET"]]
    ) -> None:
        """
        Add a route to the app to be initialized at the end of Dash initialization.
        Use this if the package requires a route to be added to the app, and you will not need to worry about at what point to add it.

        :param name: The name of the route. eg "my-new-url/path".
        :param view_func: The function to call when the route is requested. The function should return a JSON serializable object.
        :param methods: The HTTP methods that the route should respond to. eg ["GET", "POST"] or either one.
        """
        if not isinstance(name, str) or name.startswith("/"):
            raise ValueError("name must be a string and should not start with '/'")

        if not callable(view_func):
            raise ValueError("view_func must be callable")

        valid_methods = {"POST", "GET"}
        if not set(methods).issubset(valid_methods):
            raise ValueError(f"methods should only contain {valid_methods}")

        if any(route[0] == name for route in Dash.STARTUP_ROUTES):
            raise ValueError(f"Route name '{name}' is already in use.")

        Dash.STARTUP_ROUTES.append((name, view_func, methods))

    def setup_startup_routes(self) -> None:
        """
        Initialize the startup routes stored in STARTUP_ROUTES.
        """
        for _name, _view_func, _methods in self.STARTUP_ROUTES:
            self._add_url(
                f"_dash_startup_route/{_name}",
                with_app_context_factory(_view_func, self),
                _methods,
            )
        self.STARTUP_ROUTES = []

    def _setup_dev_tools(self, **kwargs):
        debug = kwargs.get("debug", False)
        dev_tools = self._dev_tools = AttributeDict()

        for attr in (
            "ui",
            "props_check",
            "serve_dev_bundles",
            "hot_reload",
            "silence_routes_logging",
            "prune_errors",
            "validate_callbacks",
        ):
            dev_tools[attr] = get_combined_config(
                attr, kwargs.get(attr, None), default=debug
            )

        for attr, _type, default in (
            ("hot_reload_interval", float, 3),
            ("hot_reload_watch_interval", float, 0.5),
            ("hot_reload_max_retry", int, 8),
        ):
            dev_tools[attr] = _type(
                get_combined_config(attr, kwargs.get(attr, None), default=default)
            )

        dev_tools["disable_version_check"] = get_combined_config(
            "disable_version_check",
            kwargs.get("disable_version_check", None),
            default=False,
        )

        return dev_tools

    def enable_dev_tools(  # pylint: disable=too-many-branches
        self,
        debug: Optional[bool] = None,
        dev_tools_ui: Optional[bool] = None,
        dev_tools_props_check: Optional[bool] = None,
        dev_tools_serve_dev_bundles: Optional[bool] = None,
        dev_tools_hot_reload: Optional[bool] = None,
        dev_tools_hot_reload_interval: Optional[int] = None,
        dev_tools_hot_reload_watch_interval: Optional[int] = None,
        dev_tools_hot_reload_max_retry: Optional[int] = None,
        dev_tools_silence_routes_logging: Optional[bool] = None,
        dev_tools_disable_version_check: Optional[bool] = None,
        dev_tools_prune_errors: Optional[bool] = None,
        dev_tools_validate_callbacks: Optional[bool] = None,
        first_run: bool = True,
    ) -> bool:
        """Activate the dev tools, called by `run`. If your application
        is served by wsgi and you want to activate the dev tools, you can call
        this method out of `__main__`.

        All parameters can be set by environment variables as listed.
        Values provided here take precedence over environment variables.

        Available dev_tools environment variables:

            - DASH_DEBUG
            - DASH_UI
            - DASH_PROPS_CHECK
            - DASH_SERVE_DEV_BUNDLES
            - DASH_HOT_RELOAD
            - DASH_HOT_RELOAD_INTERVAL
            - DASH_HOT_RELOAD_WATCH_INTERVAL
            - DASH_HOT_RELOAD_MAX_RETRY
            - DASH_SILENCE_ROUTES_LOGGING
            - DASH_DISABLE_VERSION_CHECK
            - DASH_PRUNE_ERRORS
            - DASH_VALIDATE_CALLBACKS

        :param debug: Enable/disable all the dev tools unless overridden by the
            arguments or environment variables. Default is ``True`` when
            ``enable_dev_tools`` is called directly, and ``False`` when called
            via ``run``. env: ``DASH_DEBUG``
        :type debug: bool

        :param dev_tools_ui: Show the dev tools UI. env: ``DASH_UI``
        :type dev_tools_ui: bool

        :param dev_tools_props_check: Validate the types and values of Dash
            component props. env: ``DASH_PROPS_CHECK``
        :type dev_tools_props_check: bool

        :param dev_tools_serve_dev_bundles: Serve the dev bundles. Production
            bundles do not necessarily include all the dev tools code.
            env: ``DASH_SERVE_DEV_BUNDLES``
        :type dev_tools_serve_dev_bundles: bool

        :param dev_tools_hot_reload: Activate hot reloading when app, assets,
            and component files change. env: ``DASH_HOT_RELOAD``
        :type dev_tools_hot_reload: bool

        :param dev_tools_hot_reload_interval: Interval in seconds for the
            client to request the reload hash. Default 3.
            env: ``DASH_HOT_RELOAD_INTERVAL``
        :type dev_tools_hot_reload_interval: float

        :param dev_tools_hot_reload_watch_interval: Interval in seconds for the
            server to check asset and component folders for changes.
            Default 0.5. env: ``DASH_HOT_RELOAD_WATCH_INTERVAL``
        :type dev_tools_hot_reload_watch_interval: float

        :param dev_tools_hot_reload_max_retry: Maximum number of failed reload
            hash requests before failing and displaying a pop up. Default 8.
            env: ``DASH_HOT_RELOAD_MAX_RETRY``
        :type dev_tools_hot_reload_max_retry: int

        :param dev_tools_silence_routes_logging: Silence the route logging for the
            web server (werkzeug for Flask, hypercorn for Quart, uvicorn for FastAPI).
            Enabled with debugging by default because hot reload hash checks generate
            a lot of requests.
            env: ``DASH_SILENCE_ROUTES_LOGGING``
        :type dev_tools_silence_routes_logging: bool

        :param dev_tools_disable_version_check: Silence the upgrade
            notification to prevent making requests to the Dash server.
            env: ``DASH_DISABLE_VERSION_CHECK``
        :type dev_tools_disable_version_check: bool

        :param dev_tools_prune_errors: Reduce tracebacks to just user code,
            stripping out Flask and Dash pieces. Only available with debugging.
            `True` by default, set to `False` to see the complete traceback.
            env: ``DASH_PRUNE_ERRORS``
        :type dev_tools_prune_errors: bool

        :param dev_tools_validate_callbacks: Check for circular callback
            dependencies and raise an error if any are found. env: ``DASH_VALIDATE_CALLBACKS``
        :type dev_tools_validate_callbacks: bool

        :return: debug
        """
        if debug is None:
            debug = get_combined_config("debug", None, True)

        dev_tools = self._setup_dev_tools(
            debug=debug,
            ui=dev_tools_ui,
            props_check=dev_tools_props_check,
            serve_dev_bundles=dev_tools_serve_dev_bundles,
            hot_reload=dev_tools_hot_reload,
            hot_reload_interval=dev_tools_hot_reload_interval,
            hot_reload_watch_interval=dev_tools_hot_reload_watch_interval,
            hot_reload_max_retry=dev_tools_hot_reload_max_retry,
            silence_routes_logging=dev_tools_silence_routes_logging,
            disable_version_check=dev_tools_disable_version_check,
            prune_errors=dev_tools_prune_errors,
            validate_callbacks=dev_tools_validate_callbacks,
        )

        if dev_tools.silence_routes_logging:
            # Silence route logging based on backend type
            backend_type = getattr(self.backend, "server_type", "flask")
            if backend_type == "flask":
                logging.getLogger("werkzeug").setLevel(logging.ERROR)
            elif backend_type == "quart":
                # Quart uses hypercorn as its ASGI server
                logging.getLogger("hypercorn.access").setLevel(logging.ERROR)
                logging.getLogger("hypercorn.error").setLevel(logging.ERROR)
            elif backend_type == "fastapi":
                # FastAPI uses uvicorn as its ASGI server
                logging.getLogger("uvicorn.access").setLevel(logging.ERROR)
                logging.getLogger("uvicorn.error").setLevel(logging.ERROR)

        if dev_tools.hot_reload:
            _reload = self._hot_reload
            _reload.hash = generate_hash()

            # find_spec should return None on __main__ but doesn't
            # on some Python versions https://bugs.python.org/issue14710
            packages = [
                find_spec(x)
                for x in list(ComponentRegistry.registry)
                if x != "__main__"
            ]

            # # additional condition to account for AssertionRewritingHook object
            # # loader when running pytest

            if "_pytest" in sys.modules:
                from _pytest.assertion.rewrite import (  # pylint: disable=import-outside-toplevel
                    AssertionRewritingHook,  # type: ignore[reportPrivateImportUsage]
                )

                for index, package in enumerate(packages):
                    if package and isinstance(package.loader, AssertionRewritingHook):
                        dash_spec = importlib.util.find_spec("dash")  # type: ignore[reportAttributeAccess]
                        packages[index] = dash_spec

            component_packages_dist = []
            for package in packages:
                if package and isinstance(package, ModuleSpec):
                    # For ModuleSpec objects, use submodule_search_locations or origin
                    if package.submodule_search_locations:
                        component_packages_dist.append(
                            package.submodule_search_locations[0]
                        )
                    elif package.origin:
                        component_packages_dist.append(os.path.dirname(package.origin))
                    else:
                        component_packages_dist.append("")
                else:
                    # Fallback for non-ModuleSpec objects (shouldn't happen with find_spec)
                    if hasattr(package, "path"):
                        component_packages_dist.append(os.path.dirname(package.path))  # type: ignore
                    elif hasattr(package, "_path"):
                        component_packages_dist.append(os.path.dirname(package._path[0]))  # type: ignore # pylint: disable=protected-access
                    elif hasattr(package, "filename"):
                        component_packages_dist.append(package.filename)  # type: ignore
                    else:
                        component_packages_dist.append("")

            for i, package in enumerate(packages):
                if package and isinstance(package, ModuleSpec):
                    # Check origin for ModuleSpec objects
                    pkg_dir = (
                        package.submodule_search_locations[0]
                        if package.submodule_search_locations
                        else os.path.dirname(package.origin)
                        if package.origin
                        else None
                    )
                    if pkg_dir and "dash/dash" in pkg_dir:
                        component_packages_dist[i : i + 1] = [
                            os.path.join(pkg_dir, x)
                            for x in ["dcc", "html", "dash_table"]
                        ]
                elif hasattr(package, "path") and "dash/dash" in os.path.dirname(
                    package.path  # type: ignore[union-attr]
                ):
                    component_packages_dist[i : i + 1] = [
                        os.path.join(os.path.dirname(package.path), x)  # type: ignore[union-attr]
                        for x in ["dcc", "html", "dash_table"]
                    ]

            _reload.watch_thread = threading.Thread(
                target=lambda: _watch.watch(
                    [self.config.assets_folder] + component_packages_dist,
                    self._on_assets_change,
                    sleep_time=dev_tools.hot_reload_watch_interval,
                )
            )
            _reload.watch_thread.daemon = True
            _reload.watch_thread.start()

        if debug:
            if jupyter_dash.active:
                jupyter_dash.configure_callback_exception_handling(
                    self, dev_tools.prune_errors
                )
            secret = gen_salt(20)
            self.backend.register_prune_error_handler(secret, dev_tools.prune_errors)

        if debug and dev_tools.ui:
            self.backend.register_timing_hooks(first_run)

        if (
            debug
            and dev_tools.serve_dev_bundles
            and not self.scripts.config.serve_locally
        ):
            # Dev bundles only works locally.
            self.scripts.config.serve_locally = True
            print(
                "WARNING: dev bundles requested with serve_locally=False.\n"
                "This is not supported, switching to serve_locally=True"
            )

        return debug

    # noinspection PyProtectedMember
    def _on_assets_change(self, filename, modified, deleted):
        _reload = self._hot_reload
        with _reload.lock:
            _reload.hard = True
            _reload.hash = generate_hash()

            if self.config.assets_folder in filename:
                asset_path = (
                    os.path.relpath(
                        filename,
                        os.path.commonprefix([self.config.assets_folder, filename]),
                    )
                    .replace("\\", "/")
                    .lstrip("/")
                )

                _reload.changed_assets.append(
                    {
                        "url": self.get_asset_url(asset_path),
                        "modified": int(modified),
                        "is_css": filename.endswith("css"),
                    }
                )

                if filename not in self._assets_files and not deleted:
                    res = self._add_assets_resource(asset_path, filename)
                    if filename.endswith("js"):
                        self.scripts.append_script(res)
                    elif filename.endswith("css"):
                        self.css.append_css(res)  # type: ignore[reportArgumentType]

                if deleted:
                    if filename in self._assets_files:
                        self._assets_files.remove(filename)

                    def delete_resource(resources):
                        to_delete = None
                        for r in resources:
                            if r.get("asset_path") == asset_path:
                                to_delete = r
                                break
                        if to_delete:
                            resources.remove(to_delete)

                    if filename.endswith("js"):
                        # pylint: disable=protected-access
                        delete_resource(self.scripts._resources._resources)
                    elif filename.endswith("css"):
                        # pylint: disable=protected-access
                        delete_resource(self.css._resources._resources)

    # pylint: disable=too-many-branches
    def run(
        self,
        host: Optional[str] = None,
        port: Optional[Union[str, int]] = None,
        proxy: Optional[str] = None,
        debug: Optional[bool] = None,
        hide_all_callbacks: bool = False,
        jupyter_mode: Optional[JupyterDisplayMode] = None,
        jupyter_width: str = "100%",
        jupyter_height: int = 650,
        jupyter_server_url: Optional[str] = None,
        dev_tools_ui: Optional[bool] = None,
        dev_tools_props_check: Optional[bool] = None,
        dev_tools_serve_dev_bundles: Optional[bool] = None,
        dev_tools_hot_reload: Optional[bool] = None,
        dev_tools_hot_reload_interval: Optional[int] = None,
        dev_tools_hot_reload_watch_interval: Optional[int] = None,
        dev_tools_hot_reload_max_retry: Optional[int] = None,
        dev_tools_silence_routes_logging: Optional[bool] = None,
        dev_tools_disable_version_check: Optional[bool] = None,
        dev_tools_prune_errors: Optional[bool] = None,
        dev_tools_validate_callbacks: Optional[bool] = None,
        **flask_run_options,
    ):
        """Start the flask server in local mode, you should not run this on a
        production server, use gunicorn/waitress instead.

        If a parameter can be set by an environment variable, that is listed
        too. Values provided here take precedence over environment variables.

        :param host: Host IP used to serve the application, default to "127.0.0.1"
            env: ``HOST``
        :type host: string

        :param port: Port used to serve the application, default to "8050"
            env: ``PORT``
        :type port: int

        :param proxy: If this application will be served to a different URL
            via a proxy configured outside of Python, you can list it here
            as a string of the form ``"{input}::{output}"``, for example:
            ``"http://0.0.0.0:8050::https://my.domain.com"``
            so that the startup message will display an accurate URL.
            env: ``DASH_PROXY``
        :type proxy: string

        :param debug: Set Flask debug mode and enable dev tools.
            env: ``DASH_DEBUG``
        :type debug: bool

        :param debug: Enable/disable all the dev tools unless overridden by the
            arguments or environment variables. Default is ``True`` when
            ``enable_dev_tools`` is called directly, and ``False`` when called
            via ``run``. env: ``DASH_DEBUG``
        :type debug: bool

        :param hide_all_callbacks: Default ``False``: Sets the default value of
            ``hidden`` for all callbacks added to the app. Normally all callbacks
            are visible in the devtools callbacks tab. You can set this for
            individual callbacks by setting ``hidden`` in their definitions, or set
            it ``True`` here in which case you must explicitly set it ``False`` for
            those callbacks you wish to remain visible in the devtools callbacks tab.
        :type hide_all_callbacks: bool

        :param dev_tools_ui: Show the dev tools UI. env: ``DASH_UI``
        :type dev_tools_ui: bool

        :param dev_tools_props_check: Validate the types and values of Dash
            component props. env: ``DASH_PROPS_CHECK``
        :type dev_tools_props_check: bool

        :param dev_tools_serve_dev_bundles: Serve the dev bundles. Production
            bundles do not necessarily include all the dev tools code.
            env: ``DASH_SERVE_DEV_BUNDLES``
        :type dev_tools_serve_dev_bundles: bool

        :param dev_tools_hot_reload: Activate hot reloading when app, assets,
            and component files change. env: ``DASH_HOT_RELOAD``
        :type dev_tools_hot_reload: bool

        :param dev_tools_hot_reload_interval: Interval in seconds for the
            client to request the reload hash. Default 3.
            env: ``DASH_HOT_RELOAD_INTERVAL``
        :type dev_tools_hot_reload_interval: float

        :param dev_tools_hot_reload_watch_interval: Interval in seconds for the
            server to check asset and component folders for changes.
            Default 0.5. env: ``DASH_HOT_RELOAD_WATCH_INTERVAL``
        :type dev_tools_hot_reload_watch_interval: float

        :param dev_tools_hot_reload_max_retry: Maximum number of failed reload
            hash requests before failing and displaying a pop up. Default 8.
            env: ``DASH_HOT_RELOAD_MAX_RETRY``
        :type dev_tools_hot_reload_max_retry: int

        :param dev_tools_silence_routes_logging: Silence the route logging for the
            web server (werkzeug for Flask, hypercorn for Quart, uvicorn for FastAPI).
            Enabled with debugging by default because hot reload hash checks generate
            a lot of requests.
            env: ``DASH_SILENCE_ROUTES_LOGGING``
        :type dev_tools_silence_routes_logging: bool

        :param dev_tools_disable_version_check: Silence the upgrade
            notification to prevent making requests to the Dash server.
            env: ``DASH_DISABLE_VERSION_CHECK``
        :type dev_tools_disable_version_check: bool

        :param dev_tools_prune_errors: Reduce tracebacks to just user code,
            stripping out Flask and Dash pieces. Only available with debugging.
            `True` by default, set to `False` to see the complete traceback.
            env: ``DASH_PRUNE_ERRORS``
        :type dev_tools_prune_errors: bool

        :param dev_tools_validate_callbacks: Check for circular callback
            dependencies and raise an error if any are found. env: ``DASH_VALIDATE_CALLBACKS``
        :type dev_tools_validate_callbacks: bool

        :param jupyter_mode: How to display the application when running
            inside a jupyter notebook.

        :param jupyter_width: Determine the width of the output cell
            when displaying inline in jupyter notebooks.
        :type jupyter_width: str

        :param jupyter_height: Height of app when displayed using
            jupyter_mode="inline"
        :type jupyter_height: int

        :param jupyter_server_url: Custom server url to display
            the app in jupyter notebook.

        :param flask_run_options: Given to `Flask.run`

        :return:
        """

        # Update self.config.hide_all_callbacks
        self.config.update({"hide_all_callbacks": hide_all_callbacks})

        if debug is None:
            debug = get_combined_config("debug", None, False)

        debug = self.enable_dev_tools(
            debug,
            dev_tools_ui,
            dev_tools_props_check,
            dev_tools_serve_dev_bundles,
            dev_tools_hot_reload,
            dev_tools_hot_reload_interval,
            dev_tools_hot_reload_watch_interval,
            dev_tools_hot_reload_max_retry,
            dev_tools_silence_routes_logging,
            dev_tools_disable_version_check,
            dev_tools_prune_errors,
            dev_tools_validate_callbacks,
        )

        # Evaluate the env variables at runtime

        if "CONDA_PREFIX" in os.environ:
            # Some conda systems has issue with setting the host environment
            # to an invalid hostname.
            # Related issue: https://github.com/plotly/dash/issues/3069
            host = host or "127.0.0.1"
        else:
            host = host or os.getenv("HOST", "127.0.0.1")
        assert host
        port = port or os.getenv("PORT", "8050")
        proxy = proxy or os.getenv("DASH_PROXY")

        # Verify port value
        try:
            port = int(port)  # type: ignore
            assert port in range(1, 65536)
        except Exception as e:
            e.args = (f"Expecting an integer from 1 to 65535, found port={repr(port)}",)
            raise

        # so we only see the "Running on" message once with hot reloading
        # https://stackoverflow.com/a/57231282/9188800
        if os.getenv("WERKZEUG_RUN_MAIN") != "true":
            ssl_context = flask_run_options.get("ssl_context")
            protocol = "https" if ssl_context else "http"
            path = self.config.requests_pathname_prefix

            if proxy:
                served_url, proxied_url = map(urlparse, proxy.split("::"))

                def verify_url_part(served_part, url_part, part_name):
                    if served_part != url_part:
                        raise ProxyError(
                            f"""
                            {part_name}: {url_part} is incompatible with the proxy:
                                {proxy}
                            To see your app at {proxied_url.geturl()},
                            you must use {part_name}: {served_part}
                            """
                        )

                verify_url_part(served_url.scheme, protocol, "protocol")
                verify_url_part(served_url.hostname, host, "host")
                verify_url_part(served_url.port, port, "port")

                display_url = (
                    proxied_url.scheme,
                    proxied_url.hostname,
                    f":{proxied_url.port}" if proxied_url.port else "",
                    path,
                )
            else:
                display_url = (protocol, host, f":{port}", path)

            if not jupyter_dash or not jupyter_dash.in_ipython:
                self.logger.info("Dash is running on %s://%s%s%s\n", *display_url)
                if self._enable_mcp:
                    self.logger.info(
                        " * MCP available at %s://%s%s%s%s\n",
                        *display_url[:3],
                        self.config.routes_pathname_prefix,
                        self._mcp_path,
                    )

        if self.config.extra_hot_reload_paths:
            extra_files = flask_run_options["extra_files"] = []
            for path in self.config.extra_hot_reload_paths:
                if os.path.isdir(path):
                    for dirpath, _, filenames in os.walk(path):
                        for fn in filenames:
                            extra_files.append(os.path.join(dirpath, fn))
                elif os.path.isfile(path):
                    extra_files.append(path)

        if jupyter_dash.active:
            jupyter_dash.run_app(
                self,
                mode=jupyter_mode,
                width=jupyter_width,
                height=jupyter_height,
                host=host,
                port=port,
                server_url=jupyter_server_url,
            )
        else:
            self.backend.run(
                dash_app=self, host=host, port=port, debug=debug, **flask_run_options
            )

    def enable_pages(self) -> None:
        if not self.use_pages:
            return
        if self.pages_folder:
            _import_layouts_from_pages(self.config.pages_folder)

        # Async version
        async def router_async():
            if self._got_first_request["pages"]:
                return
            self._got_first_request["pages"] = True

            inputs = {
                "pathname_": Input(_ID_LOCATION, "pathname"),
                "search_": Input(_ID_LOCATION, "search"),
            }
            inputs.update(self.routing_callback_inputs)

            @self.callback(
                Output(_ID_CONTENT, "children"),
                Output(_ID_STORE, "data"),
                inputs=inputs,
                prevent_initial_call=True,
                hidden=True,
            )
            async def update(pathname_, search_, **states):
                query_parameters = _parse_query_string(search_)
                page, path_variables = _path_to_page(
                    self.strip_relative_path(pathname_)
                )
                if page == {}:
                    for module, page in _pages.PAGE_REGISTRY.items():
                        if module.split(".")[-1] == "not_found_404":
                            layout = page["layout"]
                            title = page["title"]
                            break
                    else:
                        layout = html.H1("404 - Page not found")
                        title = self.title
                else:
                    layout = page.get("layout", "")
                    title = page["title"]

                if callable(layout):
                    layout = await execute_async_function(
                        layout,
                        **{**(path_variables or {}), **query_parameters, **states},
                    )
                if callable(title):
                    title = await execute_async_function(
                        title, **{**(path_variables or {})}
                    )
                return layout, {"title": title}

            _validate.check_for_duplicate_pathnames(_pages.PAGE_REGISTRY)
            _validate.validate_registry(_pages.PAGE_REGISTRY)

            if not self.config.suppress_callback_exceptions:

                async def get_layouts():
                    return [
                        await execute_async_function(page["layout"])
                        if callable(page["layout"])
                        else page["layout"]
                        for page in _pages.PAGE_REGISTRY.values()
                    ]

                layouts = await get_layouts()
                # pylint: disable=not-callable
                layouts += [self.layout() if callable(self.layout) else self.layout]
                self.validation_layout = html.Div(layouts)
                if _ID_CONTENT not in self.validation_layout:
                    raise Exception("`dash.page_container` not found in the layout")

            self.clientside_callback(
                """
                function(data) {
                    document.title = data.title
                }
                """,
                Output(_ID_DUMMY, "children"),
                Input(_ID_STORE, "data"),
                hidden=True,
            )

        # Sync version
        def router_sync():
            if self._got_first_request["pages"]:
                return
            self._got_first_request["pages"] = True

            inputs = {
                "pathname_": Input(_ID_LOCATION, "pathname"),
                "search_": Input(_ID_LOCATION, "search"),
            }
            inputs.update(self.routing_callback_inputs)

            @self.callback(
                Output(_ID_CONTENT, "children"),
                Output(_ID_STORE, "data"),
                inputs=inputs,
                prevent_initial_call=True,
                hidden=True,
            )
            def update(pathname_, search_, **states):
                query_parameters = _parse_query_string(search_)
                page, path_variables = _path_to_page(
                    self.strip_relative_path(pathname_)
                )
                if page == {}:
                    for module, page in _pages.PAGE_REGISTRY.items():
                        if module.split(".")[-1] == "not_found_404":
                            layout = page["layout"]
                            title = page["title"]
                            break
                    else:
                        layout = html.H1("404 - Page not found")
                        title = self.title
                else:
                    layout = page.get("layout", "")
                    title = page["title"]

                if callable(layout):
                    layout = layout(
                        **{**(path_variables or {}), **query_parameters, **states}
                    )
                if callable(title):
                    title = title(**(path_variables or {}))
                return layout, {"title": title}

            _validate.check_for_duplicate_pathnames(_pages.PAGE_REGISTRY)
            _validate.validate_registry(_pages.PAGE_REGISTRY)

            if not self.config.suppress_callback_exceptions:
                layout = self.layout
                if not isinstance(layout, list):
                    # pylint: disable=not-callable
                    layout = [self.layout() if callable(self.layout) else self.layout]
                self.validation_layout = html.Div(
                    [
                        page["layout"]() if callable(page["layout"]) else page["layout"]
                        for page in _pages.PAGE_REGISTRY.values()
                    ]
                    + layout
                )
                if _ID_CONTENT not in self.validation_layout:
                    raise Exception("`dash.page_container` not found in the layout")

            self.clientside_callback(
                """
                function(data) {
                    document.title = data.title
                }
                """,
                Output(_ID_DUMMY, "children"),
                Input(_ID_STORE, "data"),
            )

        if self._use_async:
            self.backend.before_request(router_async)
        else:
            self.backend.before_request(router_sync)

    def __call__(self, *args, **kwargs):
        return self.backend.__call__(*args, **kwargs)
