from __future__ import annotations

import contextvars
from dataclasses import dataclass
import inspect
import threading
import uuid
from typing import Any, Callable

from flownet.core.request import FlownetPacket
from flownet.core.service_event import ServiceEvent
from flownet.core.types import PayloadData
from flownet.compiler.flowtask import FlowTask
from flownet.runtime.contexts.actor import _system_calls_scope, _user_execution_scope
from flownet.runtime.loops import submit_runtime_coro
from flownet.runtime.runner_lifecycle import start_runner_lifecycle


_service_context: contextvars.ContextVar["ServiceContext" | None] = contextvars.ContextVar(
    "flownet_service_context",
    default=None,
)


class ServiceContext:
    def __init__(
        self,
        name: str,
        registry: "ServiceRegistry",
        call_manager: Any,
        local_address_fn: Callable[[], str],
        stop_event: threading.Event,
    ):
        self._name = name
        self._registry = registry
        self._call_manager = call_manager
        self._local_address_fn = local_address_fn
        self._stop_event = stop_event

    @property
    def name(self) -> str:
        return self._name

    def emit(
        self,
        event_type: str,
        payload: Any = None,
        tags: dict[str, str] | None = None,
    ) -> None:
        with _system_calls_scope(True):
            self._registry.publish(self._name, event_type, payload, tags)

    def local_address(self) -> str:
        return self._local_address_fn()

    def should_stop(self) -> bool:
        return self._stop_event.is_set()

    @property
    def stop_event(self) -> threading.Event:
        return self._stop_event

    def submit(self, coro: Any, *, loop: str = "user"):
        return self._registry.submit(coro, loop=loop)


def service_context() -> ServiceContext:
    context = _service_context.get()
    if context is None:
        raise RuntimeError("Service context not available. Use within a service runner.")
    return context


def emit(
    event_type: str,
    payload: Any = None,
    tags: dict[str, str] | None = None,
) -> None:
    service_context().emit(event_type, payload, tags)


@dataclass
class ServiceHandle:
    name: str
    service: Any
    status: str = "running"
    restart_policy: str = "none"
    last_error: Exception | None = None
    runner: "ServiceRunner" | None = None


@dataclass(frozen=True)
class FlowSubscription:
    subscription_id: str
    service_name: str
    flow_task: FlowTask
    execution_address: str
    tags: dict[str, str] | None = None


class ServiceRunner:
    def __init__(
        self,
        name: str,
        stop_event: threading.Event,
        thread: threading.Thread | None = None,
        future: Any | None = None,
        close_fn: Callable[[], None] | None = None,
    ):
        self._name = name
        self._stop_event = stop_event
        self._thread = thread
        self._future = future
        self._close_fn = close_fn

    def stop(self) -> None:
        self._stop_event.set()
        if self._close_fn is not None:
            try:
                self._close_fn()
            except Exception as exc:  # pragma: no cover - best effort cleanup
                print(f"[ServiceRunner] Failed to close {self._name}: {exc}")
        if self._future is not None:
            try:
                self._future.cancel()
            except Exception as exc:  # pragma: no cover - best effort
                print(f"[ServiceRunner] Failed to cancel {self._name}: {exc}")
        if self._thread is not None and self._thread.is_alive():
            self._thread.join(timeout=1.0)


def _emit_item(context: ServiceContext, item: Any) -> None:
    if item is None:
        return
    if isinstance(item, ServiceEvent):
        context.emit(item.event_type, item.payload, dict(item.tags))
        return
    if isinstance(item, tuple):
        if len(item) == 2:
            event_type, payload = item
            tags = None
        elif len(item) == 3:
            event_type, payload, tags = item
        else:
            raise ValueError(f"Invalid service event tuple: {item}")
        context.emit(event_type, payload, tags)
        return
    context.emit("event", item, None)


def _call_service(
    service_spec: Any,
    *args,
    **kwargs,
) -> tuple[Any, Any]:
    if inspect.isclass(service_spec):
        service_obj = service_spec(*args, **kwargs)
        result = service_obj()
        return service_obj, result
    if inspect.isgenerator(service_spec) or inspect.isasyncgen(service_spec):
        return service_spec, service_spec
    if not callable(service_spec):
        raise TypeError(f"Service spec is not callable: {type(service_spec)}")
    result = service_spec(*args, **kwargs)
    return service_spec, result


class ServiceRegistry:
    def __init__(
        self,
        call_manager: Any,
        local_address_fn: Callable[[], str],
        submit_coro: Callable[[Any], Any] = submit_runtime_coro,
        submit_user_coro: Callable[[Any], Any] | None = None,
        topic_registry: Any | None = None,
    ):
        self._call_manager = call_manager
        self._local_address_fn = local_address_fn
        self._submit_coro = submit_coro
        self._submit_user_coro = submit_user_coro or submit_coro
        self._topic_registry = topic_registry
        self._services: dict[str, ServiceHandle] = {}
        self._flow_subscriptions: dict[str, dict[str, FlowSubscription]] = {}
        self._subscriptions_by_id: dict[str, FlowSubscription] = {}
        self._lock = threading.Lock()

    def start_service(
        self,
        name: str,
        service_spec: Any,
        *args,
        restart_policy: str = "none",
        **kwargs,
    ) -> ServiceHandle:
        with self._lock:
            if name in self._services:
                raise ValueError(f"Service already running: {name}")

        stop_event = threading.Event()
        context = ServiceContext(
            name=name,
            registry=self,
            call_manager=self._call_manager,
            local_address_fn=self._local_address_fn,
            stop_event=stop_event,
        )

        service_obj, result = _call_service(service_spec, *args, **kwargs)

        handle = ServiceHandle(
            name=name,
            service=service_obj,
            restart_policy=restart_policy,
        )
        with self._lock:
            self._services[name] = handle

        try:
            runner = self._start_runner(name, context, stop_event, result, handle)
            handle.runner = runner
        except Exception:
            with self._lock:
                self._services.pop(name, None)
            raise
        return handle

    def _start_runner(
        self,
        name: str,
        context: ServiceContext,
        stop_event: threading.Event,
        result: Any,
        handle: ServiceHandle,
    ) -> ServiceRunner | None:
        def _on_stopped(_runner_kind: str) -> None:
            self._update_status(name, "stopped", None)

        def _on_cancelled() -> None:
            self._update_status(name, "stopped", None)

        def _on_error(exc: Exception) -> None:
            self._update_status(name, "error", exc)
            print(f"[ServiceRegistry] Service {name} failed: {exc}")

        started = start_runner_lifecycle(
            name=name,
            result=result,
            stop_event=stop_event,
            submit_user_coro=self._submit_user_coro,
            set_context=lambda: _service_context.set(context),
            reset_context=_service_context.reset,
            execution_scope_factory=lambda: _user_execution_scope(None),
            emit_item=lambda item: _emit_item(context, item),
            on_stopped=_on_stopped,
            on_error=_on_error,
            on_cancelled=_on_cancelled,
            thread_name=f"svc-{name}",
        )
        if started is None:
            return None
        return ServiceRunner(
            name=name,
            stop_event=stop_event,
            thread=started.thread,
            future=started.future,
            close_fn=started.close_fn,
        )

    def submit(self, coro: Any, *, loop: str = "runtime"):
        if loop == "user" and self._submit_user_coro is not None:
            return self._submit_user_coro(coro)
        return self._submit_coro(coro)

    def _update_status(self, name: str, status: str, error: Exception | None) -> None:
        with self._lock:
            handle = self._services.get(name)
            if handle is None:
                return
            handle.status = status
            handle.last_error = error

    def stop_service(self, name: str) -> bool:
        with self._lock:
            handle = self._services.get(name)
        if handle is None:
            return False
        if handle.runner is not None:
            handle.runner.stop()
        stop_method = getattr(handle.service, "stop", None)
        if callable(stop_method):
            stop_result = stop_method()
            if inspect.isawaitable(stop_result):
                self._submit_user_coro(stop_result)
        with self._lock:
            self._services.pop(name, None)
        return True

    def list_services(self) -> list[dict[str, Any]]:
        with self._lock:
            handles = list(self._services.values())
        return [
            {
                "name": handle.name,
                "status": handle.status,
                "restart_policy": handle.restart_policy,
                "last_error": str(handle.last_error) if handle.last_error else None,
            }
            for handle in handles
        ]

    def get_service(self, name: str) -> ServiceHandle | None:
        with self._lock:
            return self._services.get(name)

    def subscribe_flow(
        self,
        service_name: str,
        flow_task: FlowTask,
        execution_address: str,
        tags: dict[str, str] | None = None,
    ) -> str:
        if self._topic_registry is not None:
            return self._topic_registry.attach_task(
                service_topic_id(service_name),
                flow_task,
                execution_address,
                tags,
            )
        subscription_id = uuid.uuid4().hex
        subscription = FlowSubscription(
            subscription_id=subscription_id,
            service_name=service_name,
            flow_task=flow_task,
            execution_address=execution_address,
            tags=tags,
        )
        with self._lock:
            self._flow_subscriptions.setdefault(service_name, {})[subscription_id] = subscription
            self._subscriptions_by_id[subscription_id] = subscription
        return subscription_id

    def unsubscribe_flow(self, subscription_id: str) -> bool:
        if self._topic_registry is not None:
            return self._topic_registry.detach_task(subscription_id)
        with self._lock:
            subscription = self._subscriptions_by_id.pop(subscription_id, None)
            if subscription is None:
                return False
            subscriptions = self._flow_subscriptions.get(subscription.service_name)
            if subscriptions is not None:
                subscriptions.pop(subscription_id, None)
                if not subscriptions:
                    self._flow_subscriptions.pop(subscription.service_name, None)
        return True

    def publish(
        self,
        service_name: str,
        event_type: str,
        payload: Any = None,
        tags: dict[str, str] | None = None,
    ) -> None:
        if self._topic_registry is not None:
            service_topic = service_topic_id(service_name)
            self._topic_registry.publish(
                service_topic,
                event_type,
                payload,
                tags,
                producer_id=service_topic,
                allow_reserved_namespace=True,
            )
            return
        if tags is None:
            tags = {}
        event = ServiceEvent(
            service=service_name,
            event_type=event_type,
            payload=payload,
            tags=tags,
            source=self._local_address_fn(),
        )
        self._dispatch_flow(event)

    def _dispatch_flow(self, event: ServiceEvent) -> None:
        with self._lock:
            subscriptions = list(self._flow_subscriptions.get(event.service, {}).values())
        if not subscriptions:
            return

        async def _send():
            for subscription in subscriptions:
                payload = PayloadData(args=[event], kwargs={})
                request = subscription.flow_task.make_request(
                    payload,
                    origin_address=self._local_address_fn(),
                )
                if subscription.execution_address == self._local_address_fn():
                    try:
                        handler = getattr(self._call_manager, "runtime_handler", None)
                        if handler is not None:
                            await handler(
                                FlownetPacket(
                                    type="flowrequest",
                                    uuid=uuid.uuid4().hex,
                                    reply=False,
                                    packet_content=request,
                                )
                            )
                            continue
                    except Exception as exc:  # pragma: no cover - best effort
                        print("[ServiceRegistry] Local flow dispatch failed:", exc)
                packet = FlownetPacket(
                    type="flowrequest",
                    uuid=uuid.uuid4().hex,
                    reply=False,
                    packet_content=request,
                )
                try:
                    await self._call_manager.send_packet(subscription.execution_address, packet)
                except Exception as exc:  # pragma: no cover - best effort
                    print(
                        "[ServiceRegistry] Failed to send flowrequest to",
                        subscription.execution_address,
                        exc,
                    )

        self._submit_coro(_send())

    def handle_service_event(self, event: ServiceEvent) -> None:
        if self._topic_registry is not None:
            service_topic = service_topic_id(event.service)
            self._topic_registry.publish(
                service_topic,
                event.event_type,
                event.payload,
                dict(event.tags),
                producer_id=service_topic,
                allow_reserved_namespace=True,
            )
            return
        self._dispatch_flow(event)


def service_topic_id(service_name: str) -> str:
    return f"service:{service_name}"


__all__ = [
    "ServiceContext",
    "ServiceHandle",
    "ServiceRegistry",
    "ServiceRunner",
    "FlowSubscription",
    "service_topic_id",
    "service_context",
    "emit",
]
