"""Pretty printers for GQLDB data types."""

from typing import Any, Dict, List, Optional

from tabulate import tabulate

from .response import Response, Node, Edge, Path
from .types import Schema, Table


def print_nodes(nodes: List[Node], schemas: Optional[Dict[str, Schema]] = None) -> str:
    """Print nodes in tabular format grouped by schema.

    Args:
        nodes: List of nodes to print.
        schemas: Optional schema map for property ordering.

    Returns:
        Formatted string representation.
    """
    if not nodes:
        return "No nodes found."

    result = []
    last_schema = ""
    current_rows = []
    current_headers = []

    for node in nodes:
        if node is None:
            continue

        # Get the first label as schema name
        schema_name = node.labels[0] if node.labels else ""

        # Switch schema if needed
        if schema_name != last_schema:
            # Print previous table if exists
            if current_rows:
                result.append(tabulate(current_rows, headers=current_headers, tablefmt="grid"))
                result.append("")

            # Build new header
            current_headers = ["ID", "Labels"]
            if schemas and schema_name in schemas:
                for prop in schemas[schema_name].properties:
                    current_headers.append(prop.name)
            else:
                # Derive from node properties
                for key in node.properties.keys():
                    current_headers.append(key)

            current_rows = []
            last_schema = schema_name

        # Build row
        row = [node.id, _format_labels(node.labels)]
        if schemas and schema_name in schemas:
            for prop in schemas[schema_name].properties:
                row.append(_format_value(node.properties.get(prop.name)))
        else:
            for key in node.properties.keys():
                row.append(_format_value(node.properties.get(key)))

        current_rows.append(row)

    # Print last table
    if current_rows:
        result.append(tabulate(current_rows, headers=current_headers, tablefmt="grid"))

    result.append(f"\n{len(nodes)} node(s)")
    return "\n".join(result)


def print_nodes_without_schema(nodes: List[Node]) -> str:
    """Print nodes deriving schema from data.

    Args:
        nodes: List of nodes to print.

    Returns:
        Formatted string representation.
    """
    if not nodes:
        return "No nodes found."

    # Build schemas from nodes
    schemas: Dict[str, Schema] = {}
    for node in nodes:
        if node is None:
            continue
        for label in node.labels:
            if label not in schemas:
                schemas[label] = _build_schema_from_node(label, node)

    return print_nodes(nodes, schemas)


def print_edges(edges: List[Edge], schemas: Optional[Dict[str, Schema]] = None) -> str:
    """Print edges in tabular format grouped by schema.

    Args:
        edges: List of edges to print.
        schemas: Optional schema map for property ordering.

    Returns:
        Formatted string representation.
    """
    if not edges:
        return "No edges found."

    result = []
    last_schema = ""
    current_rows = []
    current_headers = []

    for edge in edges:
        if edge is None:
            continue

        schema_name = edge.label

        # Switch schema if needed
        if schema_name != last_schema:
            # Print previous table if exists
            if current_rows:
                result.append(tabulate(current_rows, headers=current_headers, tablefmt="grid"))
                result.append("")

            # Build new header
            current_headers = ["ID", "FROM", "TO", "Label"]
            if schemas and schema_name in schemas:
                for prop in schemas[schema_name].properties:
                    current_headers.append(prop.name)
            else:
                # Derive from edge properties
                for key in edge.properties.keys():
                    current_headers.append(key)

            current_rows = []
            last_schema = schema_name

        # Build row
        row = [edge.id, edge.from_node_id, edge.to_node_id, edge.label]
        if schemas and schema_name in schemas:
            for prop in schemas[schema_name].properties:
                row.append(_format_value(edge.properties.get(prop.name)))
        else:
            for key in edge.properties.keys():
                row.append(_format_value(edge.properties.get(key)))

        current_rows.append(row)

    # Print last table
    if current_rows:
        result.append(tabulate(current_rows, headers=current_headers, tablefmt="grid"))

    result.append(f"\n{len(edges)} edge(s)")
    return "\n".join(result)


def print_edges_without_schema(edges: List[Edge]) -> str:
    """Print edges deriving schema from data.

    Args:
        edges: List of edges to print.

    Returns:
        Formatted string representation.
    """
    if not edges:
        return "No edges found."

    # Build schemas from edges
    schemas: Dict[str, Schema] = {}
    for edge in edges:
        if edge is None:
            continue
        if edge.label and edge.label not in schemas:
            schemas[edge.label] = _build_schema_from_edge(edge.label, edge)

    return print_edges(edges, schemas)


def print_paths(paths: List[Path]) -> str:
    """Print paths with node-edge sequence notation.

    Args:
        paths: List of paths to print.

    Returns:
        Formatted string representation.
    """
    if not paths:
        return "No paths found."

    rows = []
    for i, path in enumerate(paths):
        if path is None:
            continue
        path_str = _format_path(path)
        rows.append([i, path_str])

    result = tabulate(rows, headers=["#", "Path"], tablefmt="grid")
    return result + f"\n{len(paths)} path(s)"


def print_table(table: Table) -> str:
    """Print generic table data.

    Args:
        table: Table to print.

    Returns:
        Formatted string representation.
    """
    if table is None or not table.headers:
        return "No table data."

    headers = [h.name for h in table.headers]
    rows = [[_format_value(v) for v in row] for row in table.rows]

    result = []
    if table.name:
        result.append(f"Table: {table.name}")

    result.append(tabulate(rows, headers=headers, tablefmt="grid"))
    result.append(f"\n{len(table.rows)} row(s)")
    return "\n".join(result)


def print_any(resp: Response) -> str:
    """Auto-detect type from Response and print accordingly.

    Args:
        resp: Response to print.

    Returns:
        Formatted string representation.
    """
    if resp is None or resp.is_empty():
        return "No data."

    # Try to auto-detect the type from the first column
    if resp.columns:
        try:
            ar = resp.get(0)

            # Try edges first
            try:
                edges, schemas = ar.as_edges()
                if edges:
                    return print_edges(edges, schemas)
            except (TypeError, Exception):
                pass

            # Try nodes
            try:
                nodes, schemas = ar.as_nodes()
                if nodes:
                    return print_nodes(nodes, schemas)
            except (TypeError, Exception):
                pass

            # Try paths
            try:
                paths = ar.as_paths()
                if paths:
                    return print_paths(paths)
            except (TypeError, Exception):
                pass
        except Exception:
            pass

    # Default to table format
    ar = resp.get(0)
    table = ar.as_table()
    return print_table(table)


# Helper functions

def _format_labels(labels: List[str]) -> str:
    """Format labels for display."""
    if not labels:
        return ""
    return "[" + ", ".join(labels) + "]"


def _format_value(v: Any) -> str:
    """Format a value for display."""
    if v is None:
        return "<nil>"
    if isinstance(v, list):
        return "[" + ", ".join(_format_value(item) for item in v) + "]"
    if isinstance(v, dict):
        parts = [f"{k}: {_format_value(val)}" for k, val in v.items()]
        return "{" + ", ".join(parts) + "}"
    return str(v)


def _format_path(path: Path) -> str:
    """Format a path for display."""
    if path is None:
        return "<nil>"

    parts = []
    node_idx = 0
    edge_idx = 0

    # Alternate between nodes and edges
    while node_idx < len(path.nodes) or edge_idx < len(path.edges):
        if node_idx < len(path.nodes):
            node = path.nodes[node_idx]
            label = ""
            if node.labels:
                label = ":" + node.labels[0]
            parts.append(f"({node.id}{label})")
            node_idx += 1

        if edge_idx < len(path.edges):
            edge = path.edges[edge_idx]
            parts.append(f"-[{edge.id}:{edge.label}]->")
            edge_idx += 1

    return " ".join(parts)


def _contains_any(lst: List[str], *items: str) -> bool:
    """Check if list contains any of the items."""
    for s in lst:
        if s in items:
            return True
    return False


def _build_schema_from_node(label: str, node: Node) -> Schema:
    """Build schema from a node."""
    from .types import PropertyDef, PropertyType
    properties = []
    for name, value in node.properties.items():
        properties.append(PropertyDef(name=name, type=_infer_property_type(value)))
    return Schema(name=label, properties=properties)


def _build_schema_from_edge(label: str, edge: Edge) -> Schema:
    """Build schema from an edge."""
    from .types import PropertyDef, PropertyType
    properties = []
    for name, value in edge.properties.items():
        properties.append(PropertyDef(name=name, type=_infer_property_type(value)))
    return Schema(name=label, properties=properties)


def _infer_property_type(value: Any):
    """Infer property type from value."""
    from .types import PropertyType
    if value is None:
        return PropertyType.NULL
    if isinstance(value, bool):
        return PropertyType.BOOL
    if isinstance(value, int):
        return PropertyType.INT64
    if isinstance(value, float):
        return PropertyType.DOUBLE
    if isinstance(value, str):
        return PropertyType.STRING
    if isinstance(value, bytes):
        return PropertyType.BLOB
    if isinstance(value, list):
        return PropertyType.LIST
    if isinstance(value, dict):
        return PropertyType.MAP
    return PropertyType.UNSET
