# 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, Generic, TypeVar

from streamlit import type_util
from streamlit.errors import (
    MarkdownFormattedException,
    StreamlitAPIException,
    StreamlitAPIWarning,
)
from streamlit.runtime.caching.cache_type import CacheType, get_decorator_api_name

if TYPE_CHECKING:
    from collections.abc import Callable

CACHE_DOCS_URL = "https://docs.streamlit.io/develop/concepts/architecture/caching"


def get_cached_func_name_md(func: Any) -> str:
    """Get markdown representation of the function name."""
    if hasattr(func, "__name__"):
        return f"`{func.__name__}()`"
    if hasattr(type(func), "__name__"):
        return f"`{type(func).__name__}`"
    return f"`{type(func)}`"


def get_return_value_type(return_value: Any) -> str:
    if hasattr(return_value, "__module__") and hasattr(type(return_value), "__name__"):
        return f"`{return_value.__module__}.{type(return_value).__name__}`"
    return get_cached_func_name_md(return_value)


class UnhashableTypeError(Exception):
    pass


class UnhashableParamError(StreamlitAPIException):
    def __init__(
        self,
        cache_type: CacheType,
        func: Callable[..., Any],
        arg_name: str | None,
        arg_value: Any,
        orig_exc: BaseException,
    ) -> None:
        msg = self._create_message(cache_type, func, arg_name, arg_value)
        super().__init__(msg)
        self.with_traceback(orig_exc.__traceback__)

    @staticmethod
    def _create_message(
        cache_type: CacheType,
        func: Callable[..., Any],
        arg_name: str | None,
        arg_value: Any,
    ) -> str:
        arg_name_str = arg_name if arg_name is not None else "(unnamed)"
        arg_type = type_util.get_fqn_type(arg_value)
        func_name = func.__name__ if hasattr(func, "__name__") else "unknown"
        arg_replacement_name = f"_{arg_name}" if arg_name is not None else "_arg"

        return (
            f"""
Cannot hash argument '{arg_name_str}' (of type `{arg_type}`) in '{func_name}'.

To address this, you can tell Streamlit not to hash this argument by adding a
leading underscore to the argument's name in the function signature:

```
@st.{get_decorator_api_name(cache_type)}
def {func_name}({arg_replacement_name}, ...):
    ...
```
            """
        ).strip("\n")


class CacheKeyNotFoundError(Exception):
    pass


class CacheError(Exception):
    pass


class CacheReplayClosureError(StreamlitAPIException):
    def __init__(
        self,
        cache_type: CacheType,
        cached_func: Callable[..., Any],
    ) -> None:
        func_name = get_cached_func_name_md(cached_func)
        decorator_name = get_decorator_api_name(cache_type)

        msg = (
            f"""
While running {func_name}, a streamlit element is called on some layout block
created outside the function. This is incompatible with replaying the cached
effect of that element, because the referenced block might not exist when
the replay happens.

How to fix this:
* Move the creation of $THING inside {func_name}.
* Move the call to the streamlit element outside of {func_name}.
* Remove the `@st.{decorator_name}` decorator from {func_name}.
            """
        ).strip("\n")

        super().__init__(msg)


R = TypeVar("R")


class UnserializableReturnValueError(MarkdownFormattedException, Generic[R]):
    def __init__(self, func: Callable[..., R], return_value: R) -> None:
        MarkdownFormattedException.__init__(
            self,
            f"""
            Cannot serialize the return value (of type {get_return_value_type(return_value)})
            in {get_cached_func_name_md(func)}. `st.cache_data` uses
            [pickle](https://docs.python.org/3/library/pickle.html) to serialize the
            function's return value and safely store it in the cache
            without mutating the original object. Please convert the return value to a
            pickle-serializable type. If you want to cache unserializable objects such
            as database connections or Tensorflow sessions, use `st.cache_resource`
            instead (see [our docs]({CACHE_DOCS_URL}) for differences).""",
        )


class UnevaluatedDataFrameError(StreamlitAPIException):
    """Used to display a message about uncollected dataframe being used."""


class CachedStFunctionInBackgroundModeWarning(StreamlitAPIWarning):
    """Warning shown when a background-mode cached function issues display commands.

    With ``refresh_mode="background"``, cached ``st.*`` output is not replayed on
    cache hits, so any display elements only appear during the initial miss (and any
    later foreground recompute) and then disappear on subsequent reruns.
    """

    def __init__(self, cache_type: CacheType, cached_func: Callable[..., Any]) -> None:
        func_name = get_cached_func_name_md(cached_func)
        decorator_name = get_decorator_api_name(cache_type)

        msg = (
            f"""
{func_name} is decorated with `@st.{decorator_name}(refresh_mode="background")`
and uses a Streamlit display command (e.g. `st.write`). In background mode, cached
display output is **not** replayed on cache hits, so these elements only appear when
the function actually runs (the initial cache miss and any later foreground recompute)
and then disappear on subsequent reruns.

To fix this, either move the display commands outside the cached function, or use
`refresh_mode="foreground"`.
"""
        ).strip("\n")

        super().__init__(msg)
