from __future__ import annotations

import asyncio
import inspect
import queue
import time
from typing import Any

from flownet.config import RuntimeConfigInput, coerce_flownet_config


NODE_CONTROL_PACKET_TYPE = "node_control"


class NodeControlCallError(RuntimeError):
    def __init__(
        self,
        *,
        op: str | None,
        code: str,
        message: str,
        address: str | None = None,
        error_type: str | None = None,
        details: dict[str, Any] | None = None,
    ) -> None:
        self.op = op
        self.code = code
        self.address = address
        self.error_type = error_type
        self.details = dict(details or {})
        super().__init__(message)


class NodeControlService:
    _OP_ALIASES = {
        "subscribe_flow": "subscribe_service_flow",
        "unsubscribe_flow": "unsubscribe_service_flow",
    }

    def __init__(self, runtime: Any, service_registry: Any):
        self._runtime = runtime
        self._service_registry = service_registry
        self._container_controller: Any | None = None

    def attach_container_controller(self, controller: Any) -> None:
        self._container_controller = controller

    def invoke(self, op: str, *args, **kwargs) -> Any:
        method_name = self._normalize_op(op)
        method = getattr(self, method_name, None)
        if method is None or method_name.startswith("_"):
            raise ValueError(f"Unsupported node control op: {op}")
        return method(*args, **kwargs)

    @classmethod
    def _normalize_op(cls, op: str) -> str:
        value = str(op or "").strip()
        if not value:
            raise ValueError("Node control op must be a non-empty string.")
        return cls._OP_ALIASES.get(value, value)

    def _require_container_controller(self) -> Any:
        if self._container_controller is None:
            raise RuntimeError("container_control_not_configured")
        return self._container_controller

    def _resolve_flow_run(self, run_id: str) -> Any | None:
        normalized_run_id = str(run_id or "").strip()
        if not normalized_run_id:
            raise ValueError("run_id must be a non-empty string.")

        lookup_fn = getattr(self._runtime, "lookup_flow_run", None)
        flow_run = None
        if callable(lookup_fn):
            flow_run = lookup_fn(normalized_run_id)
        if flow_run is None:
            flow_service = getattr(self._runtime, "flow_run_service", None)
            lookup_run = getattr(flow_service, "lookup_run", None)
            if callable(lookup_run):
                flow_run = lookup_run(normalized_run_id)
        return flow_run

    @staticmethod
    def _normalize_flow_run_read_item(item: Any) -> dict[str, Any] | None:
        if item is None:
            return None
        if isinstance(item, dict) and "kind" in item:
            return dict(item)

        item_type_name = item.__class__.__name__
        request_id = getattr(item, "request_id", None)
        request_ref_id = getattr(item, "request_ref_id", None) or request_id

        if item_type_name == "RunOutputDone":
            result = {
                "kind": "done",
                "request_id": request_id,
                "request_ref_id": request_ref_id,
                "final_seq": getattr(item, "final_seq", None),
                "reason": getattr(item, "reason", None),
            }
            expected_total_events = getattr(item, "expected_total_events", None)
            if expected_total_events is not None:
                result["expected_total_events"] = expected_total_events
            request_done = getattr(item, "request_done", None)
            if request_done is not None:
                result["request_done"] = bool(request_done)
            status = getattr(item, "status", None)
            if isinstance(status, str) and status.strip():
                result["status"] = status.strip()
            subscriber_done = getattr(item, "subscriber_done", None)
            if subscriber_done is not None:
                result["subscriber_done"] = bool(subscriber_done)
            return result
        if item_type_name == "RunOutputError":
            return {
                "kind": "error",
                "request_id": request_id,
                "request_ref_id": request_ref_id,
                "error_type": getattr(item, "error_type", None),
                "message": getattr(item, "message", None),
                "traceback_text": getattr(item, "traceback_text", None),
                "event": getattr(item, "event", None),
            }
        if item_type_name == "RunOutputGap":
            return {
                "kind": "gap",
                "request_id": request_id,
                "request_ref_id": request_ref_id,
                "seq": getattr(item, "seq", None),
                "reason": getattr(item, "reason", None),
            }
        return {"kind": "data", "value": item}

    @staticmethod
    def _derive_request_status_value(
        *,
        status_value: Any,
        request_done: bool,
        producer_done: bool,
        event_chain_pending: int,
        run_status: Any = None,
    ) -> str:
        normalized_status = str(status_value or "").strip().lower()
        if normalized_status:
            return normalized_status
        if request_done:
            normalized_run_status = str(run_status or "").strip().lower()
            if normalized_run_status in {"failed", "cancelled", "timed_out"}:
                return normalized_run_status
            return "completed"
        if producer_done and event_chain_pending > 0:
            return "quiescing"
        return "running"

    @staticmethod
    def _flow_run_descriptor(flow_run: Any | None) -> dict[str, Any] | None:
        if flow_run is None:
            return None

        status_fn = getattr(flow_run, "status", None)
        ingress_state_fn = getattr(flow_run, "ingress_state", None)
        capabilities_fn = getattr(flow_run, "capabilities", None)

        try:
            capabilities = list(capabilities_fn()) if callable(capabilities_fn) else []
        except Exception:
            capabilities = []

        try:
            status = status_fn() if callable(status_fn) else getattr(flow_run, "_status", None)
        except Exception:
            status = None

        try:
            ingress_state = (
                ingress_state_fn()
                if callable(ingress_state_fn)
                else getattr(flow_run, "_ingress_state", None)
            )
        except Exception:
            ingress_state = None

        run_id = str(getattr(flow_run, "run_id", "") or "")
        flow_id = str(getattr(flow_run, "flow_id", "") or "")
        native_flow_id = str(getattr(flow_run, "native_flow_id", "") or flow_id)
        augmented_task_id = getattr(flow_run, "augmented_task_id", None)
        ingress_kind = str(getattr(flow_run, "ingress_kind", "") or "")
        output_mode = str(getattr(flow_run, "output_mode", "") or "")
        request_ref_id = getattr(flow_run, "request_ref_id", None)
        if request_ref_id is not None:
            request_ref_id = str(request_ref_id)

        return {
            "run_id": run_id,
            "flow_id": flow_id,
            "native_flow_id": native_flow_id,
            "augmented_task_id": str(augmented_task_id) if augmented_task_id is not None else None,
            "ingress_kind": ingress_kind,
            "output_mode": output_mode,
            "status": str(status) if status is not None else None,
            "ingress_state": str(ingress_state) if ingress_state is not None else None,
            "request_ref_id": request_ref_id,
            "capabilities": capabilities,
        }

    def list_runtime_topics(self) -> list[dict[str, Any]]:
        return list(getattr(self._runtime, "list_runtime_topics")() or [])

    def list_runtime_actors(self) -> list[dict[str, Any]]:
        return list(getattr(self._runtime, "list_runtime_actors")() or [])

    def list_runtime_nodes(self) -> list[dict[str, Any]]:
        return list(getattr(self._runtime, "list_runtime_nodes")() or [])

    def cluster_view_snapshot(self, schema: str = "v1") -> dict[str, Any]:
        normalized_schema = str(schema or "").strip().lower() or "v1"
        if normalized_schema != "v1":
            raise ValueError(f"Unsupported cluster_view_snapshot schema: {schema!r}")

        cluster_view = getattr(self._runtime, "cluster_view", None)
        if cluster_view is None:
            return {
                "schema_version": "v1",
                "generated_at": time.time(),
                "view_epoch": 0,
                "local_node_id": None,
                "nodes": [],
            }

        local_node_id = None
        local_node_id_fn = getattr(cluster_view, "local_node_id", None)
        if callable(local_node_id_fn):
            try:
                local_node_id = local_node_id_fn()
            except Exception:
                local_node_id = None

        view_epoch = 0
        view_epoch_fn = getattr(cluster_view, "view_epoch", None)
        if callable(view_epoch_fn):
            try:
                view_epoch = int(view_epoch_fn())
            except Exception:
                view_epoch = 0

        nodes_payload: list[dict[str, Any]] = []
        list_nodes_fn = getattr(cluster_view, "list_nodes", None)
        if callable(list_nodes_fn):
            for node in list_nodes_fn() or []:
                if hasattr(node, "as_dict") and callable(node.as_dict):
                    row = dict(node.as_dict())
                elif isinstance(node, dict):
                    row = dict(node)
                else:
                    row = {
                        "node_id": getattr(node, "node_id", None),
                        "address": getattr(node, "address", None),
                        "health": getattr(node, "health", None),
                    }
                nodes_payload.append(row)

        nodes_payload.sort(key=lambda item: str(item.get("node_id") or ""))
        return {
            "schema_version": "v1",
            "generated_at": time.time(),
            "view_epoch": view_epoch,
            "local_node_id": local_node_id,
            "nodes": nodes_payload,
        }

    def cluster_gossip_stats(self) -> dict[str, Any]:
        stats_fn = getattr(self._runtime, "cluster_gossip_stats", None)
        if callable(stats_fn):
            return dict(stats_fn() or {})
        return {}

    async def spawn_container(
        self,
        config: RuntimeConfigInput,
        tenant_id: str | None = None,
        flow_id: str | None = None,
        request_id: str | None = None,
    ) -> str:
        controller = self._require_container_controller()
        resolved_config = coerce_flownet_config(config, section="container")
        return await controller.spawn_container(
            resolved_config,
            tenant_id=tenant_id,
            flow_id=flow_id,
            request_id=request_id,
        )

    def list_containers(self) -> list[dict[str, Any]]:
        controller = self._require_container_controller()
        return list(controller.list_containers() or [])

    def kill_container(self, address: str, timeout: float = 5.0) -> bool:
        controller = self._require_container_controller()
        return bool(controller.kill_container(address, timeout=timeout))

    def list_services(self) -> list[dict[str, Any]]:
        return list(self._service_registry.list_services() or [])

    def submit_flow_program_accept(
        self,
        flow_uri: str,
        version: str | None = None,
        *,
        bindings: dict[str, Any] | None = None,
        ingress: Any | None = None,
        egress: Any | None = None,
        run_config: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        submit_fn = getattr(self._runtime, "submit_flow_program", None)
        if not callable(submit_fn):
            raise RuntimeError("runtime_submit_flow_program_not_supported")
        flow_run = submit_fn(
            flow_uri,
            version=version,
            bindings=bindings,
            ingress=ingress,
            egress=egress,
            run_config=run_config,
        )
        descriptor = self._flow_run_descriptor(flow_run)
        if descriptor is None:
            raise RuntimeError("submit_flow_program_returned_empty_run")
        return descriptor

    def lookup_flow_run_descriptor(self, run_id: str) -> dict[str, Any] | None:
        flow_run = self._resolve_flow_run(run_id)
        return self._flow_run_descriptor(flow_run)

    def cancel_flow_run(self, run_id: str) -> bool:
        flow_run = self._resolve_flow_run(run_id)
        if flow_run is None:
            return False
        cancel_fn = getattr(flow_run, "cancel", None)
        if not callable(cancel_fn):
            raise RuntimeError("flow_run_cancel_not_supported")
        return bool(cancel_fn())

    def flow_run_request_delivery_status(
        self,
        run_id: str,
        request_id: str,
    ) -> dict[str, Any] | None:
        def _coerce_non_negative_int(value: Any) -> int | None:
            try:
                normalized = int(value)
            except (TypeError, ValueError):
                return None
            if normalized < 0:
                return None
            return normalized

        normalized_request_id = str(request_id or "").strip()
        if not normalized_request_id:
            raise ValueError("request_id must be a non-empty string.")
        flow_run = self._resolve_flow_run(run_id)
        if flow_run is None:
            return None
        status_fn = getattr(flow_run, "request_delivery_status", None)
        if not callable(status_fn):
            raise RuntimeError("flow_run_request_delivery_status_not_supported")
        status = status_fn(normalized_request_id)
        if not isinstance(status, dict):
            status = {}
        normalized = dict(status)
        request_ref_id = str(
            normalized.get("request_ref_id")
            or normalized.get("request_id")
            or normalized_request_id
        )
        pending = normalized.get("event_chain_pending")
        if pending is None:
            pending = normalized.get("pending")
        try:
            event_chain_pending = max(0, int(pending))
        except (TypeError, ValueError):
            event_chain_pending = 0
        producer_done = bool(
            normalized.get("producer_done")
            if "producer_done" in normalized
            else normalized.get("done_marked", False)
        )
        request_done = bool(
            normalized.get("request_done")
            if "request_done" in normalized
            else normalized.get("completed", False)
        )
        final_seq = _coerce_non_negative_int(normalized.get("final_seq"))
        if final_seq is None:
            final_seq = _coerce_non_negative_int(normalized.get("source_final_seq"))
        expected_total_events = _coerce_non_negative_int(normalized.get("expected_total_events"))
        if expected_total_events is None:
            expected_total_events = final_seq
        run_status_value = None
        run_status_fn = getattr(flow_run, "status", None)
        if callable(run_status_fn):
            try:
                run_status_value = run_status_fn()
            except Exception:
                run_status_value = None
        status_value = self._derive_request_status_value(
            status_value=normalized.get("status"),
            request_done=request_done,
            producer_done=producer_done,
            event_chain_pending=event_chain_pending,
            run_status=run_status_value,
        )
        normalized.setdefault("request_id", request_ref_id)
        normalized.setdefault("request_ref_id", request_ref_id)
        normalized.setdefault("pending", event_chain_pending)
        normalized.setdefault("event_chain_pending", event_chain_pending)
        normalized.setdefault("event_chain_done", event_chain_pending == 0)
        normalized.setdefault("done_marked", producer_done)
        normalized.setdefault("producer_done", producer_done)
        normalized.setdefault("completed", request_done)
        normalized.setdefault("request_done", request_done)
        normalized.setdefault("status", status_value)
        if final_seq is not None:
            normalized.setdefault("final_seq", final_seq)
        if expected_total_events is not None:
            normalized.setdefault("expected_total_events", expected_total_events)
        return normalized

    def flow_run_read(
        self,
        run_id: str,
        *,
        request_id: str | None = None,
        timeout: float = 5.0,
        from_seq: int | None = None,
    ) -> dict[str, Any] | None:
        normalized_request_id: str | None = None
        if request_id is not None:
            normalized_request_id = str(request_id).strip()
            if not normalized_request_id:
                raise ValueError("request_id must be a non-empty string when provided.")

        flow_run = self._resolve_flow_run(run_id)
        if flow_run is None:
            return None
        read_fn = getattr(flow_run, "read", None)
        if not callable(read_fn):
            raise RuntimeError("flow_run_read_not_supported")
        try:
            if from_seq is None:
                item = read_fn(
                    request_id=normalized_request_id,
                    timeout=float(timeout),
                )
            else:
                item = read_fn(
                    request_id=normalized_request_id,
                    timeout=float(timeout),
                    from_seq=from_seq,
                )
        except TypeError:
            item = read_fn(
                request_id=normalized_request_id,
                timeout=float(timeout),
            )
        except queue.Empty:
            return None
        return self._normalize_flow_run_read_item(item)

    async def subscribe_service_flow(
        self,
        service_name: str,
        flow_task: Any,
        execution_address: str,
        tags: dict[str, str] | None = None,
    ) -> str:
        return await asyncio.to_thread(
            self._service_registry.subscribe_flow,
            service_name,
            flow_task,
            execution_address,
            tags,
        )

    async def unsubscribe_service_flow(self, subscription_id: str) -> bool:
        return bool(
            await asyncio.to_thread(
                self._service_registry.unsubscribe_flow,
                subscription_id,
            )
        )

    def stream_tracker_event_chain_done(self, request_id: str, delta: int) -> int:
        normalized_request_id = str(request_id or "").strip()
        if not normalized_request_id:
            raise ValueError("request_id must be a non-empty string.")
        try:
            normalized_delta = int(delta)
        except (TypeError, ValueError) as exc:
            raise ValueError("delta must be an integer.") from exc

        tracker = getattr(self._runtime, "stream_run_tracker", None)
        if tracker is None:
            raise RuntimeError("stream_run_tracker_not_configured")
        event_chain_done = getattr(tracker, "event_chain_done", None)
        if not callable(event_chain_done):
            raise RuntimeError("stream_run_tracker_event_chain_done_not_supported")
        return int(event_chain_done(normalized_request_id, normalized_delta))

    def stream_tracker_request_producer_done(self, request_id: str) -> bool:
        normalized_request_id = str(request_id or "").strip()
        if not normalized_request_id:
            raise ValueError("request_id must be a non-empty string.")

        tracker = getattr(self._runtime, "stream_run_tracker", None)
        if tracker is None:
            raise RuntimeError("stream_run_tracker_not_configured")
        mark_producer_done = getattr(tracker, "mark_producer_done", None)
        if not callable(mark_producer_done):
            mark_producer_done = getattr(tracker, "mark_done", None)
        if not callable(mark_producer_done):
            raise RuntimeError("stream_run_tracker_request_producer_done_not_supported")
        return bool(mark_producer_done(normalized_request_id))

    def event_chain_request_done(
        self,
        coordinator_target: str,
        request_id: str,
        delta: int,
    ) -> int:
        normalized_request_id = str(request_id or "").strip()
        if not normalized_request_id:
            raise ValueError("request_id must be a non-empty string.")
        try:
            normalized_delta = int(delta)
        except (TypeError, ValueError) as exc:
            raise ValueError("delta must be an integer.") from exc

        normalized_target = str(coordinator_target or "").strip()
        if not normalized_target:
            return 0

        event_chain_done = getattr(self._runtime, "event_chain_request_done", None)
        if not callable(event_chain_done):
            raise RuntimeError("runtime_event_chain_request_done_not_supported")
        result = event_chain_done(
            normalized_target,
            normalized_request_id,
            normalized_delta,
        )
        try:
            return int(result)
        except (TypeError, ValueError):
            return 0

    def stream_tracker_stats(self) -> dict[str, Any]:
        tracker = getattr(self._runtime, "stream_run_tracker", None)
        if tracker is None:
            raise RuntimeError("stream_run_tracker_not_configured")
        stats_fn = getattr(tracker, "stats", None)
        if not callable(stats_fn):
            raise RuntimeError("stream_run_tracker_stats_not_supported")
        stats = stats_fn()
        if isinstance(stats, dict):
            return dict(stats)
        return {"value": stats}

    def stream_tracker_request_status(self, request_id: str) -> dict[str, Any]:
        def _coerce_non_negative_int(value: Any) -> int | None:
            try:
                normalized = int(value)
            except (TypeError, ValueError):
                return None
            if normalized < 0:
                return None
            return normalized

        normalized_request_id = str(request_id or "").strip()
        if not normalized_request_id:
            raise ValueError("request_id must be a non-empty string.")

        tracker = getattr(self._runtime, "stream_run_tracker", None)
        if tracker is None:
            raise RuntimeError("stream_run_tracker_not_configured")
        status_fn = getattr(tracker, "request_status_snapshot", None)
        if not callable(status_fn):
            status_fn = getattr(tracker, "request_status", None)
        if not callable(status_fn):
            raise RuntimeError("stream_run_tracker_request_status_not_supported")

        status = status_fn(normalized_request_id)
        if not isinstance(status, dict):
            status = {}
        normalized = dict(status)
        request_ref_id = str(
            normalized.get("request_ref_id")
            or normalized.get("request_id")
            or normalized_request_id
        )
        pending = normalized.get("event_chain_pending")
        if pending is None:
            pending = normalized.get("pending")
        try:
            event_chain_pending = max(0, int(pending))
        except (TypeError, ValueError):
            event_chain_pending = 0
        producer_done = bool(
            normalized.get("producer_done")
            if "producer_done" in normalized
            else normalized.get("done_marked", False)
        )
        request_done = bool(
            normalized.get("request_done")
            if "request_done" in normalized
            else normalized.get("completed", False)
        )
        final_seq = _coerce_non_negative_int(normalized.get("final_seq"))
        expected_total_events = _coerce_non_negative_int(normalized.get("expected_total_events"))
        if expected_total_events is None:
            expected_total_events = final_seq
        status_value = self._derive_request_status_value(
            status_value=normalized.get("status"),
            request_done=request_done,
            producer_done=producer_done,
            event_chain_pending=event_chain_pending,
        )
        normalized.setdefault("request_id", request_ref_id)
        normalized.setdefault("request_ref_id", request_ref_id)
        normalized.setdefault("pending", event_chain_pending)
        normalized.setdefault("event_chain_pending", event_chain_pending)
        normalized.setdefault("event_chain_done", event_chain_pending == 0)
        normalized.setdefault("done_marked", producer_done)
        normalized.setdefault("producer_done", producer_done)
        normalized.setdefault("completed", request_done)
        normalized.setdefault("request_done", request_done)
        normalized.setdefault("status", status_value)
        if final_seq is not None:
            normalized.setdefault("final_seq", final_seq)
        if expected_total_events is not None:
            normalized.setdefault("expected_total_events", expected_total_events)
        return normalized


def node_control_ok(op: str, result: Any) -> dict[str, Any]:
    return {
        "ok": True,
        "op": op,
        "result": result,
    }


def node_control_error(
    op: str | None,
    *,
    code: str,
    message: str,
    error_type: str,
    details: dict[str, Any] | None = None,
) -> dict[str, Any]:
    return {
        "ok": False,
        "op": op,
        "error": {
            "code": str(code or "internal_error"),
            "message": str(message or "Node control request failed."),
            "type": str(error_type or "RuntimeError"),
            "details": dict(details or {}),
        },
    }


def decode_node_control_response(payload: Any, *, op: str, address: str) -> Any:
    if not isinstance(payload, dict):
        raise NodeControlCallError(
            op=op,
            code="invalid_response",
            message=(
                "Node control response must be a mapping; "
                f"got {type(payload).__name__}."
            ),
            address=address,
            error_type="TypeError",
        )

    if payload.get("ok") is True:
        return payload.get("result")

    error = payload.get("error")
    if not isinstance(error, dict):
        error = {}
    code = str(error.get("code") or "internal_error")
    message = str(error.get("message") or "Node control request failed.")
    error_type = str(error.get("type") or "RuntimeError")
    details = error.get("details")
    if not isinstance(details, dict):
        details = {}

    raise NodeControlCallError(
        op=op,
        code=code,
        message=(
            "Node control call failed "
            f"[address={address}, op={op}, code={code}, type={error_type}]: {message}"
        ),
        address=address,
        error_type=error_type,
        details=details,
    )


async def resolve_node_control_op(service: Any, payload: Any) -> dict[str, Any]:
    if not isinstance(payload, dict):
        return node_control_error(
            None,
            code="invalid_request",
            message="Node control packet_content must be a mapping.",
            error_type="TypeError",
        )

    op = payload.get("op")
    if not isinstance(op, str) or not op.strip():
        return node_control_error(
            None,
            code="invalid_request",
            message="Node control op must be a non-empty string.",
            error_type="ValueError",
        )

    args = payload.get("args")
    if args is None:
        args = []
    if not isinstance(args, (list, tuple)):
        return node_control_error(
            op,
            code="invalid_request",
            message="Node control args must be a list or tuple.",
            error_type="TypeError",
        )

    kwargs = payload.get("kwargs")
    if kwargs is None:
        kwargs = {}
    if not isinstance(kwargs, dict):
        return node_control_error(
            op,
            code="invalid_request",
            message="Node control kwargs must be a mapping.",
            error_type="TypeError",
        )

    try:
        result = service.invoke(op, *tuple(args), **dict(kwargs))
        if inspect.isawaitable(result):
            result = await result
        return node_control_ok(op, result)
    except ValueError as exc:
        return node_control_error(
            op,
            code="unsupported_op",
            message=str(exc),
            error_type=exc.__class__.__name__,
        )
    except Exception as exc:
        return node_control_error(
            op,
            code="internal_error",
            message=str(exc),
            error_type=exc.__class__.__name__,
        )


__all__ = [
    "NODE_CONTROL_PACKET_TYPE",
    "NodeControlCallError",
    "NodeControlService",
    "node_control_ok",
    "node_control_error",
    "decode_node_control_response",
    "resolve_node_control_op",
]
