"""
Utility functions.
"""
import inspect
import pathlib
import random
import string
import sys
import warnings
from functools import lru_cache
from typing import Optional
from typing import Type

import packaging.version

import saltfactories


def random_string(prefix, size=6, uppercase=True, lowercase=True, digits=True):
    """
    Generates a random string.

    :keyword str prefix: The prefix for the random string
    :keyword int size: The size of the random string
    :keyword bool uppercase: If true, include upper-cased ascii chars in choice sample
    :keyword bool lowercase: If true, include lower-cased ascii chars in choice sample
    :keyword bool digits: If true, include digits in choice sample
    :return str: The random string
    """
    if not any([uppercase, lowercase, digits]):
        msg = "At least one of 'uppercase', 'lowercase' or 'digits' needs to be true"
        raise RuntimeError(msg)
    choices = []
    if uppercase:
        choices.extend(string.ascii_uppercase)
    if lowercase:
        choices.extend(string.ascii_lowercase)
    if digits:
        choices.extend(string.digits)

    return prefix + "".join(random.choice(choices) for _ in range(size))  # noqa: S311


@lru_cache(maxsize=1)
def running_username():
    """
    Return the username that is running the code.
    """
    # Do not move these deferred imports. It allows running against a Salt
    # onedir build in salt's repo checkout.
    import salt.utils.user  # pylint: disable=import-outside-toplevel

    return salt.utils.user.get_user()


def cast_to_pathlib_path(value):
    """
    Cast the passed value to an instance of ``pathlib.Path``.
    """
    if value is None:
        return value
    if isinstance(value, pathlib.Path):
        return value
    try:
        return pathlib.Path(value.strpath)
    except AttributeError:
        return pathlib.Path(str(value))


def warn_until(
    version: str,
    message: str,
    category: Type[Warning] = DeprecationWarning,  # noqa: FA100
    stacklevel: Optional[int] = None,  # noqa: FA100
    _dont_call_warnings: bool = False,
    _pkg_version_: Optional[str] = None,  # noqa: FA100
) -> None:
    """
    Show a deprecation warning.

    Helper function to raise a warning, by default, a ``DeprecationWarning``,
    until the provided ``version``, after which, a ``RuntimeError`` will
    be raised to remind the developers to remove the warning because the
    target version has been reached.

    :param version:
        The version string after which the warning becomes a ``RuntimeError``.
        For example ``2.1``.
    :param message:
        The warning message to be displayed.
    :param category:
        The warning class to be thrown, by default ``DeprecationWarning``
    :param stacklevel:
        There should be no need to set the value of ``stacklevel``.
    :param _dont_call_warnings:
        This parameter is used just to get the functionality until the actual
        error is to be issued. When we're only after the version checks to
        raise a ``RuntimeError``.
    """
    _version = packaging.version.parse(version)
    if _pkg_version_ is None:
        _pkg_version_ = saltfactories.__version__  # type: ignore[attr-defined]
    _pkg_version = packaging.version.parse(_pkg_version_)

    if stacklevel is None:
        # Attribute the warning to the calling function, not to warn_until()
        stacklevel = 3

    _pytest_is_rewriting = False
    caller = inspect.getframeinfo(sys._getframe(stacklevel - 1))  # noqa: SLF001
    caller_filename = pathlib.Path(caller.filename)

    if str(caller_filename.as_posix()).endswith("/_pytest/assertion/rewrite.py"):
        # Pytest is rewriting code, increase stack level so that the right
        # module triggering the warning is shown.
        stacklevel += 1
        _pytest_is_rewriting = True

    if _pkg_version >= _version:
        if _pytest_is_rewriting:
            # We need to grab the caller with the new stack level
            caller = inspect.getframeinfo(sys._getframe(stacklevel - 1))  # noqa: SLF001
        msg = (
            f"The warning triggered on filename '{caller.filename}', line number {caller.lineno}, "
            f"is supposed to be shown until version {_pkg_version_} is released. Current version "
            f"is now {version}. Please remove the warning."
        )
        raise RuntimeError(msg)

    if _dont_call_warnings is False:
        warnings.warn(
            message.format(version=version),
            category,
            stacklevel=stacklevel,
        )
