# 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

from typing import TYPE_CHECKING, Any, Final

from streamlit import config, errors, logger, runtime
from streamlit.elements.lib.form_utils import is_in_form
from streamlit.errors import (
    StreamlitAPIWarning,
    StreamlitInvalidFormCallbackError,
    StreamlitValueAssignmentNotAllowedError,
)
from streamlit.runtime.scriptrunner_utils.script_run_context import (
    in_cached_function,
)
from streamlit.runtime.state import WidgetCallback, get_session_state
from streamlit.runtime.state.common import require_valid_user_key
from streamlit.string_util import to_str

if TYPE_CHECKING:
    from collections.abc import Sequence

    from streamlit.delta_generator import DeltaGenerator


_LOGGER: Final = logger.get_logger(__name__)


def check_callback_rules(dg: DeltaGenerator, on_change: WidgetCallback | None) -> None:
    """Ensures that widgets other than `st.form_submit_button` within a form don't have
    an on_change callback set.

    Raises
    ------
    StreamlitInvalidFormCallbackError:
        Raised when the described rule is violated.
    """

    if runtime.exists() and is_in_form(dg) and on_change is not None:
        raise StreamlitInvalidFormCallbackError()


_shown_default_value_warning: bool = False


def check_session_state_rules(
    default_value: Any, key: str | None, writes_allowed: bool = True
) -> None:
    """Ensures that no values are set for widgets with the given key when writing
    is not allowed.

    Additionally, if `global.disableWidgetStateDuplicationWarning` is False, logs a
    warning when a widget has a default value but its value is also set via session state.

    Raises
    ------
    StreamlitValueAssignmentNotAllowedError:
        Raised when writing is not allowed but session state contains a new value.
    """
    global _shown_default_value_warning  # noqa: PLW0603

    if key is None or not runtime.exists():
        return

    session_state = get_session_state()
    if not session_state.is_new_state_value(key):
        return

    if not writes_allowed:
        raise StreamlitValueAssignmentNotAllowedError(key=key)

    if (
        default_value is not None
        and not _shown_default_value_warning
        and not config.get_option("global.disableWidgetStateDuplicationWarning")
    ):
        _LOGGER.warning(
            'The widget with key "%s" was created with a default value but also had '
            "its value set via the Session State API.",
            key,
            stack_info=True,
        )
        _shown_default_value_warning = True


class CachedWidgetWarning(StreamlitAPIWarning):
    def __init__(self) -> None:
        super().__init__(
            """
Your script uses a widget command in a cached function
(function decorated with `@st.cache_data` or `@st.cache_resource`).
This code will only be called when we detect a cache "miss",
which can lead to unexpected results.

To fix this, move all widget commands outside the cached function.
"""
        )


def check_cache_replay_rules() -> None:
    """Check if a widget is allowed to be used in the current context.
    More specifically, this checks if the current context is inside a
    cached function that disallows widget usage. If so, it raises a warning.

    If there are other similar checks in the future, we could extend this
    function to check for those as well. And rename it to check_widget_usage_rules.
    """
    if in_cached_function.get():
        from streamlit import exception

        # st.exception renders the warning at the widget call site; the log
        # makes it visible to CLI users and agents.
        warning = CachedWidgetWarning()
        _LOGGER.warning("%s", warning, stack_info=True)
        exception(warning)


def check_widget_policies(
    dg: DeltaGenerator,
    key: str | None,
    on_change: WidgetCallback | None = None,
    *,
    default_value: Sequence[Any] | Any | None = None,
    writes_allowed: bool = True,
    enable_check_callback_rules: bool = True,
) -> None:
    """Check all widget policies for the given DeltaGenerator."""
    check_cache_replay_rules()
    if enable_check_callback_rules:
        check_callback_rules(dg, on_change)
    if key is not None:
        require_valid_user_key(key)
    check_session_state_rules(
        default_value=default_value, key=key, writes_allowed=writes_allowed
    )


def validate_label_visibility(label_visibility: str | None) -> None:
    """Raise if ``label_visibility`` is not a supported value."""
    if label_visibility not in {"visible", "hidden", "collapsed"}:
        raise errors.StreamlitValueError(
            "label_visibility", ["'visible'", "'hidden'", "'collapsed'"]
        )


def maybe_raise_label_warnings(
    label: object | None, label_visibility: str | None
) -> str:
    """Coerce ``label`` to ``str``, warn if empty, and validate ``label_visibility``.

    Returns the coerced label so callers can assign it to protobuf string
    fields without raising a protobuf ``TypeError``.
    """
    coerced = "" if label is None else to_str(label)
    if not coerced:
        _LOGGER.warning(
            "`label` got an empty value. This is discouraged for accessibility "
            "reasons and may be disallowed in the future by raising an exception. "
            "Please provide a non-empty label and hide it with label_visibility "
            "if needed.",
            stack_info=True,
        )
    validate_label_visibility(label_visibility)
    return coerced
