"""
Module with miscellaneous functions. For example myproperty, which is decarator for creating
simplified properties or json_to_py that can convert json string to correct python types or
str_to_infer_type that will convert string to correct type.
"""

from __future__ import annotations
from typing import Callable, Any, Union
import builtins
import time
import sys
from pathlib import Path

from typeguard import check_type
from typing_extensions import Literal, get_origin, get_args
import mylogging


_JUPYTER = 1 if hasattr(builtins, "__IPYTHON__") else 0


class ValidationError(TypeError):
    pass


def validate(value, allowed_type: Any, name: str) -> None:
    """Type validation. It also works for Union and validate Literal values.

    Instead of typeguard validation, it define just subset of types, but is simplier
    and needs no extra import, therefore can be faster.

    Args:
        value (Any): Value that will be validated.
        allowed_type (Any, optional): For example int, str or list. It can be also Union
            or Literal. If Literal, validated value has to be one of Literal values.
            Defaults to None.
        name (str | None, optional): If error raised, name will be printed. Defaults to None.

    Raises:
        ValidationError: Type does not fit.

    # Examples:
    #     >>> from typing_extension import Literal
    #     ...
    #     >>> validate(1, int)
    #     >>> validate(None, Union[list, None])
    #     >>> validate("two", Literal["one", "two"])
    #     >>> validate("three", Literal["one", "two"])
    #     Traceback (most recent call last):
    #     ValidationError: ...
    """
    try:
        check_type(value=value, expected_type=allowed_type, argname=name)
    except TypeError:

        # TODO Wrap error with colors and remove stack only to configuration line...
        # ValidationError(mylogging.return_str("validateio"))

        raise


def small_validate(value, allowed_type: Any = None, name: str | None = None) -> None:
    """Type validation. It also works for Union and validate Literal values.

    Instead of typeguard validation, it define just subset of types, but is simplier
    and needs no extra import, therefore can be faster.

    Args:
        value (Any): Value that will be validated.
        allowed_type (Any, optional): For example int, str or list. It can be also Union
            or Literal. If Literal, validated value has to be one of Literal values.
            Defaults to None.
        name (str | None, optional): If error raised, name will be printed. Defaults to None.

    Raises:
        TypeError: Type does not fit.

    Examples:
        >>> from typing_extensions import Literal
        ...
        >>> small_validate(1, int)
        >>> small_validate(None, Union[list, None])
        >>> small_validate("two", Literal["one", "two"])
        >>> small_validate("three", Literal["one", "two"])  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
        ValidationError: ...
    """
    if type:

        # If Union
        if get_origin(allowed_type) == Union:

            if type(value) in get_args(allowed_type):
                return
            else:
                raise ValidationError(
                    mylogging.return_str(
                        f"Allowed type for variable < {name} > are {allowed_type}, but you try to set an {type(value)}"
                    )
                )

        # If Literal - parse options
        elif get_origin(allowed_type) == Literal:
            options = getattr(allowed_type, "__args__")
            if value in options:
                return
            else:
                raise ValidationError(
                    mylogging.return_str(
                        f"New value < {value} > for variable < {name} > is not in allowed options {options}."
                    )
                )

        else:
            if isinstance(value, allowed_type):
                return
            else:
                raise ValidationError(
                    mylogging.return_str(
                        f"Allowed allowed_type for variable < {name} > is {allowed_type}, but you try to set an {type(value)}"
                    )
                )


def str_to_infer_type(string_var: str) -> Any:
    """Convert string to another type (for example to int, float, list or dict).

    Args:
        string_var (str): String that should be converted.

    Returns:
        Any: New infered type.

    Examples:
        >>> type(str_to_infer_type("1"))
        <class 'int'>
        >>> type(str_to_infer_type("1.2"))
        <class 'float'>
        >>> type(str_to_infer_type("['one']"))
        <class 'list'>
        >>> type(str_to_infer_type("{'one': 1}"))
        <class 'dict'>
    """
    import ast

    evaluated = string_var
    try:
        evaluated = ast.literal_eval(evaluated)
    except Exception:
        pass
    return evaluated


def json_to_py(json: dict, replace_comma_decimal: bool = True, replace_true_false: bool = True) -> Any:
    """Take json and eval it from strings. If string to string, if float to float, if object then to dict.

    When to use? - If sending object as parameter in function.

    Args:
        json (dict): JSON with various formats as string.
        replace_comma_decimal (bool, optional): Some countries use comma as decimal separator (e.g. 12,3).
            If True, comma replaced with dot (Only if there are no brackets (list, dict...)
            and if not converted to number string remain untouched) . For example '2,6' convert to 2.6.
            Defaults to True
        replace_true_false (bool, optional): If string is 'false' or 'true' (for example from javascript),
            it will be capitalized first for correct type conversion. Defaults to True

    Returns:
        dict: Python dictionary with correct types.

    Example:
        >>> json_to_py({'one_two': '1,2'})
        {'one_two': 1.2}

    """

    import ast

    evaluated = json.copy()

    for i, j in json.items():

        replace_condition = isinstance(j, str) and "(" not in j and "[" not in j and "{" not in j

        if replace_comma_decimal and replace_condition:
            j = j.replace(",", ".")

        if replace_true_false and replace_condition:
            if j == "true":
                evaluated[i] = True
            if j == "false":
                evaluated[i] = False
            if j == "true" or j == "false":
                continue

        try:
            evaluated[i] = ast.literal_eval(j)
        except Exception:
            pass

    return evaluated


def watchdog(timeout: int | float, function: Callable, *args, **kwargs) -> Any:
    """Time-limited execution for python function. TimeoutError raised if not finished during defined time.

    Args:
        timeout (int | float): Max time execution in seconds.
        function (Callable): Function that will be evaluated.
        *args: Args for the function.
        *kwargs: Kwargs for the function.

    Raises:
        TimeoutError: If defined time runs out.
        RuntimeError: If function call with defined params fails.

    Returns:
        Any: Depends on used function.

    Examples:
        >>> import time
        >>> def sleep(sec):
        ...     for _ in range(sec):
        ...         time.sleep(1)
        >>> watchdog(1, sleep, 0)
        >>> watchdog(1, sleep, 10)
        Traceback (most recent call last):
        TimeoutError: ...
    """

    old_tracer = sys.gettrace()

    def tracer(frame, event, arg, start=time.time()):
        "Helper."
        now = time.time()
        if now > start + timeout:
            raise TimeoutError("Time exceeded")
        return tracer if event == "call" else None

    try:
        sys.settrace(tracer)
        result = function(*args, **kwargs)

    except TimeoutError:
        sys.settrace(old_tracer)
        raise TimeoutError(
            mylogging.return_str(
                "Timeout defined in watchdog exceeded.",
                caption="TimeoutError",
                level="ERROR",
            )
        )

    except Exception:
        sys.settrace(old_tracer)
        raise RuntimeError(
            mylogging.return_str(
                f"Watchdog with function {function.__name__}, args {args} and kwargs {kwargs} failed."
            )
        )

    finally:
        sys.settrace(old_tracer)

    return result


def get_console_str_with_quotes(string: str | Path):
    """In terminal if value or contain spaces, it's not taken as one param.
    This wraps it with quotes to be able to use paths and values as needed.

    Args:
        string (str, Path): String  to be edited.

    Returns:
        str: Wrapped string that can be used in terminal.
    """
    if isinstance(string, (Path)):
        string = string.as_posix()
    string = string.strip("'")
    string = string.strip('"')
    return f'"{string}"'
