import time
from collections.abc import Iterable
from pathlib import Path
from typing import Literal, cast

import py4cytoscape as p4c
from networkx import DiGraph, Graph
from py4cytoscape import network_selection as p4c_network_selection
from py4cytoscape.exceptions import CyError
from requests.exceptions import RequestException

from lang_main.constants import (
    CYTO_BASE_NETWORK_NAME,
    CYTO_COLLECTION_NAME,
    CYTO_ITER_NEIGHBOUR_DEPTH,
    CYTO_LAYOUT_NAME,
    CYTO_LAYOUT_PROPERTIES,
    CYTO_MAX_EDGE_COUNT,
    CYTO_MAX_NODE_COUNT,
    CYTO_NETWORK_ZOOM_FACTOR,
    CYTO_NUMBER_SUBGRAPHS,
    CYTO_PATH_STYLESHEET,
    CYTO_SANDBOX_NAME,
    CYTO_SELECTION_PROPERTY,
    CYTO_STYLESHEET_NAME,
    PROPERTY_NAME_DEGREE_WEIGHTED,
    SAVE_PATH_FOLDER,
)
from lang_main.errors import GraphRenderError
from lang_main.loggers import logger_rendering as logger
from lang_main.render import cytoscape_monkeypatch as cs_monkeypatch
from lang_main.types import (
    CytoExportFileTypes,
    CytoExportPageSizes,
    CytoLayoutProperties,
    CytoLayouts,
    CytoNodeID,
)

# monkeypatch non-stable py4cytoscape function
p4c_network_selection.select_edges_connecting_selected_nodes = (
    cs_monkeypatch.select_edges_connecting_selected_nodes
)
p4c.select_edges_connecting_selected_nodes = (
    cs_monkeypatch.select_edges_connecting_selected_nodes
)


# ** Cytoscape API related, using py4cytoscape
def verify_connection() -> None:
    """Cytoscape: checks if CyREST and Cytoscape versions are compatible nad
    if Cytoscape API endpoint is reachable

    Raises
    ------
    CyError
        incompatible CyREST or Cytoscape versions
    RequestException
        API endpoint not reachable
    """
    try:
        p4c.cytoscape_ping()
    except CyError as error:
        logger.error('[CyError] CyREST or Cytoscape version not supported.')
        raise error
    except RequestException as error:
        logger.error('[CytoAPIConnection] Connection to CyREST API failed.')
        raise error


def verify_graph_render_size(
    graph: Graph | DiGraph,
    max_node_count: int | None = CYTO_MAX_NODE_COUNT,
    max_edge_count: int | None = CYTO_MAX_EDGE_COUNT,
) -> None:
    """verify that the graph size can still be handled within an acceptable time
    frame for rendering in Cytoscape

    Parameters
    ----------
    graph : Graph | DiGraph
        graph to verify
    max_node_count : int | None, optional
        maximum allowed number of nodes, by default CYTO_MAX_NODE_COUNT
    max_edge_count : int | None, optional
        maximum allowed number of edges, by default CYTO_MAX_EDGE_COUNT

    Raises
    ------
    GraphRenderError
        if any of the provided limits is exceeded
    """
    num_nodes = len(graph.nodes)
    num_edges = len(graph.edges)
    if max_node_count is not None and num_nodes > max_node_count:
        raise GraphRenderError(
            f'Maximum number of nodes for rendering exceeded. '
            f'Limit {max_node_count}, Counted: {num_nodes}'
        )

    if max_edge_count is not None and num_edges > max_edge_count:
        raise GraphRenderError(
            f'Maximum number of edges for rendering exceeded. '
            f'Limit {max_edge_count}, Counted: {num_edges}'
        )


def change_default_layout() -> None:
    """Cytoscape: resets the default layout to `grid` to accelerate the import process
    (grid layout one of the fastest)

    Raises
    ------
    RequestException
        API endpoint not reachable or CyREST operation not successful
    """
    body: dict[str, str] = {'value': 'grid', 'key': 'layout.default'}
    try:
        p4c.cyrest_put('properties/cytoscape3.props/layout.default', body=body)
    except RequestException as error:
        logger.error('[CytoAPIConnection] Property change of default layout not successful.')
        raise error


def import_to_cytoscape(
    graph: DiGraph | Graph,
    network_name: str = CYTO_BASE_NETWORK_NAME,
    sandbox_name: str = CYTO_SANDBOX_NAME,
    reinitialise_sandbox: bool = True,
) -> None:
    """Cytoscape: import NetworkX graph as new network collection

    Parameters
    ----------
    graph : DiGraph | Graph
        NetworkX graph object
    """
    logger.debug('Checking Cytoscape connection...')
    verify_connection()
    logger.debug('Checking graph size for rendering...')
    verify_graph_render_size(graph)
    logger.debug('Setting default layout to improve import speed...')
    change_default_layout()
    logger.debug('Setting Cytoscape sandbox...')
    p4c.sandbox_set(
        sandbox_name=sandbox_name,
        reinitialize=reinitialise_sandbox,
        copy_samples=False,
    )
    logger.debug('Importing to and analysing network in Cytoscape...')
    p4c.delete_all_networks()
    p4c.create_network_from_networkx(
        graph,
        title=network_name,
        collection=CYTO_COLLECTION_NAME,
    )
    analyse_network(network_name=network_name)
    logger.debug('Import and analysis of network to Cytoscape successful.')


def verify_table_property(
    property: str,
    table_type: Literal['node', 'edge', 'network'] = 'node',
    network_name: str = CYTO_BASE_NETWORK_NAME,
) -> bool:
    table = p4c.get_table_columns(table=table_type, network=network_name)

    return property in table.columns


def analyse_network(
    property_degree_weighted: str = PROPERTY_NAME_DEGREE_WEIGHTED,
    network_name: str = CYTO_BASE_NETWORK_NAME,
) -> None:
    node_table = p4c.get_table_columns(table='node', network=network_name)
    net_analyse_possible: bool = True
    if len(node_table) < 4:
        net_analyse_possible = False

    if net_analyse_possible:
        p4c.analyze_network(directed=False)
        node_table = p4c.get_table_columns(table='node', network=network_name)
        node_table['stress_norm'] = node_table['Stress'] / node_table['Stress'].max()
        node_table[CYTO_SELECTION_PROPERTY] = (
            node_table[property_degree_weighted]
            * node_table['BetweennessCentrality']
            * node_table['stress_norm']
        )
    else:
        node_table[CYTO_SELECTION_PROPERTY] = 1

    p4c.load_table_data(node_table, data_key_column='name', network=network_name)


def reset_current_network_to_base() -> None:
    """resets to currently selected network in Cytoscape back to the base one"""
    p4c.set_current_network(CYTO_BASE_NETWORK_NAME)


def fit_content(
    zoom_factor: float = CYTO_NETWORK_ZOOM_FACTOR,
    network_name: str = CYTO_BASE_NETWORK_NAME,
) -> None:
    p4c.hide_all_panels()
    p4c.fit_content(selected_only=False, network=network_name)
    zoom_current = p4c.get_network_zoom(network=network_name)
    zoom_new = zoom_current * zoom_factor
    p4c.set_network_zoom_bypass(zoom_new, bypass=False, network=network_name)


def export_network_to_image(
    filename: str,
    target_folder: Path = SAVE_PATH_FOLDER,
    filetype: CytoExportFileTypes = 'SVG',
    network_name: str = CYTO_BASE_NETWORK_NAME,
    pdf_export_page_size: CytoExportPageSizes = 'A4',
    sandbox_name: str = CYTO_SANDBOX_NAME,
) -> None:
    """Cytoscape: export current selected view as image

    Parameters
    ----------
    filename : str
        export filename
    filetype : CytoExportFileTypes, optional
        export filetype supported by Cytoscape, by default 'SVG'
    network_name : str, optional
        network to export, by default CYTO_BASE_NETWORK_NAME
    pdf_export_page_size : CytoExportPageSizes, optional
        page size which should be used for PDF exports supported by Cytoscape,
        by default 'A4'
    """
    logger.debug('Exporting image to file...')
    if not target_folder.exists():
        target_folder.mkdir(parents=True)
    dst_file_pth = (target_folder / filename).with_suffix(f'.{filetype.lower()}')

    text_as_font = True
    if filetype == 'SVG':
        text_as_font = False

    # close non-necessary windows and fit graph in frame before image display
    fit_content(network_name=network_name)
    # image is generated in sandbox directory and transferred to target destination
    # (preparation for remote instances of Cytoscape)
    p4c.export_image(
        filename=filename,
        type=filetype,
        network=network_name,
        overwrite_file=True,
        all_graphics_details=True,
        export_text_as_font=text_as_font,
        page_size=pdf_export_page_size,
    )
    # TODO remove if Cytoscape >= 3.10.* is running in container
    # p4c.export_image(
    #     filename=filename,
    #     type=filetype,
    #     network=network_name,
    #     overwrite_file=True,
    # )
    logger.debug('Exported image to sandbox.')
    logger.debug('Transferring image from sandbox to target destination...')
    sandbox_filename = f'{filename}.{filetype.lower()}'
    p4c.sandbox_get_from(
        source_file=sandbox_filename,
        dest_file=str(dst_file_pth),
        overwrite=True,
        sandbox_name=sandbox_name,
    )
    logger.debug('Transfer of image from sandbox to target destination successful.')


def layout_network(
    layout_name: CytoLayouts = CYTO_LAYOUT_NAME,
    layout_properties: CytoLayoutProperties = CYTO_LAYOUT_PROPERTIES,
    network_name: str = CYTO_BASE_NETWORK_NAME,
) -> None:
    """Cytoscape: apply a supported layout algorithm to currently selected
    network

    Parameters
    ----------
    layout_name : CytoLayouts, optional
        layout algorithm supported by Cytoscape (name of the CyREST API, does not
        necessarily match the name in the Cytoscape UI),
        by default CYTO_LAYOUT_NAME
    layout_properties : CytoLayoutProperties, optional
        configuration of parameters for the given layout algorithm,
        by default CYTO_LAYOUT_PROPERTIES
    network_name : str, optional
        network to apply the layout algorithm on, by default CYTO_BASE_NETWORK_NAME
    """
    logger.debug('Applying layout to network...')
    p4c.set_layout_properties(layout_name, layout_properties)
    p4c.layout_network(layout_name=layout_name, network=network_name)
    fit_content(network_name=network_name)
    logger.debug('Layout application to network successful.')


def apply_style_to_network(
    style_name: str = CYTO_STYLESHEET_NAME,
    pth_to_stylesheet: Path = CYTO_PATH_STYLESHEET,
    network_name: str = CYTO_BASE_NETWORK_NAME,
    node_size_property: str = 'node_selection',
    min_node_size: int = 15,
    max_node_size: int = 40,
    sandbox_name: str = CYTO_SANDBOX_NAME,
) -> None:
    """Cytoscape: apply a chosen Cytoscape style to the defined network

    Parameters
    ----------
    style_name : str, optional
        Cytoscape name of the style which should be applied,
        by default CYTO_STYLESHEET_NAME
    pth_to_stylesheet : Path, optional
        path where the stylesheet definition in Cytoscape's XML format can
        be found,
        by default CYTO_PATH_STYLESHEET
    network_name : str, optional
        network to apply the style on, by default CYTO_BASE_NETWORK_NAME

    Raises
    ------
    FileNotFoundError
        if provided stylesheet can not be found under the provided path
    """
    logger.debug('Applying style to network...')
    styles_avail = cast(list[str], p4c.get_visual_style_names())
    if style_name not in styles_avail:
        if not pth_to_stylesheet.exists():
            # existence for standard path verified at import, but not for other
            # provided paths
            raise FileNotFoundError(
                f'Visual stylesheet for Cytoscape not found under: >>{pth_to_stylesheet}<<'
            )
        # send to sandbox
        sandbox_filename = pth_to_stylesheet.name
        p4c.sandbox_send_to(
            source_file=pth_to_stylesheet,
            dest_file=sandbox_filename,
            overwrite=True,
            sandbox_name=sandbox_name,
        )
        # load stylesheet
        p4c.import_visual_styles(sandbox_filename)

    p4c.set_visual_style(style_name, network=network_name)
    # node size mapping, only if needed property is available
    # TODO check removal
    # size_prop_available = verify_table_property(
    #     property=node_size_property,
    #     network_name=network_name,
    # )
    # if size_prop_available:
    scheme = p4c.scheme_c_number_continuous(
        start_value=min_node_size, end_value=max_node_size
    )
    node_size_map = p4c.gen_node_size_map(
        node_size_property,
        number_scheme=scheme,
        mapping_type='c',
        style_name=style_name,
        default_number=min_node_size,
    )
    p4c.set_node_size_mapping(**node_size_map)
    # TODO removal
    # else:
    #     node_table = p4c.get_table_columns(table='node', network=network_name)
    #     nodes_SUID = node_table['SUID'].to_list()
    #     p4c.set_node_size_bypass(nodes_SUID, new_sizes=min_node_size, network=network_name)
    # p4c.set_visual_style(style_name, network=network_name)
    # time.sleep(1)  # if not waited image export could be without applied style
    fit_content(network_name=network_name)
    logger.debug('Style application to network successful.')


def get_subgraph_node_selection(
    network_name: str = CYTO_BASE_NETWORK_NAME,
    num_subgraphs: int = CYTO_NUMBER_SUBGRAPHS,
) -> list[CytoNodeID]:
    """Cytoscape: obtain the relevant nodes for iterative subgraph generation

    Parameters
    ----------
    network_name : str, optional
        network to retrieve the nodes from, by default CYTO_BASE_NETWORK_NAME
    property_degree_weighted : str, optional
        property name which contains the weighted degree,
        by default PROPERTY_NAME_DEGREE_WEIGHTED
    num_subgraphs : int, optional
        number of relevant nodes which form the basis to generate subgraphs from,
        by default CYTO_NUMBER_SUBGRAPHS

    Returns
    -------
    list[CytoNodeID]
        list containing all relevant Cytoscape nodes
    """
    logger.debug('Selecting nodes for subgraph generation...')
    node_table = p4c.get_table_columns(table='node', network=network_name)
    node_table = node_table.sort_values(by=CYTO_SELECTION_PROPERTY, ascending=False)
    p4c.load_table_data(node_table, data_key_column='name', network=network_name)
    node_table_choice = node_table.iloc[:num_subgraphs, :]
    logger.debug('Selection of nodes for subgraph generation successful.')

    return node_table_choice['SUID'].to_list()


def select_neighbours_of_node(
    node: CytoNodeID,
    neighbour_iter_depth: int = CYTO_ITER_NEIGHBOUR_DEPTH,
    network_name: str = CYTO_BASE_NETWORK_NAME,
) -> None:
    """Cytoscape: iterative selection of a node's neighbouring nodes and
    their connecting edges

    Parameters
    ----------
    node : CytoNodeID
        node which neighbours should be selected
    neighbour_iter_depth : int, optional
        indicates how many levels of neighbours should be choosen, e.g. 1 --> only
        first-level neighbours are considered which are directly connected to the node,
        2 --> all nodes with iteration depth of 1 are chosen and additionally their
        direct neighbours,
        by default CYTO_ITER_NEIGHBOUR_DEPTH
    network_name : str, optional
        network to perform action on, by default CYTO_BASE_NETWORK_NAME
    """
    logger.debug('Selecting node neighbours for %s...', node)
    p4c.clear_selection(network=network_name)
    p4c.select_nodes(node, network=network_name)

    for _ in range(neighbour_iter_depth):
        _ = p4c.select_first_neighbors(network=network_name)

    _ = p4c.select_edges_connecting_selected_nodes()
    logger.debug('Selection of node neighbours for %s successful.', node)


def make_subnetwork(
    index: int,
    network_name: str = CYTO_BASE_NETWORK_NAME,
    export_image: bool = True,
    target_folder: Path = SAVE_PATH_FOLDER,
) -> None:
    """Cytoscape: generate a new subnetwork based on the currently
    selected nodes and edges

    Parameters
    ----------
    index : int
        id-like property to identify the subnetwork relative to its parent
    network_name : str, optional
        network to generate subnetwork from, by default CYTO_BASE_NETWORK_NAME
    export_image : bool, optional
        trigger image export of newly generated subnetwork, by default True
    """
    logger.debug('Generating subnetwork with index %d...', index)
    subnetwork_name = network_name + f'_sub_{index+1}'
    p4c.create_subnetwork(
        nodes='selected',
        edges='selected',
        subnetwork_name=subnetwork_name,
        network=network_name,
    )
    p4c.set_current_network(subnetwork_name)

    if export_image:
        time.sleep(1)
        export_network_to_image(
            filename=subnetwork_name,
            target_folder=target_folder,
            network_name=subnetwork_name,
        )

    logger.debug('Generation of subnetwork with index %d successful.', index)


def build_subnetworks(
    nodes_to_analyse: Iterable[CytoNodeID],
    network_name: str = CYTO_BASE_NETWORK_NAME,
    export_image: bool = True,
    target_folder: Path = SAVE_PATH_FOLDER,
) -> None:
    """Cytoscape: iteratively build subnetworks from a collection of nodes
    and their respective neighbouring nodes

    Parameters
    ----------
    nodes_to_analyse : Iterable[CytoNodeID]
        collection of nodes to make subnetworks from, for each node a dedicated
        subnetwork will be generated
    network_name : str, optional
        network which contains the provided nodes,
        by default CYTO_BASE_NETWORK_NAME
    export_image : bool, optional
        trigger image export of newly generated subnetworks, by default True
    """
    logger.debug('Generating all subnetworks for node selection...')
    for idx, node in enumerate(nodes_to_analyse):
        select_neighbours_of_node(node=node, network_name=network_name)
        make_subnetwork(
            index=idx,
            network_name=network_name,
            export_image=export_image,
            target_folder=target_folder,
        )
    logger.debug('Generation of all subnetworks for node selection successful.')
