import logging
import os
import sys
import json
import asyncio
import uuid
import weakref
from typing import override

from flut._flut.runtime import flut_unpack

logger = logging.getLogger(__name__)


# The single global engine instance - all other modules only need this
_engine = None


def get_engine():
    """Get the current FlutterEngine instance."""
    return _engine


class FlutterEngine:
    """Flutter Engine built on top of FlutNative."""

    def __init__(self, native, root_widget):
        global _engine

        self._native = native
        self._root_widget = root_widget
        self._dart_call_fn = None
        self._notify_set_state_fn = None
        self._shutting_down = False
        self._action_context = None
        self._event_loop = None  # Stored when running in async mode
        self._dart_enter_isolate = None
        self._dart_exit_isolate = None
        self._dart_current_isolate = None
        self._dart_isolate_handle = None

        # Registries - all owned by the engine
        self.action_registry = {}  # Maps action IDs → Python callbacks
        self.state_registry = {}  # Maps stable IDs → State objects (StatefulWidget)
        self.widget_registry = {}  # Maps widget IDs → Widget (pending first build)

        # Build scope tracking for action cleanup
        self._current_build_scope = (
            None  # State currently building (for action registration)
        )

        # Set global engine reference
        _engine = self

    def call_dart(self, call_type, data):
        """Call Dart synchronously. Returns response dict or None.

        Safe from any context. When called outside Dart's isolate scope
        (e.g. from asyncio tasks), automatically enters/exits the isolate
        around the FFI call.
        """
        if self._dart_call_fn is None:
            raise RuntimeError("Dart call is not registered.")

        entered = False
        if self._dart_current_isolate is not None:
            if not self._dart_current_isolate():
                self._dart_enter_isolate(self._dart_isolate_handle)
                entered = True
        try:
            return self._dart_call_fn(call_type, data)
        finally:
            if entered:
                self._dart_exit_isolate()

    def _setup_dart_call_callback(self, callback_addr):
        """Set up FFI callback for calling Dart via native layer."""
        self._dart_call_fn = self._native.setup_call_dart(callback_addr)

    def _setup_set_state_notification(self, callback_addr):
        """Set up FFI callback for notifying Dart via native layer."""
        self._notify_set_state_fn = self._native.setup_notify_dart(callback_addr)

    def notify_set_state(self, state_id):
        """Send set state notification to Dart."""
        if self._notify_set_state_fn is None:
            if self._shutting_down:
                return
            raise RuntimeError("Set state call is not registered.")
        try:
            ids_json = json.dumps(state_id)
            self._notify_set_state_fn(ids_json.encode("utf-8"))
        except Exception as e:
            logger.error("Error notifying Dart: %s", e)

    def call_dart_async(self, call_type, data):
        """Fire-and-forget call to Dart via async notification channel.

        Uses NativeCallable.listener — safe from any thread/context
        (including asyncio tasks). No return value.
        """
        if self._notify_set_state_fn is None:
            if self._shutting_down:
                return
            raise RuntimeError("Set state call is not registered.")
        try:
            msg = {"_type": call_type}
            msg.update(data)
            msg_json = json.dumps(msg)
            self._notify_set_state_fn(msg_json.encode("utf-8"))
        except Exception as e:
            logger.error("Error notifying Dart: %s", e)

    def register_action(self, callback) -> str:
        """Register an action callback that Dart can invoke.

        Must be called during a State's build() method.
        """
        if self._current_build_scope is None:
            raise RuntimeError("Action registered outside of build scope.")

        action_id = self._current_build_scope._allocate_action_id()
        self.action_registry[action_id] = callback
        return action_id

    def confirm_action(self, action_id: str):
        """Confirm an action was executed, removing it from the registry."""
        self.action_registry.pop(action_id, None)

    def initialize(self):
        logger.info("Flut engine start")
        try:
            self._native.initialize()
        except Exception as e:
            logger.error("Flut engine failed to initialize: %s", e)
            raise
        logger.info("Flut engine ready")

    def run(self, assets_path, width, height, title):
        self._native.run(self._handle_method_call, assets_path, width, height, title)

    async def run_async(self, assets_path, width, height, title, loop=None):
        self._event_loop = loop or asyncio.get_running_loop()
        await self._native.run_async(
            self._handle_method_call,
            assets_path,
            width,
            height,
            title,
            self._event_loop,
        )

    def shutdown(self):
        self._shutting_down = True
        self._notify_set_state_fn = None
        self._dart_call_fn = None
        self._dart_enter_isolate = None
        self._dart_exit_isolate = None
        self._dart_current_isolate = None
        self._dart_isolate_handle = None
        try:
            self._native.shutdown()
        except Exception as e:
            logger.error("Flut engine failed to shutdown: %s", e)
            raise
        logger.info("Flut engine shutdown")

    def _process_single_action(self, action_data):
        """Process a single action event from Dart.

        Returns the callback result if any (used for key events that need sync return).
        """
        action_id = action_data.get("id")
        self._action_context = action_data.get("context") or None
        result = None

        if action_id in self.action_registry:
            cb = self.action_registry[action_id]

            # Prepare arguments
            if action_data.get("args") is not None:
                args = tuple(
                    flut_unpack(a) if isinstance(a, dict) and "_flut_type" in a else a
                    for a in action_data["args"]
                )
            else:
                args = ()

            # Prepare keyword arguments
            kwargs = {}
            if action_data.get("kwargs") is not None:
                for k, v in action_data["kwargs"].items():
                    if isinstance(v, dict) and "_flut_type" in v:
                        kwargs[k] = flut_unpack(v)
                    else:
                        kwargs[k] = v

            # Run callback
            # Note: We don't auto-delete actions because repeatable events like
            # onPanUpdate fire multiple times. Cleanup is handled when widgets
            # rebuild and register new actions (old IDs become orphaned but the
            # memory impact is minimal for typical apps).
            if asyncio.iscoroutinefunction(cb):
                # Async callback - schedule on stored event loop
                if self._event_loop is not None:
                    self._event_loop.create_task(cb(*args, **kwargs))
                else:
                    logger.error(
                        "Async callback for %s ignored (no event loop - use run_app_async)",
                        action_id,
                    )
            else:
                result = cb(*args, **kwargs)
        else:
            logger.error("No callback found for %s", action_id)

        self._action_context = None
        return result

    def _handle_method_call(self, request_json):
        """
        Generic handler for method calls from Dart.
        request_json is a dict like {"type": "...", "data": ...}
        Returns JSON bytes or None.
        """
        try:
            req_type = request_json.get("type")
            data = request_json.get("data") or {}

            if req_type == "widget_build":
                from flut.flutter.widgets.framework import (
                    BuildContext,
                    StatefulWidget,
                    StatelessWidget,
                )

                widget_id = data.get("id")
                context_oid = data.get("contextOid")
                new_widget_id = data.get("newWidgetId")

                context = BuildContext(context_oid) if context_oid else None

                if widget_id is None:
                    if self._root_widget is not None:
                        resp = self._root_widget._flut_pack()
                        release_oids = collect_deleted_objects()
                        if release_oids:
                            resp["_flut_release_oids"] = release_oids
                        return json.dumps(resp).encode("utf-8")
                    return json.dumps(None).encode("utf-8")

                # Check state_registry for existing State (StatefulWidget only)
                state = self.state_registry.get(widget_id)

                if state is None:
                    # First build - check widget_registry
                    widget = self.widget_registry.get(widget_id)
                    if widget is not None:
                        # Don't delete - widget may be needed for didUpdateWidget later
                        if isinstance(widget, StatefulWidget):
                            # StatefulWidget - create State
                            state = widget.createState()
                            state._flut_widget = widget
                            state._flut_id = widget_id
                            state.initState()
                            self.state_registry[widget_id] = state
                        elif isinstance(widget, StatelessWidget):
                            # StatelessWidget - build directly, then cleanup
                            built = widget.build(context)
                            del self.widget_registry[widget_id]
                            subtree_json = built._flut_pack() if built else None
                            release_oids = collect_deleted_objects()
                            if release_oids:
                                if subtree_json is None:
                                    subtree_json = {}
                                subtree_json["_flut_release_oids"] = release_oids
                            return json.dumps(subtree_json).encode("utf-8")
                        else:
                            raise TypeError(
                                f"Unknown widget type: {type(widget).__name__}"
                            )

                if state is not None:
                    # Check if widget was replaced (Dart State persisted)
                    if new_widget_id and new_widget_id != widget_id:
                        new_widget = self.widget_registry.get(new_widget_id)
                        if new_widget is not None:
                            old_widget = state._flut_widget
                            state._flut_widget = new_widget
                            state.didUpdateWidget(old_widget)
                            self.widget_registry.pop(widget_id, None)
                            state._flut_current_widget_id = new_widget_id
                        else:
                            logger.error(
                                "newWidgetId '%s' not in widget_registry", new_widget_id
                            )
                    state._flut_last_build_context = context

                    # Clear previous build's actions before rebuilding
                    for action_id in state._flut_registered_actions:
                        self.action_registry.pop(action_id, None)
                    state._flut_registered_actions = []

                    # Set build scope so child widgets register to this State
                    self._current_build_scope = state
                    state._flut_action_counter = (
                        0  # Reset counter for stable action IDs
                    )
                    try:
                        built = state.build(context)
                        subtree_json = built._flut_pack() if built else None
                    finally:
                        self._current_build_scope = None

                    release_oids = collect_deleted_objects()
                    if release_oids:
                        if subtree_json is None:
                            subtree_json = {}
                        subtree_json["_flut_release_oids"] = release_oids
                    return json.dumps(subtree_json).encode("utf-8")

                logger.error(
                    "Widget '%s' not found in state_registry or widget_registry",
                    widget_id,
                )
                return json.dumps(None).encode("utf-8")

            elif req_type == "register_dart_callback":
                # Dart is registering its callback address for Python to call
                callback_addr = data.get("callback_addr")
                if callback_addr:
                    self._setup_dart_call_callback(callback_addr)
                    return json.dumps({"success": True}).encode("utf-8")
                return json.dumps({"error": "No callback_addr"}).encode("utf-8")

            elif req_type == "register_set_state_callback":
                # Dart is registering its notification callback address
                callback_addr = data.get("callback_addr")
                if callback_addr:
                    self._setup_set_state_notification(callback_addr)
                    return json.dumps({"success": True}).encode("utf-8")
                return json.dumps({"error": "No callback_addr"}).encode("utf-8")

            elif req_type == "register_isolate_functions":
                import ctypes

                enter_addr = data.get("enter")
                exit_addr = data.get("exit")
                current_addr = data.get("current")
                isolate_addr = data.get("isolate")
                ENTER_FN = ctypes.CFUNCTYPE(None, ctypes.c_void_p)
                EXIT_FN = ctypes.CFUNCTYPE(None)
                CURRENT_FN = ctypes.CFUNCTYPE(ctypes.c_void_p)
                self._dart_enter_isolate = ENTER_FN(enter_addr)
                self._dart_exit_isolate = EXIT_FN(exit_addr)
                self._dart_current_isolate = CURRENT_FN(current_addr)
                self._dart_isolate_handle = ctypes.c_void_p(isolate_addr)
                return json.dumps({"success": True}).encode("utf-8")

            elif req_type == "action":
                result = self._process_single_action(data)

                if result is not None:
                    if hasattr(result, "_flut_pack"):
                        result_value = result._flut_pack()
                    else:
                        result_value = result
                    return json.dumps({"result": result_value}).encode("utf-8")

                return json.dumps({"status": "ok"}).encode("utf-8")

            elif req_type == "state_dispose":
                # Dart State disposed - clean up Python state but keep widget
                # Widget must remain for scroll resurrection (child may re-mount)
                widget_id = data.get("id")
                if widget_id:
                    # Clean up state only (not widget)
                    state = self.state_registry.pop(widget_id, None)
                    if state:
                        # Clear actions registered by this state
                        for action_id in state._flut_registered_actions:
                            self.action_registry.pop(action_id, None)
                        # Call State.dispose()
                        state.dispose()
                return json.dumps({"status": "ok"}).encode("utf-8")

        except Exception as e:
            logger.error("Error handling method call: %s", e, exc_info=True)
            return json.dumps({"_flut_error": str(e)}).encode("utf-8")


def _get_native_and_assets(assets_path):
    """Get native implementation and resolve assets path for current platform."""
    from .native.native import FlutNative

    native = FlutNative._create()

    if assets_path is None:
        assets_path = native.get_default_assets_path()
        if len(sys.argv) > 1:
            assets_path = sys.argv[1]

    if not os.path.exists(assets_path):
        logger.error(
            "Assets not found at: %s. Build the Flutter app first.", assets_path
        )
        return None, None

    return native, assets_path


def run_app(widget, assets_path=None, width=800, height=600, title="Flut"):
    """Run the app (sync mode)."""
    native, assets_path = _get_native_and_assets(assets_path)
    if native is None:
        return None

    engine = FlutterEngine(native, widget)
    engine.initialize()

    try:
        engine.run(assets_path, width, height, title)
    finally:
        engine.shutdown()

    return engine


async def run_app_async(widget, assets_path=None, width=800, height=600, title="Flut"):
    """
    Run the app with asyncio integration (Windows, Linux, and macOS).

    Integrates platform's UI loop with asyncio:
    - Windows: Win32 messages + IOCP via MsgWaitForMultipleObjectsEx
    - Linux: GTK events + epoll via eventfd + g_unix_fd_add
    - macOS: CFRunLoop + kqueue via pipe + CFFileDescriptor

    Single-threaded, event-driven.

    Usage:
        async def main():
            # Start background async tasks
            asyncio.create_task(my_background_task())

            # Run Flutter app (this drives the event loop)
            await run_app_async(MyWidget())

        asyncio.run(main())

    Args:
        widget: The root widget for the app.
        assets_path: Path to flutter_assets (auto-detected if None).
        width: Initial window width.
        height: Initial window height.
        title: Window title.

    Returns:
        The FlutterEngine instance after the app closes.
    """
    native, assets_path = _get_native_and_assets(assets_path)
    if native is None:
        return None

    engine = FlutterEngine(native, widget)
    engine.initialize()

    try:
        await engine.run_async(assets_path, width, height, title)
    finally:
        engine.shutdown()

    return engine


def generate_flut_id() -> str:
    """Generate a unique ID for Flut objects."""
    return str(uuid.uuid4())


class FlutObject:
    def __init__(self, flut_type: str):
        if not flut_type:
            raise ValueError("flut_type must not be null or empty")
        self._flut_type = flut_type

    def _flut_base_props(self) -> dict:
        return {"_flut_type": self._flut_type}

    def _flut_pack(self) -> dict:
        raise NotImplementedError(f"{type(self).__name__} must implement _flut_pack()")

    def _register_action(
        self,
        callback,
        base_class: type | None = None,
        method_name: str | None = None,
        args: tuple = (),
    ) -> str | None:
        if method_name is not None and base_class is not None:
            if getattr(type(self), method_name) is getattr(base_class, method_name):
                return None
        engine = get_engine()
        if engine is None:
            raise RuntimeError("Engine is not initialized.")
        if args:

            def wrapper():
                result = callback(*args)
                if result is not None:
                    return result._flut_pack()
                return None

            return engine.register_action(wrapper)
        return engine.register_action(callback)


class FlutImmutableObject(FlutObject):
    pass


class FlutAbstractObject(FlutObject):
    pass


class FlutStaticObject:

    @staticmethod
    def _static_call(class_name: str, method_name: str, *args):
        engine = get_engine()
        if engine is None:
            raise RuntimeError("Engine is not initialized.")
        packed_args = []
        for arg in args:
            if hasattr(arg, "_flut_oid"):
                packed_args.append({"oid": arg._flut_oid})
            elif hasattr(arg, "_flut_pack"):
                packed_args.append(arg._flut_pack())
            else:
                packed_args.append(arg)
        result = engine.call_dart(
            "static",
            {
                "name": f"{class_name}.{method_name}",
                "args": packed_args,
            },
        )
        if result and "value" in result:
            value = result["value"]
            if isinstance(value, dict):
                decoded = flut_unpack(value)
                return decoded if decoded is not None else value
            return value
        return None


class FlutEnumObject(FlutObject):
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        for name in cls.__annotations__:
            setattr(cls, name, cls(name))

    def __init__(self, value: str):
        super().__init__(type(self).__name__)
        self._flut_value = value

    @override
    def _flut_pack(self) -> dict:
        result = self._flut_base_props()
        result["_flut_value"] = self._flut_value
        return result


# Realtime object system - for objects that sync properties with Dart
_deleted_object_ids = []  # list of oids pending release to Dart


def collect_deleted_objects() -> list:
    """Collect deleted object IDs for piggybacking in response.

    Returns oids to include in the response JSON. Dart processes these
    after handling the widget tree, avoiding sync calls during builds.
    """
    global _deleted_object_ids
    if not _deleted_object_ids:
        return []
    oids = _deleted_object_ids
    _deleted_object_ids = []
    return oids


class FlutRealtimeObject(FlutObject):
    """Base class for objects that have realtime property sync with Dart.

    Uses integer _flut_oid for Dart-side lookup. When garbage collected,
    the oid is added to _deleted_object_ids for batch release to Dart.

    Python-created objects pass props and bindings to __init__.
    Dart-created objects pass oid directly to __init__.
    """

    _init_action_counter = 0
    _oid_registry: dict[int, weakref.ref] = {}

    def __init__(
        self,
        flut_type: str,
        oid: int | None = None,
        props: dict | None = None,
        bindings: list | None = None,
    ):
        super().__init__(flut_type)
        self._flut_oid = oid
        self._flut_init_actions = []
        if oid is not None:
            FlutRealtimeObject._oid_registry[oid] = weakref.ref(self)
        if oid is None:
            init_data = dict(props) if props else {}
            if bindings:
                for binding in bindings:
                    key = binding[0]
                    callback = binding[1]
                    base_class = binding[2] if len(binding) > 2 else None
                    method_name = binding[3] if len(binding) > 3 else None
                    action_id = self._register_init_action(
                        callback, base_class, method_name
                    )
                    if action_id is not None:
                        init_data[key] = action_id
            self._flut_create(**init_data)

    def _flut_create(self, **props):
        """Create the Dart-side object with constructor params. Sets _flut_oid."""
        engine = get_engine()
        if engine is None:
            raise RuntimeError("Engine is not initialized.")
        data = {"_flut_type": self._flut_type}
        data.update(props)
        result = engine.call_dart("createObject", data)
        if result and "oid" in result:
            self._flut_oid = result["oid"]
            FlutRealtimeObject._oid_registry[self._flut_oid] = weakref.ref(self)
        else:
            raise RuntimeError("Failed to create object.")

    def _register_init_action(self, callback, base_class=None, method_name=None):
        """Register a permanent action for this RT object (not tied to build scope)."""
        if callback is None:
            return None
        if method_name is not None and base_class is not None:
            if getattr(type(self), method_name) is getattr(base_class, method_name):
                return None
        engine = get_engine()
        if engine is None:
            raise RuntimeError("Engine is not initialized.")
        FlutRealtimeObject._init_action_counter += 1
        action_id = f"init:{FlutRealtimeObject._init_action_counter}"
        engine.action_registry[action_id] = callback
        self._flut_init_actions.append(action_id)
        return action_id

    def _unregister_init_action(self, action_id):
        if action_id in self._flut_init_actions:
            self._flut_init_actions.remove(action_id)
        engine = get_engine()
        if engine is not None:
            engine.action_registry.pop(action_id, None)

    def _register_oneshot_action(self, callback):
        action_id = [None]

        def wrapper(*args, **kwargs):
            self._unregister_init_action(action_id[0])
            callback(*args, **kwargs)

        action_id[0] = self._register_init_action(wrapper)
        return action_id[0]

    @override
    def _flut_base_props(self) -> dict:
        """Return base realtime object properties."""
        result = super()._flut_base_props()
        result["_flut_oid"] = self._flut_oid
        return result

    @override
    def _flut_pack(self) -> dict:
        return self._flut_base_props()

    def _get(self, property: str):
        engine = get_engine()
        if engine is None:
            raise RuntimeError("Engine is not initialized.")
        result = engine.call_dart("get", {"oid": self._flut_oid, "property": property})
        if result and "value" in result:
            return result["value"]
        return None

    def _set(self, property: str, value):
        engine = get_engine()
        if engine is None:
            raise RuntimeError("Engine is not initialized.")
        engine.call_dart_async(
            "set", {"oid": self._flut_oid, "property": property, "value": value}
        )

    def _call(self, method: str, **args):
        engine = get_engine()
        if engine is None:
            raise RuntimeError("Engine is not initialized.")
        result = engine.call_dart(
            "call", {"oid": self._flut_oid, "method": method, "args": args}
        )
        if result and "value" in result:
            return result["value"]
        return None

    def _call_void(self, method: str, **args):
        """Fire-and-forget method call. Safe from any context (including async)."""
        engine = get_engine()
        if engine is None:
            raise RuntimeError("Engine is not initialized.")
        engine.call_dart_async(
            "call", {"oid": self._flut_oid, "method": method, "args": args}
        )

    @staticmethod
    def _lookup_oid(oid: int):
        ref = FlutRealtimeObject._oid_registry.get(oid)
        if ref is not None:
            obj = ref()
            if obj is not None:
                return obj
        return None

    def __del__(self):
        # Guard against interpreter shutdown (globals may be None)
        try:
            if self._flut_oid is not None:
                if FlutRealtimeObject._oid_registry.get(self._flut_oid) is not None:
                    ref = FlutRealtimeObject._oid_registry[self._flut_oid]
                    if ref() is None or ref() is self:
                        FlutRealtimeObject._oid_registry.pop(self._flut_oid, None)
            if _deleted_object_ids is not None and self._flut_oid is not None:
                _deleted_object_ids.append(self._flut_oid)
            if _engine is not None:
                for action_id in getattr(self, "_flut_init_actions", []):
                    _engine.action_registry.pop(action_id, None)
        except (TypeError, AttributeError):
            pass  # Interpreter shutting down
