# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import math
import numbers
from dataclasses import dataclass
from typing import TYPE_CHECKING, Final, Literal, TypeAlias, cast, overload

from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.js_number import JSNumber, JSNumberBoundsException
from streamlit.elements.lib.layout_utils import (
    WidthWithoutContent,
    create_layout_config,
)
from streamlit.elements.lib.policies import (
    check_widget_policies,
    maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
    Key,
    LabelVisibility,
    compute_and_register_element_id,
    get_label_visibility_proto_value,
    to_key,
)
from streamlit.errors import (
    StreamlitInvalidMinMaxError,
    StreamlitInvalidNumberFormatError,
    StreamlitJSNumberBoundsError,
    StreamlitMixedNumericTypesError,
    StreamlitValueAboveMaxError,
    StreamlitValueBelowMinError,
)
from streamlit.logger import get_logger
from streamlit.proto.NumberInput_pb2 import NumberInput as NumberInputProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
    BindOption,
    OnChangeMode,
    PersistStateOption,
    WidgetArgs,
    WidgetCallback,
    WidgetKwargs,
    get_session_state,
    register_widget,
    validate_on_change_mode,
)
from streamlit.string_util import to_help_str, validate_icon_or_emoji

if TYPE_CHECKING:
    from streamlit.delta_generator import DeltaGenerator

_LOGGER: Final = get_logger(__name__)

Number: TypeAlias = int | float


@dataclass
class NumberInputSerde:
    value: Number | None
    data_type: int
    min_value: Number
    max_value: Number

    def serialize(self, v: Number | None) -> Number | None:
        return v

    def deserialize(self, ui_value: Number | None) -> Number | None:
        val: Number | None = ui_value if ui_value is not None else self.value

        if val is not None:
            if self.data_type == NumberInputProto.INT:
                val = int(val)
            # Reset to default if outside [min_value, max_value]. This
            # rejects out-of-range values seeded from URL query params;
            # a no-op for frontend values since the UI enforces bounds.
            # Returning the default triggers _seed_widget_from_url's
            # "deserialized == default" check, which clears the URL param.
            if (
                (self.data_type == NumberInputProto.FLOAT and not math.isfinite(val))
                or val < self.min_value
                or val > self.max_value
            ):
                return self.value

        return val


class NumberInputMixin:
    # Each numeric group has one overload for a concrete value (or the "min"
    # default) and one for value=None. A single constrained TypeVar covering
    # both would leave ty and pyright inferring int | None where the runtime
    # always returns int.
    # If "min_value: int" is given and all other numerical inputs are
    #   "int"s or not provided (value optionally being "min"), return "int"
    @overload
    def number_input(
        self,
        label: str,
        min_value: int,
        max_value: int | None = None,
        value: int | Literal["min"] = "min",
        step: int | None = None,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> int: ...

    # If "min_value: int, value: None" is given and all other numerical inputs
    #   are "int"s or not provided, return "int | None"
    @overload
    def number_input(
        self,
        label: str,
        min_value: int,
        max_value: int | None = None,
        value: None = None,
        step: int | None = None,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> int | None: ...

    # If "max_value: int" is given and all other numerical inputs are
    #   "int"s or not provided (value optionally being "min"), return "int"
    @overload
    def number_input(
        self,
        label: str,
        min_value: None = None,
        *,
        max_value: int,
        value: int | Literal["min"] = "min",
        step: int | None = None,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> int: ...

    # If "max_value: int, value=None" is given and all other numerical inputs
    #   are "int"s or not provided, return "int | None"
    @overload
    def number_input(
        self,
        label: str,
        min_value: None = None,
        *,
        max_value: int,
        value: None = None,
        step: int | None = None,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> int | None: ...

    # If "value=int" is given and all other numerical inputs are "int"s
    #   or not provided, return "int"
    @overload
    def number_input(
        self,
        label: str,
        min_value: int | None = None,
        max_value: int | None = None,
        *,
        value: int,
        step: int | None = None,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> int: ...

    # If "step=int" is given and all other numerical inputs are "int"s
    #   or not provided (value optionally being "min"), return "int"
    @overload
    def number_input(
        self,
        label: str,
        min_value: None = None,
        max_value: None = None,
        value: int | Literal["min"] = "min",
        *,
        step: int,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> int: ...

    # If "step=int, value=None" is given and all other numerical inputs
    #   are "int"s or not provided, return "int | None"
    @overload
    def number_input(
        self,
        label: str,
        min_value: None = None,
        max_value: None = None,
        value: None = None,
        *,
        step: int,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> int | None: ...

    # If all numerical inputs are floats (with value optionally being "min")
    #   or are not provided, return "float"
    @overload
    def number_input(
        self,
        label: str,
        min_value: float | None = None,
        max_value: float | None = None,
        value: float | Literal["min"] = "min",
        step: float | None = None,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> float: ...

    # If only "value=None" is given and none of the other numerical inputs
    #   are "int"s, return "float | None"
    @overload
    def number_input(
        self,
        label: str,
        min_value: float | None = None,
        max_value: float | None = None,
        value: None = None,
        step: float | None = None,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> float | None: ...

    @gather_metrics("number_input")
    def number_input(
        self,
        label: str,
        min_value: Number | None = None,
        max_value: Number | None = None,
        value: Number | Literal["min"] | None = "min",
        step: Number | None = None,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> Number | None:
        r"""Display a numeric input widget.

        .. note::
            Integer values exceeding +/- ``(1<<53) - 1`` cannot be accurately
            stored or returned by the widget due to serialization constraints
            between the Python server and JavaScript client. You must handle
            such numbers as floats, leading to a loss in precision.

        Parameters
        ----------
        label : str
            A short label explaining to the user what this input is for.
            The label can optionally contain GitHub-flavored Markdown of the
            following types: Bold, Italics, Strikethroughs, Inline Code, Links,
            and Images. Images display like icons, with a max height equal to
            the font height.

            Unsupported Markdown elements are unwrapped so only their children
            (text contents) render. Common block-level Markdown (headings,
            lists, blockquotes) is automatically escaped and displays as
            literal text in labels.

            See the ``body`` parameter of |st.markdown|_ for additional,
            supported Markdown directives.

            For accessibility reasons, you should never set an empty label, but
            you can hide it with ``label_visibility`` if needed. In the future,
            we may disallow empty labels by raising an exception.

            .. |st.markdown| replace:: ``st.markdown``
            .. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown

        min_value : int, float, or None
            The minimum permitted value.
            If this is ``None`` (default), there will be no minimum for float
            values and a minimum of ``- (1<<53) + 1`` for integer values.

        max_value : int, float, or None
            The maximum permitted value.
            If this is ``None`` (default), there will be no maximum for float
            values and a maximum of ``(1<<53) - 1`` for integer values.

        value : int, float, "min" or None
            The value of this widget when it first renders. If this is
            ``"min"`` (default), the initial value is ``min_value`` unless
            ``min_value`` is ``None``. If ``min_value`` is ``None``, the widget
            initializes with a value of ``0.0`` or ``0``.

            If ``value`` is ``None``, the widget will initialize with no value
            and return ``None`` until the user provides input.

        step : int, float, or None
            The stepping interval.
            Defaults to 1 if the value is an int, 0.01 otherwise.
            If the value is not specified, the format parameter will be used.

        format : str or None
            A printf-style format string controlling how the interface should
            display numbers. The output must be purely numeric. This does not
            impact the return value of the widget. For more information about
            the formatting specification, see `sprintf.js
            <https://github.com/alexei/sprintf.js?tab=readme-ov-file#format-specification>`_.

            For example, ``format="%0.1f"`` adjusts the displayed decimal
            precision to only show one digit after the decimal.

        key : str, int, or None
            An optional string or integer to use as the unique key for
            the widget. If this is ``None`` (default), a key will be
            generated for the widget based on the values of the other
            parameters. No two widgets may have the same key. Assigning
            a key stabilizes the widget's identity and preserves its
            state across reruns even when other parameters change.

            A key lets you read or update the widget's value via
            ``st.session_state[key]``. For more details, see `Widget
            behavior <https://docs.streamlit.io/develop/concepts/architecture/widget-behavior>`_.

            Additionally, if ``key`` is provided, it will be used as a
            CSS class name prefixed with ``st-key-``.

        help : str or None
            A tooltip that gets displayed next to the widget label. Streamlit
            only displays the tooltip when ``label_visibility="visible"``. If
            this is ``None`` (default), no tooltip is displayed.

            The tooltip can optionally contain GitHub-flavored Markdown,
            including the Markdown directives described in the ``body``
            parameter of ``st.markdown``.

        on_change : callable, "rerun", "ignore", or None
            How the number input should respond to value changes. This controls
            whether or not Streamlit reruns the app when the user interacts
            with the number input. ``on_change`` can be one of the following:

            - ``"rerun"`` (default): Streamlit will rerun the app when the
              user commits a new value (pressing Enter, blurring the field,
              clicking the ``+/-`` buttons, pressing the up or down arrow
              keys, or clearing the value).

            - ``"ignore"``: Streamlit will not rerun the app when the user
              commits a new value. The number input still updates in the UI.
              The new value is available on the next rerun triggered by
              something else, such as another widget interaction. Ignored
              commits are held in the browser and are lost if the page is
              refreshed before that rerun, unless ``bind="query-params"``
              is set (see ``bind``). Inside ``st.form``, this has no
              effect: the form already defers all commits until submit.

            - A ``callable``: Streamlit will rerun the app and execute the
              ``callable`` as a callback function before the rest of the app.

            - ``None``: This is the same as ``on_change="rerun"``. This value
              exists for backwards compatibility and shouldn't be used.

        args : list or tuple
            An optional list or tuple of args to pass to the callback.

        kwargs : dict
            An optional dict of kwargs to pass to the callback.

        placeholder : str or None
            An optional string displayed when the number input is empty.
            If None, no placeholder is displayed.

        disabled : bool
            An optional boolean that disables the number input if set to
            ``True``. The default is ``False``.

        required : bool
            An optional boolean that requires a non-empty value if set to
            ``True``. The default is ``False``. If this is ``True``, empty
            values (``None``) cannot be submitted.

            Outside a form, clearing the field does not rerun the app, and
            the last committed value is kept. Inside a form, submission is
            blocked until the field has a value. The widget still returns
            its default value until the user provides input.

            ``required=True`` does not change the widget's default. A
            number input still starts at ``min_value`` (or ``0``) unless
            you pass ``value=None``. Use ``value=None`` together with
            ``required=True`` for an empty required field.

            .. note::
               This check runs in the user's browser and can be bypassed.
               If requiredness is security-relevant, you must also check the
               value on the server (in your app code) after it is submitted.

        label_visibility : "visible", "hidden", or "collapsed"
            The visibility of the label. The default is ``"visible"``. If this
            is ``"hidden"``, Streamlit displays an empty spacer instead of the
            label, which can help keep the widget aligned with other widgets.
            If this is ``"collapsed"``, Streamlit displays no label or spacer.

        icon : str, None
            An optional emoji or icon to display within the input field to the
            left of the value. If ``icon`` is ``None`` (default), no icon is
            displayed. If ``icon`` is a string, the following options are
            valid:

            - A single-character emoji. For example, you can set ``icon="🚨"``
              or ``icon="🔥"``. Emoji short codes are not supported.

            - An icon from the Material Symbols library (rounded style) in the
              format ``":material/icon_name:"`` where "icon_name" is the name
              of the icon in snake case.

              For example, ``icon=":material/thumb_up:"`` will display the
              Thumb Up icon. Find additional icons in the `Material Symbols \
              <https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
              font library.

            - ``"spinner"``: Displays a spinner as an icon.

        width : "stretch" or int
            The width of the number input widget. This can be one of the
            following:

            - ``"stretch"`` (default): The width of the widget matches the
              width of the parent container.
            - An integer specifying the width in pixels: The widget has a
              fixed width. If the specified width is greater than the width of
              the parent container, the width of the widget matches the width
              of the parent container.

        bind : "query-params" or None
            Binding mode for syncing the widget's value with a URL query
            parameter. If this is ``None`` (default), the widget's value
            is not synced to the URL. When this is set to
            ``"query-params"``, changes to the widget update the URL, and
            the widget can be initialized or updated through a query
            parameter in the URL. This requires ``key`` to be set. The
            key is used as the query parameter name.

            When the widget's value equals its default, the query
            parameter is removed from the URL to keep it clean. A bound
            query parameter can't be set or deleted through
            ``st.query_params``; it can only be programmatically changed
            through ``st.session_state``.

            Invalid query parameter values are ignored and removed
            from the URL. If ``value`` is ``None``, an empty query
            parameter (e.g., ``?my_key=``) clears the widget.

            When ``on_change="ignore"``, the URL is updated as soon as the
            value is committed (Enter, blur, the ``+/-`` buttons, the up or
            down arrow keys, or clearing the value); typing alone does not
            update it. As with widgets inside a form, the URL can show a
            value that Python hasn't received yet. Python receives the new
            value on the next rerun, so a page load or share uses the
            updated URL value.

        persist_state : "page", "session", or None
            How long to preserve the widget's value when it isn't rendered.
            If this is ``None`` (default), the value is lost when the widget
            stops being rendered or the user switches pages. If this is
            ``"page"``, the value is preserved only while the user stays on the
            page where the widget is defined (for example, while the widget is
            conditionally hidden); it is discarded on a page switch and is not
            restored if the user returns to the page. If this is ``"session"``,
            the value is preserved for the entire session, including across
            page switches, so it returns when the user navigates back. This
            requires ``key`` to be set. If ``bind="query-params"`` is also set,
            the binding takes precedence: the value is stored in the URL, so it
            persists across page switches regardless of the ``persist_state``
            scope.

        Returns
        -------
        int or float or None
            The current value of the numeric input widget or ``None`` if the widget
            is empty. The return type will match the data type of the value parameter.

        Examples
        --------
        >>> import streamlit as st
        >>>
        >>> number = st.number_input("Insert a number")
        >>> st.write("The current number is ", number)

        .. output::
           https://doc-number-input.streamlit.app/
           height: 260px

        To initialize an empty number input, use ``None`` as the value:

        >>> import streamlit as st
        >>>
        >>> number = st.number_input(
        ...     "Insert a number", value=None, placeholder="Type a number..."
        ... )
        >>> st.write("The current number is ", number)

        .. output::
           https://doc-number-input-empty.streamlit.app/
           height: 260px

        """
        ctx = get_script_run_ctx()
        return self._number_input(
            label=label,
            min_value=min_value,
            max_value=max_value,
            value=value,
            step=step,
            format=format,
            key=key,
            help=help,
            on_change=on_change,
            args=args,
            kwargs=kwargs,
            placeholder=placeholder,
            disabled=disabled,
            required=required,
            label_visibility=label_visibility,
            icon=icon,
            width=width,
            bind=bind,
            persist_state=persist_state,
            ctx=ctx,
        )

    def _number_input(
        self,
        label: str,
        min_value: Number | None = None,
        max_value: Number | None = None,
        value: Number | Literal["min"] | None = "min",
        step: Number | None = None,
        format: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
        ctx: ScriptRunContext | None = None,
    ) -> Number | None:
        key = to_key(key)

        on_change_callback = validate_on_change_mode(
            on_change,
            supported_modes=("rerun", "ignore"),
        )

        check_widget_policies(
            self.dg,
            key,
            on_change_callback,
            default_value=value if value != "min" else None,
        )
        label = maybe_raise_label_warnings(label, label_visibility)

        element_id = compute_and_register_element_id(
            "number_input",
            user_key=key,
            # `required` is hashed only for unkeyed widgets: toggling it cannot make a
            # stored value incompatible, so it is omitted from keyed identity.
            key_as_main_identity=True,
            dg=self.dg,
            label=label,
            min_value=min_value,
            max_value=max_value,
            value=value,
            step=step,
            format=format,
            help=help,
            placeholder=None if placeholder is None else str(placeholder),
            icon=icon,
            width=width,
            required=required,
        )

        # Ensure that all arguments are of the same type.
        number_input_args = [min_value, max_value, value, step]

        all_int_args = all(
            isinstance(a, (numbers.Integral, type(None), str))
            for a in number_input_args
        )

        all_float_args = all(
            isinstance(a, (float, type(None), str)) for a in number_input_args
        )

        if not all_int_args and not all_float_args:
            raise StreamlitMixedNumericTypesError(
                value=value, min_value=min_value, max_value=max_value, step=step
            )

        session_state = get_session_state().filtered_state
        if key is not None and key in session_state and session_state[key] is None:
            value = None

        if value == "min":
            if min_value is not None:
                value = min_value
            elif all_int_args and all_float_args:
                value = 0.0  # if no values are provided, defaults to float
            elif all_int_args:
                value = 0
            else:
                value = 0.0

        int_value = isinstance(value, numbers.Integral)
        float_value = isinstance(value, float)

        if value is None:
            if all_int_args and not all_float_args:
                # Select int type if all relevant args are ints:
                int_value = True
            else:
                # Otherwise, defaults to float:
                float_value = True

        # Use default format depending on value type if format was not provided:
        number_format = ("%d" if int_value else "%0.2f") if format is None else format

        # A type/format mismatch only affects how the value is displayed, so it
        # is reported to the developer via the console rather than the app.
        if number_format in {"%d", "%u", "%i"} and float_value:
            _LOGGER.warning(
                "st.number_input value has type float, but format %s displays as integer.",
                number_format,
                stack_info=True,
            )
        elif number_format[-1] == "f" and int_value:
            _LOGGER.warning(
                "st.number_input value has type int so is displayed as int despite "
                "format string %s.",
                number_format,
                stack_info=True,
            )

        if step is None:
            step = 1 if int_value else 0.01

        try:
            float(number_format % 2)
        except (TypeError, ValueError):
            raise StreamlitInvalidNumberFormatError(number_format)

        # Ensure that the value matches arguments' types.
        all_ints = int_value and all_int_args

        # Compare the bounds against each other before comparing them to the
        # value: the checks below are skipped when `value` is None, and they
        # would otherwise report a misleading value error for inverted bounds.
        if min_value is not None and max_value is not None and min_value > max_value:
            raise StreamlitInvalidMinMaxError(min_value, max_value)

        if min_value is not None and value is not None and min_value > value:
            raise StreamlitValueBelowMinError(value=value, min_value=min_value)

        if max_value is not None and value is not None and max_value < value:
            raise StreamlitValueAboveMaxError(value=value, max_value=max_value)

        # Record whether the user explicitly provided bounds before we backfill
        # unset bounds with JS safe-number sentinels below. The frontend uses
        # these flags to decide which range-validation message to show.
        has_user_min = min_value is not None
        has_user_max = max_value is not None

        # Bounds checks. JSNumber produces human-readable exceptions that
        # we simply re-package as StreamlitAPIExceptions.
        try:
            if all_ints:
                if min_value is not None:
                    JSNumber.validate_int_bounds(int(min_value), "`min_value`")
                else:
                    # Issue 6740: If min_value not provided, set default to minimum safe integer
                    # to avoid JS issues from smaller numbers entered via UI
                    min_value = JSNumber.MIN_SAFE_INTEGER
                if max_value is not None:
                    JSNumber.validate_int_bounds(int(max_value), "`max_value`")
                else:
                    # See note above - set default to max safe integer
                    max_value = JSNumber.MAX_SAFE_INTEGER
                if step is not None:
                    JSNumber.validate_int_bounds(int(step), "`step`")
                if value is not None:
                    JSNumber.validate_int_bounds(int(value), "`value`")
            else:
                if min_value is not None:
                    JSNumber.validate_float_bounds(min_value, "`min_value`")
                else:
                    # See note above
                    min_value = JSNumber.MIN_NEGATIVE_VALUE
                if max_value is not None:
                    JSNumber.validate_float_bounds(max_value, "`max_value`")
                else:
                    # See note above
                    max_value = JSNumber.MAX_VALUE
                if step is not None:
                    JSNumber.validate_float_bounds(step, "`step`")
                if value is not None:
                    JSNumber.validate_float_bounds(value, "`value`")
        except JSNumberBoundsException as e:
            raise StreamlitJSNumberBoundsError(str(e))

        data_type = NumberInputProto.INT if all_ints else NumberInputProto.FLOAT

        number_input_proto = NumberInputProto()
        number_input_proto.id = element_id
        number_input_proto.data_type = data_type
        number_input_proto.label = label
        if value is not None:
            number_input_proto.default = value
        if placeholder is not None:
            number_input_proto.placeholder = str(placeholder)
        number_input_proto.form_id = current_form_id(self.dg)
        number_input_proto.disabled = disabled
        number_input_proto.required = required
        number_input_proto.label_visibility.value = get_label_visibility_proto_value(
            label_visibility
        )

        if help is not None:
            number_input_proto.help = to_help_str(help)

        # min_value/max_value are guaranteed to be non-None here (unset bounds
        # were backfilled with JS sentinels above). We always send them as the
        # input's min/max attributes, but has_min/has_max only reflect bounds
        # the user actually set.
        if min_value is not None:
            number_input_proto.min = min_value
            number_input_proto.has_min = has_user_min

        if max_value is not None:
            number_input_proto.max = max_value
            number_input_proto.has_max = has_user_max

        if step is not None:
            number_input_proto.step = step

        number_input_proto.format = number_format

        if icon is not None:
            number_input_proto.icon = validate_icon_or_emoji(icon)

        # Set query param key if bound
        if bind == "query-params" and key is not None:
            number_input_proto.query_param_key = str(key)

        if isinstance(on_change, str) and on_change == "ignore":
            number_input_proto.ignore_rerun = True

        # min_value and max_value are guaranteed to be Number (not None) after
        # the JSNumber defaults above.
        serde = NumberInputSerde(
            value,
            data_type,
            min_value,
            max_value,
        )
        widget_state = register_widget(
            number_input_proto.id,
            on_change_handler=on_change_callback,
            args=args,
            kwargs=kwargs,
            deserializer=serde.deserialize,
            serializer=serde.serialize,
            ctx=ctx,
            value_type="double_value",
            disabled=disabled,
            bind=bind,
            persist_state=persist_state,
            # Clearable when value=None: the widget can be in an empty state,
            # so ?key= (empty URL param) should clear the widget to None.
            clearable=(value is None),
        )

        # Validate the current value against the new min/max bounds.
        # If the value is no longer valid (outside bounds), reset to default.
        # This handles the case where min_value/max_value change dynamically
        # and the previously entered value is no longer within bounds.
        # Note: This is NOT redundant with the serde bounds check — the
        # serde only runs on serialized values (URL seeding, frontend
        # submissions), while this catches already-deserialized values
        # carried over from a previous rerun with different bounds.
        # Both paths reset to the widget's default value for consistency.
        current_value = widget_state.value
        value_needs_reset = False

        # Check if the current value is outside the new bounds.
        if current_value is not None and (
            (number_input_proto.has_min and current_value < number_input_proto.min)
            or (number_input_proto.has_max and current_value > number_input_proto.max)
        ):
            # Value is outside new bounds - reset to default.
            value_needs_reset = True
            current_value = value

            # Update session_state so subsequent accesses in this run
            # return the corrected value. Use reset_state_value to avoid
            # the "cannot be modified after widget instantiated" error.
            if key is not None:
                get_session_state().reset_state_value(key, current_value)

        if value_needs_reset or widget_state.value_changed:
            if current_value is not None:
                number_input_proto.value = current_value
            number_input_proto.set_value = True

        layout_config = create_layout_config(width=width)

        self.dg._enqueue(
            "number_input",
            number_input_proto,
            layout_config=layout_config,
            has_one_shot_effect=value_needs_reset or widget_state.value_changed,
        )
        return current_value

    @property
    def dg(self) -> DeltaGenerator:
        """The associated DeltaGenerator."""
        return cast("DeltaGenerator", self)
