from __future__ import annotations

import ast
import inspect
from pathlib import Path
import re
import sys
from typing import (
    Any,
    Callable,
    ClassVar,
    Literal,
    Optional,
    Union,
    cast,
    overload,
    TYPE_CHECKING,
)
from typing_extensions import Self
from collections import defaultdict

from instaui.constants.runtime import RuntimeMode
from instaui.constants.ui import (
    SCOPED_STYLE_GROUP_ID,
    DEFAULT_SLOT_NAME,
    MOUNTED_EVENT_NAME,
)
from instaui.hooks.element_create import trigger_element_create
from instaui.internal.assets import add_component_dependency, ComponentDependencyInfo
from instaui.internal import assets
from instaui.internal.assets.component_dep import ComponentDependencyOverride
from instaui.internal.ui.event import EventMixin
from instaui.internal.ui.event_args import EventArgs
from instaui.internal.ui.event_modifier import TEventModifier, parse_event_components
from instaui.internal.ui.slot import Slot
from instaui.internal.ui.variable import Variable
from instaui.systems.json_system import to_json_str
from instaui.types import TVModelModifier
from .bindable import mark_used, is_bindable
from .app_context import get_app, get_current_container, get_current_scope
from .directive import Directive
from .renderable import Renderable


if TYPE_CHECKING:
    from instaui.internal.ui.vfor_item import VForItem
    from instaui.internal.ui.element_ref import ElementRef


TVarGetterStrategy = Union[Literal["as_needed", "all"], list]

# Refer to the NiceGUI project.
# https://github.com/zauberzeug/nicegui/blob/main/nicegui/element.py
PROPS_PATTERN = re.compile(
    r"""
# Match a key-value pair optionally followed by whitespace or end of string
([:\w\-]+)          # Capture group 1: Key
(?:                 # Optional non-capturing group for value
    =               # Match the equal sign
    (?:             # Non-capturing group for value options
        (           # Capture group 2: Value enclosed in double quotes
            "       # Match  double quote
            [^"\\]* # Match any character except quotes or backslashes zero or more times
            (?:\\.[^"\\]*)*  # Match any escaped character followed by any character except quotes or backslashes zero or more times
            "       # Match the closing quote
        )
        |
        (           # Capture group 3: Value enclosed in single quotes
            '       # Match a single quote
            [^'\\]* # Match any character except quotes or backslashes zero or more times
            (?:\\.[^'\\]*)*  # Match any escaped character followed by any character except quotes or backslashes zero or more times
            '       # Match the closing quote
        )
        |           # Or
        ([\w\-.%:\/]+)  # Capture group 4: Value without quotes
    )
)?                  # End of optional non-capturing group for value
(?:$|\s)            # Match end of string or whitespace
""",
    re.VERBOSE,
)


class PostInitMeta(type):
    def __call__(cls, *args, **kwargs):
        obj = cast(Element, super().__call__(*args, **kwargs))
        obj._define_scope_id = get_current_scope().id
        get_current_container().add_child(obj)
        return obj


class Element(Renderable, metaclass=PostInitMeta):
    dependency: ClassVar[Optional[ComponentDependencyInfo]] = None

    def __init__(self, tag: Optional[Union[str, Variable]] = None):
        if self.dependency:
            tag = self.dependency.tag_name or ""

        super().__init__()

        self._tag = tag
        self._str_classes: list[str] = []
        self._map_classes: dict[str, Union[bool, Any]] = {}
        self._binding_classes: list[Any] = []
        self._style: dict[str, str] = {}
        self._binging_style: dict[str, Any] = {}
        self._proxy_style: list[Any] = []
        self._props: dict[str, Any] = {}
        self._binding_props: dict[str, Any] = {}
        self._proxy_props: list[Any] = []

        self._events: defaultdict[str, list[EventArgs]] = defaultdict(list)
        self._lifecycle_events: defaultdict[str, list[EventArgs]] = defaultdict(list)
        self._directives: dict[Directive, None] = {}
        self._slots: defaultdict[str, Slot] = defaultdict(Slot)
        self._element_ref: Optional[ElementRef] = None
        self._define_scope_id = sys.maxsize

        if self.dependency:
            add_component_dependency(type(self), self.dependency)
        trigger_element_create(self)

    def __init_subclass__(
        cls,
        *,
        esm: Union[str, Path, None] = None,
        externals: Optional[dict[str, Path]] = None,
        css: Union[list[Union[str, Path]], None] = None,
        zero_externals: Optional[dict[str, Path]] = None,
        zero_css: Union[list[Union[str, Path]], None] = None,
    ) -> None:
        super().__init_subclass__()

        if esm:
            esm = _make_dependency_path(esm, cls)

            if externals:
                externals = {
                    key: _make_dependency_path(value, cls)
                    for key, value in externals.items()
                }

            if css:
                css = set(_make_dependency_path(c, cls) for c in css)  # type: ignore

            tag_name = f"instaui-{esm.stem}"

            has_overrides = zero_externals or zero_css

            if not has_overrides:
                overrides = None
            else:
                overrides = {}

                overrides[RuntimeMode.ZERO] = ComponentDependencyOverride(
                    zero_externals=zero_externals, zero_css=zero_css
                )

            cls.dependency = ComponentDependencyInfo(
                tag_name=tag_name,
                esm=esm,
                externals=cast(dict[str, Path], externals or {}),
                css=cast(list[Path], css or list()),  # type: ignore
                overrides=overrides,
            )

    @property
    def slots(self):
        return self._slots

    @property
    def default_slot(self) -> Slot:
        return self._slots[DEFAULT_SLOT_NAME]

    def __enter__(self):
        self.default_slot.__enter__()
        return self

    def __exit__(self, *_):
        self.default_slot.__exit__(*_)

    def add_slot(self, name: str) -> Slot:
        return self._slots[name]

    @property
    def tag(self):
        assert self._tag is not None, "tag is not set"
        return self._tag

    def on_mounted(
        self,
        handler: EventMixin,
        *,
        params: Optional[list] = None,
    ):
        assert isinstance(handler, EventMixin), (
            "handler must be an instance of EventMixin"
        )

        handler._attach_to_element(params=params)
        args = EventArgs(MOUNTED_EVENT_NAME, handler, params=params)
        self._lifecycle_events[MOUNTED_EVENT_NAME].append(args)

        return self

    def scoped_style(
        self,
        style: str,
        *,
        selector: Union[str, Callable[[str], str]] = "*",
        with_self=False,
    ):
        """
        Applies scoped CSS styles to child elements within the component.

        Args:
            style (str): The CSS style rules to be applied within the scope.
            selector (Union[str, Callable[[str], str]], optional): CSS selector or function
                that generates a selector to target specific child elements. Defaults to "*".
            with_self (bool, optional): If True, applies the styles to the component itself
                in addition to its children. Defaults to False.

        Example:
        .. code-block:: python
            # Apply red outline to all direct children
            ui.column().scoped_style("outline: 1px solid red;")

            # Apply styles only to elements with specific class
            ui.box().scoped_style("color: blue;", selector=".target-class")

            # Apply styles to component itself and children
            ui.column().scoped_style("outline: 1px solid red;", with_self=True)

            # Use lambda function for dynamic selector generation
            ui.box().scoped_style(
                "outline: 1px solid red;",
                selector=lambda t: f"{t}:has(.hover) .target"
            )
        """
        app = get_app()
        ssid = app.gen_scoped_style_group_id()

        select_box = f"*[insta-scoped-style={ssid}]"
        real_selector = (
            f"{select_box} {selector}"
            if isinstance(selector, str)
            else selector(select_box)
        )

        if with_self:
            real_selector = f"{select_box},{real_selector}"

        real_selector = f":where({real_selector})"
        style_code = f"{real_selector} {{ {style} }}"

        self.props({"insta-scoped-style": ssid})
        assets.add_style_tag(style_code, group_id=SCOPED_STYLE_GROUP_ID)
        return self

    def slot_props(self, name: str):
        return self._slots[DEFAULT_SLOT_NAME].slot_props(name)

    @staticmethod
    def _update_classes(
        classes: list[str],
        add: str,
    ) -> list[str]:
        return list(dict.fromkeys(classes + add.split()))

    @staticmethod
    def _parse_style(text: Union[str, dict[str, str]]) -> dict[str, str]:
        if isinstance(text, dict):
            return text

        if not text:
            return {}

        result = {}
        for item in text.split(";"):
            item = item.strip()
            if item:
                key, value = item.split(":")
                key = key.strip()
                value = value.strip()
                result[key] = value

        return result

    @staticmethod
    def _parse_props(props: Union[str, dict[str, Any]]) -> dict[str, Any]:
        if isinstance(props, dict):
            return props

        if not props:
            return {}

        dictionary = {}
        for match in PROPS_PATTERN.finditer(props or ""):
            key = match.group(1)
            value = match.group(2) or match.group(3) or match.group(4)
            if value is None:
                dictionary[key] = True
            else:
                if (value.startswith("'") and value.endswith("'")) or (
                    value.startswith('"') and value.endswith('"')
                ):
                    value = ast.literal_eval(value)
                dictionary[key] = value
        return dictionary

    def key(self, key: Any):
        """Set the key prop of the component.

        Args:
            key (str): The key prop value.

        """
        self.props({"key": key})
        return self

    def vmodel(
        self,
        value: Any,
        modifiers: Union[TVModelModifier, list[TVModelModifier], None] = None,
        *,
        prop_name: str = "value",
        is_html_component=False,
    ):
        if prop_name == "value":
            prop_name = "modelValue"

        if isinstance(modifiers, str):
            modifiers = [modifiers]

        self.directive(
            Directive(
                is_sys=is_html_component,
                name="vmodel",
                arg=prop_name,
                modifiers=modifiers,
                value=value,  # type: ignore
            )
        )

        return self

    @overload
    def classes(self, add: str) -> Self: ...
    @overload
    def classes(self, add: dict[str, bool]) -> Self: ...

    @overload
    def classes(self, add: str) -> Self: ...

    def classes(
        self,
        add: Union[
            str,
            dict[str, bool],
            VForItem,
        ],
    ) -> Self:
        """
        Adds one or more CSS classes to the element, supporting static strings,
        reactive string references, or conditional class bindings.

        Args:
            add (str | dict[str, TMaybeRef[bool]] | TMaybeRef[str] | VForItem):
                CSS class configuration to apply. It can be:
                - A static class name string.
                - A reactive reference to a dynamic class name string.
                - A dictionary mapping class names to reactive boolean values,
                which toggle classes on or off.
                - A loop item reference when used inside v-for style bindings.

        Example:
        .. code-block:: python
            from instaui import ui, html

            # Static class
            html.span("target").classes("test")

            # Dynamic class string
            ref = ui.state("c1")
            html.span("target").classes(ref)

            # Conditional class binding
            c1 = ui.state(True)
            c2 = ui.state(False)
            html.span("target").classes({"c1": c1, "c2": c2})
        """

        mark_used(add, host_scope_id=self._define_scope_id)

        if isinstance(add, str):
            self._str_classes = self._update_classes(self._str_classes, add)

        elif isinstance(add, dict):
            self._map_classes.update(add)

        elif is_bindable(add):
            self._binding_classes.append(add)

        return self

    def style(self, add: Union[str, dict[str, Any]]) -> Self:
        """
        Applies inline CSS styles to the element. Supports static strings, dictionaries, or reactive references
        to dynamically update styles.

        Args:
            add (Union[str, dict[str, Any]]): The style(s) to apply. Can be:
                - A CSS string (e.g., "color: red;")
                - A dictionary mapping CSS properties to values (e.g., {"color": "red"})
                - A reactive reference to a string or dictionary for dynamic updates.

        Example:
        .. code-block:: python
            from instaui import ui, html

            # Apply static string style
            html.span("inline style").style("color: red;")

            # Apply dictionary style
            ref = ui.state("red")
            html.span("target").style({"color": ref})

            # Apply reactive string style
            style = ui.state("color: red;")
            html.span("target").style(style)
        """

        mark_used(add, host_scope_id=self._define_scope_id)

        if isinstance(add, str):
            self._style.update(self._parse_style(add))

        elif isinstance(add, dict):
            self._style.update(
                {key: value for key, value in add.items() if not is_bindable(value)}
            )
            self._binging_style.update(
                {key: value for key, value in add.items() if is_bindable(value)}
            )

        elif is_bindable(add):
            self._proxy_style.append(add)
            return self

        return self

    def props(self, add: Union[str, dict[str, Any]]) -> Self:
        """
        Applies one or more HTML properties to the element. Supports constant values,
        string boolean attributes, reactive bindings, and dynamic evaluated props.

        Args:
            add (Union[str, dict[str, Any]]):
                The property source to apply.
                - If a string, the property is treated as a boolean attribute (e.g., "disabled").
                - If a dict, key-value pairs are applied as element properties.
                - If a reactive reference (state), the property updates automatically
                when the referenced value changes.

        Example:
        .. code-block:: python
            from instaui import ui, html

            # Apply constant dictionary
            html.button("Submit").props({"disabled": True})

            # Apply boolean attribute using a string
            html.button("Click").props("disabled")

            # Bind reactive state to a property
            value = ui.state(True)
            html.checkbox(value)
            html.button("Submit").props({"disabled": value})

            # Apply dictionary state with multiple properties
            value = ui.state({"disabled": True})
            html.button("target").props(value)
        """

        mark_used(add, host_scope_id=self._define_scope_id)

        if isinstance(add, str):
            self._props.update(self._parse_props(add))

        elif is_bindable(add):
            self._proxy_props.append(add)
            return self

        elif isinstance(add, dict):
            self._props.update(
                {
                    key: value
                    for key, value in add.items()
                    if (not is_bindable(value)) and value is not None
                }
            )

            self._binding_props.update(
                {key: value for key, value in add.items() if is_bindable(value)}
            )

        return self

    def on(
        self,
        event_name: str,
        handler: EventMixin,
        *,
        params: Optional[list] = None,
        modifier: Optional[list[TEventModifier]] = None,
        with_keys: Optional[list[str]] = None,
    ):
        """
        Attaches an event handler to the element. Supports modifiers for event handling,

        Args:
            event_name (str): The name of the event to listen for.
            handler (EventMixin): The event handler to attach.
            params (Optional[list], optional): A list of values corresponding to handler parameters defined with `ui.event_param()`.
                The order of values in the list will match the order of the placeholders in the
                handler function signature. This allows dynamic context-specific values to be
                injected into the event handler at runtime.
            modifier (Optional[list[TEventModifier]], optional): A list of modifiers to apply. Defaults to None.
            with_keys (Optional[list[str]], optional): A list of key names that trigger the event handler.
                The handler will only execute when one of the specified keys is pressed during the event.
                Common key names include 'enter', 'esc', 'tab', 'space', 'arrowup', 'arrowdown',
                'arrowleft', 'arrowright', 'delete', 'backspace', etc. Multiple keys can be specified
                to allow any of them to trigger the handler. Defaults to None.
        """

        assert isinstance(handler, EventMixin), (
            "handler must be an instance of EventMixin"
        )
        mark_used(handler, host_scope_id=self._define_scope_id)

        handler._attach_to_element(params=params, modifier=modifier)
        event_name, modifier, with_keys = parse_event_components(
            event_name, modifier, with_keys
        )
        args = EventArgs(
            event_name, handler, params=params, modifier=modifier, with_keys=with_keys
        )
        self._events[event_name].append(args)

        return self

    def directive(self, directive: Directive) -> Self:
        self._directives[directive] = None
        return self

    def display(self, value: bool) -> Self:
        return self.directive(Directive(is_sys=False, name="vshow", value=value))

    def event_dataset(self, data: Any, name: str = "event-data") -> Self:
        name = f"data-{name}"
        self.props({name: to_json_str(data)})
        return self

    def element_ref(self, ref: ElementRef):
        """
        Associates an `ElementRef` with the component, allowing interaction with
        the underlying UI element from Python or through event callbacks.

        Args:
            ref (ElementRef): A reference object used to access or manipulate
                the rendered UI element programmatically.

        Example:
        .. code-block:: python
            from instaui import ui, html
            from custom import Counter

            cp = ui.element_ref()

            @ui.event(outputs=[cp])
            def on_click():
                return ui.run_element_method("reset")

            Counter().element_ref(cp)
            html.button("reset").on_click(on_click)
        """

        mark_used(ref, host_scope_id=self._define_scope_id)
        self._element_ref = ref
        return self

    def use(self, *use_fns: Callable[[Self], None]) -> Self:
        """Use functions to the component object.

        Args:
            use_fns (Callable[[Self], None]): The list of use functions.

        Examples:
        .. code-block:: python
            def use_red_color(element: html.paragraph):
                element.style('color: red')

            html.paragraph('Hello').use(use_red_color)
        """

        for fn in use_fns:
            fn(self)
        return self

    @classmethod
    def use_init(cls, init_fn: Callable[[type[Self]], Self]) -> Self:
        """Use this method to initialize the component.

        Args:
            init_fn (Callable[[type[Self]], Self]): The initialization function.

        Examples:
        .. code-block:: python
            def fack_init(cls: type[html.table]) -> html.table:
                return cls(columns=['name', 'age'],rows = [{'name': 'Alice', 'age': 25}, {'name': 'Bob', 'age': 30}])

            ui.table.use_init(fack_init)
        """
        return init_fn(cls)


def _make_dependency_path(path: Union[str, Path], cls: type):
    if isinstance(path, str):
        path = Path(path)

    if not path.is_absolute():
        path = Path(inspect.getfile(cls)).parent / path

    return path
