"""Formatting operations."""

from __future__ import annotations

import inspect
import json
import os
import re
import sys
from typing import TYPE_CHECKING, Any, List, Union

from reflex import constants
from reflex.utils import exceptions, serializers, types
from reflex.utils.serializers import serialize
from reflex.vars import BaseVar, Var

if TYPE_CHECKING:
    from reflex.components.component import ComponentStyle
    from reflex.event import EventChain, EventHandler, EventSpec

WRAP_MAP = {
    "{": "}",
    "(": ")",
    "[": "]",
    "<": ">",
    '"': '"',
    "'": "'",
    "`": "`",
}


def get_close_char(open: str, close: str | None = None) -> str:
    """Check if the given character is a valid brace.

    Args:
        open: The open character.
        close: The close character if provided.

    Returns:
        The close character.

    Raises:
        ValueError: If the open character is not a valid brace.
    """
    if close is not None:
        return close
    if open not in WRAP_MAP:
        raise ValueError(f"Invalid wrap open: {open}, must be one of {WRAP_MAP.keys()}")
    return WRAP_MAP[open]


def is_wrapped(text: str, open: str, close: str | None = None) -> bool:
    """Check if the given text is wrapped in the given open and close characters.

    "(a) + (b)" --> False
    "((abc))"   --> True
    "(abc)"     --> True

    Args:
        text: The text to check.
        open: The open character.
        close: The close character.

    Returns:
        Whether the text is wrapped.
    """
    close = get_close_char(open, close)
    if not (text.startswith(open) and text.endswith(close)):
        return False

    depth = 0
    for ch in text[:-1]:
        if ch == open:
            depth += 1
        if ch == close:
            depth -= 1
        if depth == 0:  # it shouldn't close before the end
            return False
    return True


def wrap(
    text: str,
    open: str,
    close: str | None = None,
    check_first: bool = True,
    num: int = 1,
) -> str:
    """Wrap the given text in the given open and close characters.

    Args:
        text: The text to wrap.
        open: The open character.
        close: The close character.
        check_first: Whether to check if the text is already wrapped.
        num: The number of times to wrap the text.

    Returns:
        The wrapped text.
    """
    close = get_close_char(open, close)

    # If desired, check if the text is already wrapped in braces.
    if check_first and is_wrapped(text=text, open=open, close=close):
        return text

    # Wrap the text in braces.
    return f"{open * num}{text}{close * num}"


def indent(text: str, indent_level: int = 2) -> str:
    """Indent the given text by the given indent level.

    Args:
        text: The text to indent.
        indent_level: The indent level.

    Returns:
        The indented text.
    """
    lines = text.splitlines()
    if len(lines) < 2:
        return text
    return os.linesep.join(f"{' ' * indent_level}{line}" for line in lines) + os.linesep


def to_snake_case(text: str) -> str:
    """Convert a string to snake case.

    The words in the text are converted to lowercase and
    separated by underscores.

    Args:
        text: The string to convert.

    Returns:
        The snake case string.
    """
    s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text)
    return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower().replace("-", "_")


def to_camel_case(text: str, allow_hyphens: bool = False) -> str:
    """Convert a string to camel case.

    The first word in the text is converted to lowercase and
    the rest of the words are converted to title case, removing underscores.

    Args:
        text: The string to convert.
        allow_hyphens: Whether to allow hyphens in the string.

    Returns:
        The camel case string.
    """
    char = "_" if allow_hyphens else "-_"
    words = re.split(f"[{char}]", text.lstrip(char))
    leading_underscores_or_hyphens = "".join(re.findall(rf"^[{char}]+", text))
    # Capitalize the first letter of each word except the first one
    converted_word = words[0] + "".join(x.capitalize() for x in words[1:])
    return leading_underscores_or_hyphens + converted_word


def to_title_case(text: str) -> str:
    """Convert a string from snake case to title case.

    Args:
        text: The string to convert.

    Returns:
        The title case string.
    """
    return "".join(word.capitalize() for word in text.split("_"))


def to_kebab_case(text: str) -> str:
    """Convert a string to kebab case.

    The words in the text are converted to lowercase and
    separated by hyphens.

    Args:
        text: The string to convert.

    Returns:
        The title case string.
    """
    return to_snake_case(text).replace("_", "-")


def format_string(string: str) -> str:
    """Format the given string as a JS string literal..

    Args:
        string: The string to format.

    Returns:
        The formatted string.
    """
    # Escape backticks.
    string = string.replace(r"\`", "`")
    string = string.replace("`", r"\`")

    # Wrap the string so it looks like {`string`}.
    string = wrap(string, "`")
    string = wrap(string, "{")

    return string


def format_var(var: Var) -> str:
    """Format the given Var as a javascript value.

    Args:
        var: The Var to format.

    Returns:
        The formatted Var.
    """
    if not var._var_is_local or var._var_is_string:
        return str(var)
    if types._issubclass(var._var_type, str):
        return format_string(var._var_full_name)
    if is_wrapped(var._var_full_name, "{"):
        return var._var_full_name
    return json_dumps(var._var_full_name)


def format_route(route: str, format_case=True) -> str:
    """Format the given route.

    Args:
        route: The route to format.
        format_case: whether to format case to kebab case.

    Returns:
        The formatted route.
    """
    route = route.strip("/")
    # Strip the route and format casing.
    if format_case:
        route = to_kebab_case(route)

    # If the route is empty, return the index route.
    if route == "":
        return constants.PageNames.INDEX_ROUTE

    return route


def format_cond(
    cond: str | Var,
    true_value: str | Var,
    false_value: str | Var = '""',
    is_prop=False,
) -> str:
    """Format a conditional expression.

    Args:
        cond: The cond.
        true_value: The value to return if the cond is true.
        false_value: The value to return if the cond is false.
        is_prop: Whether the cond is a prop

    Returns:
        The formatted conditional expression.
    """
    # Use Python truthiness.
    cond = f"isTrue({cond})"

    def create_var(cond_part):
        return Var.create_safe(cond_part, _var_is_string=type(cond_part) is str)

    # Format prop conds.
    if is_prop:
        true_value = create_var(true_value)
        prop1 = true_value._replace(
            _var_is_local=True,
        )

        false_value = create_var(false_value)
        prop2 = false_value._replace(_var_is_local=True)
        # unwrap '{}' to avoid f-string semantics for Var
        return f"{cond} ? {prop1._var_name_unwrapped} : {prop2._var_name_unwrapped}"

    # Format component conds.
    return wrap(f"{cond} ? {true_value} : {false_value}", "{")


def format_match(cond: str | Var, match_cases: List[BaseVar], default: Var) -> str:
    """Format a match expression whose return type is a Var.

    Args:
        cond: The condition.
        match_cases: The list of cases to match.
        default: The default case.

    Returns:
        The formatted match expression

    """
    switch_code = f"(() => {{ switch (JSON.stringify({cond})) {{"

    for case in match_cases:
        conditions = case[:-1]
        return_value = case[-1]

        case_conditions = " ".join(
            [
                f"case JSON.stringify({condition._var_name_unwrapped}):"
                for condition in conditions
            ]
        )
        case_code = (
            f"{case_conditions}  return ({return_value._var_name_unwrapped});  break;"
        )
        switch_code += case_code

    switch_code += f"default:  return ({default._var_name_unwrapped});  break;"
    switch_code += "};})()"

    return switch_code


def format_prop(
    prop: Union[Var, EventChain, ComponentStyle, str],
) -> Union[int, float, str]:
    """Format a prop.

    Args:
        prop: The prop to format.

    Returns:
        The formatted prop to display within a tag.

    Raises:
        exceptions.InvalidStylePropError: If the style prop value is not a valid type.
        TypeError: If the prop is not valid.
    """
    # import here to avoid circular import.
    from reflex.event import EventChain

    try:
        # Handle var props.
        if isinstance(prop, Var):
            if not prop._var_is_local or prop._var_is_string:
                return str(prop)
            if types._issubclass(prop._var_type, str):
                return format_string(prop._var_full_name)
            prop = prop._var_full_name

        # Handle event props.
        elif isinstance(prop, EventChain):
            sig = inspect.signature(prop.args_spec)  # type: ignore
            if sig.parameters:
                arg_def = ",".join(f"_{p}" for p in sig.parameters)
                arg_def = f"({arg_def})"
            else:
                # add a default argument for addEvents if none were specified in prop.args_spec
                # used to trigger the preventDefault() on the event.
                arg_def = "(_e)"

            chain = ",".join([format_event(event) for event in prop.events])
            event = f"addEvents([{chain}], {arg_def}, {json_dumps(prop.event_actions)})"
            prop = f"{arg_def} => {event}"

        # Handle other types.
        elif isinstance(prop, str):
            if is_wrapped(prop, "{"):
                return prop
            return json_dumps(prop)

        # For dictionaries, convert any properties to strings.
        elif isinstance(prop, dict):
            prop = serializers.serialize_dict(prop)  # type: ignore

        else:
            # Dump the prop as JSON.
            prop = json_dumps(prop)
    except exceptions.InvalidStylePropError:
        raise
    except TypeError as e:
        raise TypeError(f"Could not format prop: {prop} of type {type(prop)}") from e

    # Wrap the variable in braces.
    assert isinstance(prop, str), "The prop must be a string."
    return wrap(prop, "{", check_first=False)


def format_props(*single_props, **key_value_props) -> list[str]:
    """Format the tag's props.

    Args:
        single_props: Props that are not key-value pairs.
        key_value_props: Props that are key-value pairs.

    Returns:
        The formatted props list.
    """
    # Format all the props.
    return [
        f"{name}={format_prop(prop)}"
        for name, prop in sorted(key_value_props.items())
        if prop is not None
    ] + [str(prop) for prop in single_props]


def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]:
    """Get the state and function name of an event handler.

    Args:
        handler: The event handler to get the parts of.

    Returns:
        The state and function name.
    """
    # Get the class that defines the event handler.
    parts = handler.fn.__qualname__.split(".")

    # If there's no enclosing class, just return the function name.
    if len(parts) == 1:
        return ("", parts[-1])

    # Get the state and the function name.
    state_name, name = parts[-2:]

    # Construct the full event handler name.
    try:
        # Try to get the state from the module.
        state = vars(sys.modules[handler.fn.__module__])[state_name]
    except Exception:
        # If the state isn't in the module, just return the function name.
        return ("", to_snake_case(handler.fn.__qualname__))

    return (state.get_full_name(), name)


def format_event_handler(handler: EventHandler) -> str:
    """Format an event handler.

    Args:
        handler: The event handler to format.

    Returns:
        The formatted function.
    """
    state, name = get_event_handler_parts(handler)
    if state == "":
        return name
    return f"{state}.{name}"


def format_event(event_spec: EventSpec) -> str:
    """Format an event.

    Args:
        event_spec: The event to format.

    Returns:
        The compiled event.
    """
    args = ",".join(
        [
            ":".join(
                (
                    name._var_name,
                    wrap(json.dumps(val._var_name).strip('"').replace("`", "\\`"), "`")
                    if val._var_is_string
                    else val._var_full_name,
                )
            )
            for name, val in event_spec.args
        ]
    )
    event_args = [
        wrap(format_event_handler(event_spec.handler), '"'),
    ]
    event_args.append(wrap(args, "{"))

    if event_spec.client_handler_name:
        event_args.append(wrap(event_spec.client_handler_name, '"'))
    return f"Event({', '.join(event_args)})"


def format_event_chain(
    event_chain: EventChain | Var[EventChain],
    event_arg: Var | None = None,
) -> str:
    """Format an event chain as a javascript invocation.

    Args:
        event_chain: The event chain to queue on the frontend.
        event_arg: The browser-native event (only used to preventDefault).

    Returns:
        Compiled javascript code to queue the given event chain on the frontend.

    Raises:
        ValueError: When the given event chain is not a valid event chain.
    """
    if isinstance(event_chain, Var):
        from reflex.event import EventChain

        if event_chain._var_type is not EventChain:
            raise ValueError(f"Invalid event chain: {event_chain}")
        return "".join(
            [
                "(() => {",
                format_var(event_chain),
                f"; preventDefault({format_var(event_arg)})" if event_arg else "",
                "})()",
            ]
        )

    chain = ",".join([format_event(event) for event in event_chain.events])
    return "".join(
        [
            f"addEvents([{chain}]",
            f", {format_var(event_arg)}" if event_arg else "",
            ")",
        ]
    )


def format_query_params(router_data: dict[str, Any]) -> dict[str, str]:
    """Convert back query params name to python-friendly case.

    Args:
        router_data: the router_data dict containing the query params

    Returns:
        The reformatted query params
    """
    params = router_data[constants.RouteVar.QUERY]
    return {k.replace("-", "_"): v for k, v in params.items()}


def format_state(value: Any) -> Any:
    """Recursively format values in the given state.

    Args:
        value: The state to format.

    Returns:
        The formatted state.

    Raises:
        TypeError: If the given value is not a valid state.
    """
    # Handle dicts.
    if isinstance(value, dict):
        return {k: format_state(v) for k, v in value.items()}

    # Handle lists, sets, typles.
    if isinstance(value, types.StateIterBases):
        return [format_state(v) for v in value]

    # Return state vars as is.
    if isinstance(value, types.StateBases):
        return value

    # Serialize the value.
    serialized = serialize(value)
    if serialized is not None:
        return serialized

    raise TypeError(f"No JSON serializer found for var {value} of type {type(value)}.")


def format_state_name(state_name: str) -> str:
    """Format a state name, replacing dots with double underscore.

    This allows individual substates to be accessed independently as javascript vars
    without using dot notation.

    Args:
        state_name: The state name to format.

    Returns:
        The formatted state name.
    """
    return state_name.replace(".", "__")


def format_ref(ref: str) -> str:
    """Format a ref.

    Args:
        ref: The ref to format.

    Returns:
        The formatted ref.
    """
    # Replace all non-word characters with underscores.
    clean_ref = re.sub(r"[^\w]+", "_", ref)
    return f"ref_{clean_ref}"


def format_array_ref(refs: str, idx: Var | None) -> str:
    """Format a ref accessed by array.

    Args:
        refs : The ref array to access.
        idx : The index of the ref in the array.

    Returns:
        The formatted ref.
    """
    clean_ref = re.sub(r"[^\w]+", "_", refs)
    if idx is not None:
        idx._var_is_local = True
        return f"refs_{clean_ref}[{idx}]"
    return f"refs_{clean_ref}"


def format_breadcrumbs(route: str) -> list[tuple[str, str]]:
    """Take a route and return a list of tuple for use in breadcrumb.

    Args:
        route: The route to transform.

    Returns:
        list[tuple[str, str]]: the list of tuples for the breadcrumb.
    """
    route_parts = route.lstrip("/").split("/")

    # create and return breadcrumbs
    return [
        (part, "/".join(["", *route_parts[: i + 1]]))
        for i, part in enumerate(route_parts)
    ]


def format_library_name(library_fullname: str):
    """Format the name of a library.

    Args:
        library_fullname: The fullname of the library.

    Returns:
        The name without the @version if it was part of the name
    """
    lib, at, version = library_fullname.rpartition("@")
    if not lib:
        lib = at + version

    return lib


def json_dumps(obj: Any) -> str:
    """Takes an object and returns a jsonified string.

    Args:
        obj: The object to be serialized.

    Returns:
        A string
    """
    return json.dumps(obj, ensure_ascii=False, default=serialize)


def unwrap_vars(value: str) -> str:
    """Unwrap var values from a JSON string.

    For example, "{var}" will be unwrapped to "var".

    Args:
        value: The JSON string to unwrap.

    Returns:
        The unwrapped JSON string.
    """

    def unescape_double_quotes_in_var(m: re.Match) -> str:
        prefix = m.group(1) or ""
        # Since the outer quotes are removed, the inner escaped quotes must be unescaped.
        return prefix + re.sub('\\\\"', '"', m.group(2))

    # This substitution is necessary to unwrap var values.
    return (
        re.sub(
            pattern=r"""
            (?<!\\)      # must NOT start with a backslash
            "            # match opening double quote of JSON value
            (<reflex.Var>.*?</reflex.Var>)?  # Optional encoded VarData (non-greedy)
            {(.*?)}      # extract the value between curly braces (non-greedy)
            "            # match must end with an unescaped double quote
        """,
            repl=unescape_double_quotes_in_var,
            string=value,
            flags=re.VERBOSE,
        )
        .replace('"`', "`")
        .replace('`"', "`")
    )


def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]:
    """Collapse keys with consecutive suffixes into a single list value.

    Separators dash and underscore are removed, unless this would overwrite an existing key.

    Args:
        form_dict: The dict to collapse.

    Returns:
        The collapsed dict.
    """
    ending_digit_regex = re.compile(r"^(.*?)[_-]?(\d+)$")
    collapsed = {}
    for k in sorted(form_dict):
        m = ending_digit_regex.match(k)
        if m:
            collapsed.setdefault(m.group(1), []).append(form_dict[k])
    # collapsing never overwrites valid data from the form_dict
    collapsed.update(form_dict)
    return collapsed


def format_data_editor_column(col: str | dict):
    """Format a given column into the proper format.

    Args:
        col: The column.

    Raises:
        ValueError: invalid type provided for column.

    Returns:
        The formatted column.
    """
    if isinstance(col, str):
        return {"title": col, "id": col.lower(), "type": "str"}

    if isinstance(col, (dict,)):
        if "id" not in col:
            col["id"] = col["title"].lower()
        if "type" not in col:
            col["type"] = "str"
        if "overlayIcon" not in col:
            col["overlayIcon"] = None
        return col

    if isinstance(col, BaseVar):
        return col

    raise ValueError(
        f"unexpected type ({(type(col).__name__)}: {col}) for column header in data_editor"
    )


def format_data_editor_cell(cell: Any):
    """Format a given data into a renderable cell for data_editor.

    Args:
        cell: The data to format.

    Returns:
        The formatted cell.
    """
    return {"kind": Var.create(value="GridCellKind.Text"), "data": cell}
