"""
Apps Visualizer: graph elements model
"""

from typing import List, Dict
from enum import Enum

from hopeit.dataobjects import dataclass, dataobject, field
from hopeit.app.config import (
    AppConnection,
    AppDescriptor,
    EventDescriptor,
    EventType,
    StreamQueueStrategy,
)
from hopeit.server.names import auto_path


class NodeType(str, Enum):
    REQUEST = "REQUEST"
    EVENT = "EVENT"
    STREAM = "STREAM"
    MULTIPART = "MULTIPART"


@dataobject
@dataclass
class Node:
    id: str
    label: str
    type: NodeType
    inputs: List[str] = field(default_factory=list)
    outputs: List[str] = field(default_factory=list)
    slots: List[str] = field(default_factory=list)


@dataobject
@dataclass
class Edge:
    id: str
    label: str
    source: str
    target: str


@dataobject
@dataclass
class Graph:
    nodes: List[Node]
    edges: List[Edge]


def get_nodes(
    events: Dict[str, EventDescriptor], *, expanded_view: bool = False
) -> Dict[str, Node]:
    """
    Create Node metadata from EventDescriptors from app_config,
    expanding effective events using engine functionallity.

    :param expanded_view: bool, if True, stream queues are shown as separate streams,
        otherwise they are shown in a single node with multiple input/outputs.
    """
    nodes = {}
    for event_name, event_info in events.items():
        inputs, outputs = [], []
        if event_info.type in (EventType.GET, EventType.POST, EventType.MULTIPART):
            port_name = f"{event_name}.{event_info.type.value}"
            inputs.append(port_name)
            request_node = Node(
                id=port_name,
                label=event_info.type.value,
                type=NodeType.REQUEST,
                outputs=[port_name],
            )
            nodes[port_name] = request_node

        if event_info.read_stream:
            queues = event_info.read_stream.queues
            for qid, queue in (
                zip(queues, queues) if expanded_view else iter([("", "|".join(queues))])
            ):
                stream_id = f">{event_info.read_stream.name}.{qid}".strip(".")
                stream_name = f"{event_info.read_stream.name}"
                if qid not in ("", "AUTO"):
                    stream_name += f".{qid}"
                stream_node = nodes.get(
                    stream_id, Node(id=stream_id, label=stream_name, type=NodeType.STREAM)
                )
                stream_node.slots = sorted(set([*stream_node.slots, *queue.split("|")]))
                nodes[stream_id] = stream_node

                for q in [queue] if expanded_view else queues:
                    port_name = f"{event_name}.{stream_name}.{q}"
                    inputs.append(port_name)
                    stream_node.outputs.append(port_name)

        if event_info.write_stream:
            queues = event_info.write_stream.queues
            if (
                event_info.read_stream
                and event_info.write_stream.queue_strategy == StreamQueueStrategy.PROPAGATE
            ):
                queues = [
                    qx if qy == "AUTO" else qy
                    for qx in event_info.read_stream.queues
                    for qy in queues
                ]
            for qid, queue in (
                zip(queues, queues) if expanded_view else iter([("", "|".join(queues))])
            ):
                stream_id = f">{event_info.write_stream.name}.{qid}".strip(".")
                stream_name = f"{event_info.write_stream.name}"
                if qid not in ("", "AUTO"):
                    stream_name += f".{qid}"
                stream_node = nodes.get(
                    stream_id, Node(id=stream_id, label=stream_name, type=NodeType.STREAM)
                )
                stream_node.slots = sorted(set([*stream_node.slots, *queue.split("|")]))
                nodes[stream_id] = stream_node

                for q in [queue] if expanded_view else queues:
                    port_name = f"{event_name}.{stream_name}.{q}"
                    stream_node.inputs.append(port_name)
                    outputs.append(port_name)

        nodes[event_name] = Node(
            id=event_name, label=event_name, type=NodeType.EVENT, inputs=inputs, outputs=outputs
        )

    return nodes


def add_app_connections(
    nodes: Dict[str, Node],
    *,
    app_connections: Dict[str, AppConnection],
    events: Dict[str, EventDescriptor],
):
    """
    Add to nodes map, input/output ports for app_connections in order to show call
    dependencies between events.
    """
    for event_name, event_info in events.items():
        app_key = ".".join(event_name.split(".", maxsplit=2)[0:2])
        source_node = nodes[event_name]
        if source_node:
            for conn in event_info.connections:
                app_connection = app_connections[f"{app_key}.{conn.app_connection}"]
                target_app_key = AppDescriptor(
                    name=app_connection.name, version=app_connection.version
                ).app_key()
                dest_node_name = f"{target_app_key}."
                if app_connection.plugin_name:
                    dest_node_name += (
                        auto_path(
                            app_connection.plugin_name,
                            app_connection.plugin_version or app_connection.version,
                        )
                        + "."
                    )
                dest_node_name += f"{conn.event}.{conn.type.value}"
                dest_node = nodes.get(dest_node_name)
                if dest_node:
                    port = f"{event_name}.{app_connection.name}.{conn.event}.{conn.type.value}"
                    source_node.outputs.append(port)
                    dest_node.inputs.append(port)


def get_edges(nodes: Dict[str, Node]):
    """
    Builds Edge list from list of Nodes
    """
    inputs = {port: node for node in nodes.values() for port in node.inputs}
    outputs = {port: node for node in nodes.values() for port in node.outputs}

    edges = []
    for k, source in outputs.items():
        target = inputs.get(k)
        if target:
            edges.append(Edge(id=k, label=k.split(".")[-1], source=source.id, target=target.id))

    return edges
