"""Response handling for GQLDB Python driver."""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, TypeVar

from .types import TypedValue, PropertyType, Schema, PropertyDef, Table, Header, Attr
from .types.graph_models import GqldbNode, GqldbEdge, GqldbPath

T = TypeVar("T")


@dataclass
class Row:
    """Represents a single row in the query result."""
    values: List[TypedValue] = field(default_factory=list)

    def get(self, index: int) -> Any:
        """Get the value at the given column index.

        Args:
            index: Column index.

        Returns:
            The value at the index.

        Raises:
            IndexError: If index is out of range.
        """
        if index < 0 or index >= len(self.values):
            raise IndexError(f"Column index out of range: {index}")
        return self.values[index].to_python()

    def get_string(self, index: int) -> str:
        """Get the value at the given column index as a string.

        Args:
            index: Column index.

        Returns:
            The value as a string.
        """
        val = self.get(index)
        if val is None:
            return ""
        if isinstance(val, str):
            return val
        return str(val)

    def get_int(self, index: int) -> int:
        """Get the value at the given column index as an integer.

        Args:
            index: Column index.

        Returns:
            The value as an integer.
        """
        val = self.get(index)
        if val is None:
            return 0
        if isinstance(val, (int, float)):
            return int(val)
        raise TypeError(f"Cannot convert {type(val)} to int")

    def get_float(self, index: int) -> float:
        """Get the value at the given column index as a float.

        Args:
            index: Column index.

        Returns:
            The value as a float.
        """
        val = self.get(index)
        if val is None:
            return 0.0
        if isinstance(val, (int, float)):
            return float(val)
        raise TypeError(f"Cannot convert {type(val)} to float")

    def get_bool(self, index: int) -> bool:
        """Get the value at the given column index as a boolean.

        Args:
            index: Column index.

        Returns:
            The value as a boolean.
        """
        val = self.get(index)
        if val is None:
            return False
        if isinstance(val, bool):
            return val
        raise TypeError(f"Cannot convert {type(val)} to bool")


@dataclass
class Response:
    """Represents the result of a GQL query."""
    columns: List[str] = field(default_factory=list)
    rows: List[Row] = field(default_factory=list)
    row_count: int = 0
    has_more: bool = False
    warnings: List[str] = field(default_factory=list)
    rows_affected: int = 0

    def is_empty(self) -> bool:
        """Check if the response has no rows.

        Returns:
            True if empty.
        """
        return len(self.rows) == 0

    def first(self) -> Optional[Row]:
        """Get the first row.

        Returns:
            First row or None if empty.
        """
        return self.rows[0] if self.rows else None

    def last(self) -> Optional[Row]:
        """Get the last row.

        Returns:
            Last row or None if empty.
        """
        return self.rows[-1] if self.rows else None

    def get_by_name(self, row: Row, column_name: str) -> Any:
        """Get the value for a column by name.

        Args:
            row: Row to get value from.
            column_name: Column name.

        Returns:
            The value.

        Raises:
            KeyError: If column not found.
        """
        try:
            index = self.columns.index(column_name)
            return row.get(index)
        except ValueError:
            raise KeyError(f"Column not found: {column_name}")

    def __iter__(self) -> Iterator[Row]:
        """Iterate over rows."""
        return iter(self.rows)

    def __len__(self) -> int:
        """Return the number of rows."""
        return len(self.rows)

    def for_each(self, fn: Callable[[Row, int], None]) -> None:
        """Iterate over all rows with a callback.

        Args:
            fn: Callback function receiving (row, index).
        """
        for i, row in enumerate(self.rows):
            fn(row, i)

    def map(self, fn: Callable[[Row], T]) -> List[T]:
        """Transform each row using a callback.

        Args:
            fn: Transformation function.

        Returns:
            List of transformed values.
        """
        return [fn(row) for row in self.rows]

    def to_dicts(self) -> List[Dict[str, Any]]:
        """Convert the response to a list of dictionaries.

        Returns:
            List of dictionaries.
        """
        result = []
        for row in self.rows:
            d = {}
            for i, col in enumerate(self.columns):
                d[col] = row.get(i)
            result.append(d)
        return result

    def to_json(self) -> str:
        """Convert the response to JSON.

        Returns:
            JSON string.
        """
        return json.dumps(self.to_dicts(), default=str)

    def single_value(self) -> Any:
        """Get the single value from a single-row, single-column response.

        Returns:
            The single value.

        Raises:
            ValueError: If response has multiple rows or columns.
        """
        if not self.rows:
            return None
        if len(self.rows) > 1:
            raise ValueError(f"Expected single row, got {len(self.rows)}")
        if not self.rows[0].values:
            return None
        if len(self.rows[0].values) > 1:
            raise ValueError(f"Expected single column, got {len(self.rows[0].values)}")
        return self.rows[0].get(0)

    def single_int(self) -> int:
        """Get the single integer value.

        Returns:
            The single integer value.
        """
        val = self.single_value()
        if val is None:
            return 0
        if isinstance(val, (int, float)):
            return int(val)
        raise TypeError(f"Cannot convert {type(val)} to int")

    def single_string(self) -> str:
        """Get the single string value.

        Returns:
            The single string value.
        """
        val = self.single_value()
        if val is None:
            return ""
        if isinstance(val, str):
            return val
        return str(val)

    def alias(self, column_name: str) -> AliasResult:
        """Get an AliasResult for the specified column name.

        Args:
            column_name: Column name/alias to extract.

        Returns:
            AliasResult for chaining.

        Raises:
            KeyError: If column not found.
        """
        try:
            col_idx = self.columns.index(column_name)
        except ValueError:
            raise KeyError(f"Column alias not found: {column_name}")

        return AliasResult(
            alias=column_name,
            column_idx=col_idx,
            response=self
        )

    def get(self, index: int) -> AliasResult:
        """Get an AliasResult for the column at the specified index.

        Args:
            index: Column index (0-based).

        Returns:
            AliasResult for chaining.

        Raises:
            IndexError: If index out of range.
        """
        if index < 0 or index >= len(self.columns):
            raise IndexError(f"Column index out of range: {index} (total: {len(self.columns)})")

        return AliasResult(
            alias=self.columns[index],
            column_idx=index,
            response=self
        )

    # DEPRECATED: The global as_nodes(), as_edges(), as_paths(), as_table(), as_attr() methods
    # have been removed. Use resp.alias("column").as_nodes() or resp.get(index).as_nodes() instead.

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

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

    def _infer_property_type(self, value: Any) -> PropertyType:
        """Infer property type from value."""
        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


@dataclass
class InsertNodesResult:
    """Result of an insert nodes operation."""
    success: bool = False
    node_ids: List[str] = field(default_factory=list)
    node_count: int = 0
    message: str = ""


@dataclass
class InsertEdgesResult:
    """Result of an insert edges operation."""
    success: bool = False
    edge_ids: List[str] = field(default_factory=list)
    edge_count: int = 0
    message: str = ""
    skipped_count: int = 0


@dataclass
class DeleteResult:
    """Result of a delete operation."""
    success: bool = False
    deleted_count: int = 0
    message: str = ""


@dataclass
class ExportConfig:
    """Configuration for the unified Export operation."""
    graph_name: str = ""
    batch_size: int = 0
    export_nodes: bool = True
    export_edges: bool = True
    node_labels: List[str] = field(default_factory=list)
    edge_labels: List[str] = field(default_factory=list)
    include_metadata: bool = False


@dataclass
class ExportStats:
    """Statistics for an export operation."""
    nodes_exported: int = 0
    edges_exported: int = 0
    bytes_written: int = 0
    duration_ms: int = 0


@dataclass
class ExportChunk:
    """A chunk of exported data in JSON Lines format."""
    data: bytes = field(default_factory=bytes)
    is_final: bool = False
    stats: Optional[ExportStats] = None

    def get_data_as_string(self) -> str:
        """Get the data as a UTF-8 string."""
        return self.data.decode('utf-8') if self.data else ""

    def has_stats(self) -> bool:
        """Check if statistics are available."""
        return self.stats is not None


@dataclass
class ExportNodesResult:
    """A batch of exported nodes.

    Deprecated: Use export() with ExportConfig instead.
    """
    nodes: List["ExportedNode"] = field(default_factory=list)
    has_more: bool = False


@dataclass
class ExportEdgesResult:
    """A batch of exported edges.

    Deprecated: Use export() with ExportConfig instead.
    """
    edges: List["ExportedEdge"] = field(default_factory=list)
    has_more: bool = False


@dataclass
class ExportedNode:
    """A node exported from the database.

    Deprecated: Use export() with ExportConfig instead.
    """
    id: str = ""
    labels: List[str] = field(default_factory=list)
    properties: Dict[str, Any] = field(default_factory=dict)


@dataclass
class ExportedEdge:
    """An edge exported from the database.

    Deprecated: Use export() with ExportConfig instead.
    """
    id: str = ""
    label: str = ""
    from_node_id: str = ""
    to_node_id: str = ""
    properties: Dict[str, Any] = field(default_factory=dict)


@dataclass
class Node:
    """Represents a node in the graph database."""
    id: str = ""
    labels: List[str] = field(default_factory=list)
    properties: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict:
        return {
            'id': self.id,
            'schema': self.labels[0] if self.labels else '',
            'uuid': self.properties.get('uuid'),
            'values': dict(self.properties)
        }


@dataclass
class Edge:
    """Represents an edge in the graph database."""
    id: str = ""
    label: str = ""
    from_node_id: str = ""
    to_node_id: str = ""
    properties: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict:
        return {
            'id': self.id,
            'fromId': self.from_node_id,
            'toId': self.to_node_id,
            'schema': self.label,
            'fromUuid': self.properties.get('fromUuid'),
            'toUuid': self.properties.get('toUuid'),
            'uuid': self.properties.get('uuid'),
            'values': dict(self.properties)
        }


@dataclass
class Path:
    """Represents a path in the graph database."""
    nodes: List[Node] = field(default_factory=list)
    edges: List[Edge] = field(default_factory=list)


@dataclass
class AliasResult:
    """Represents a single column's data from the response.

    Allows chaining operations like resp.alias("n2").as_nodes() or resp.get(1).as_nodes().
    """
    alias: str              # Column name
    column_idx: int         # Column index
    response: 'Response'    # Parent response reference

    def _collect_node(self, val: Any, nodes: List[Node], schemas: Dict[str, Schema]) -> None:
        """Convert a GqldbNode to Node and add to the list."""
        if isinstance(val, GqldbNode):
            node = Node(id=val.id, labels=val.labels, properties=val.properties)
            nodes.append(node)
            for label in node.labels:
                if label not in schemas:
                    schemas[label] = self.response._build_schema_from_node(label, node)

    def _collect_edge(self, val: Any, edges: List[Edge], schemas: Dict[str, Schema]) -> None:
        """Convert a GqldbEdge to Edge and add to the list."""
        if isinstance(val, GqldbEdge):
            edge = Edge(id=val.id, label=val.label, from_node_id=val.from_node_id,
                        to_node_id=val.to_node_id, properties=val.properties)
            edges.append(edge)
            if edge.label and edge.label not in schemas:
                schemas[edge.label] = self.response._build_schema_from_edge(edge.label, edge)

    def as_nodes(self) -> Tuple[List[Node], Dict[str, Schema]]:
        """Extract nodes from the aliased column.

        Supports both direct NODE columns and LIST of NODE columns
        (e.g., group variables from quantified patterns).

        Returns:
            Tuple of (list of nodes, schema map).

        Raises:
            TypeError: If the column contains non-null values that are not nodes or lists of nodes.
        """
        nodes: List[Node] = []
        schemas: Dict[str, Schema] = {}

        for row in self.response.rows:
            if self.column_idx >= len(row.values):
                continue

            tv = row.values[self.column_idx]

            if tv.is_null:
                continue

            if tv.type == PropertyType.NODE:
                self._collect_node(tv.to_python(), nodes, schemas)
            elif tv.type == PropertyType.LIST:
                # LIST of NODE (group variable from quantified pattern)
                for item in tv.to_python():
                    self._collect_node(item, nodes, schemas)
            else:
                raise TypeError(
                    f"Type mismatch: column '{self.alias}' (index {self.column_idx}) "
                    f"contains {tv.type}, expected NODE or LIST of NODE"
                )

        return nodes, schemas

    def as_edges(self) -> Tuple[List[Edge], Dict[str, Schema]]:
        """Extract edges from the aliased column.

        Supports both direct EDGE columns and LIST of EDGE columns
        (e.g., group variables from quantified patterns).

        Returns:
            Tuple of (list of edges, schema map).

        Raises:
            TypeError: If the column contains non-null values that are not edges or lists of edges.
        """
        edges: List[Edge] = []
        schemas: Dict[str, Schema] = {}

        for row in self.response.rows:
            if self.column_idx >= len(row.values):
                continue

            tv = row.values[self.column_idx]

            if tv.is_null:
                continue

            if tv.type == PropertyType.EDGE:
                self._collect_edge(tv.to_python(), edges, schemas)
            elif tv.type == PropertyType.LIST:
                # LIST of EDGE (group variable from quantified pattern)
                for item in tv.to_python():
                    self._collect_edge(item, edges, schemas)
            else:
                raise TypeError(
                    f"Type mismatch: column '{self.alias}' (index {self.column_idx}) "
                    f"contains {tv.type}, expected EDGE or LIST of EDGE"
                )

        return edges, schemas

    def as_paths(self) -> List[Path]:
        """Extract paths from the aliased column.

        Returns:
            List of paths.

        Raises:
            TypeError: If the column contains non-null values that are not paths.
        """
        paths: List[Path] = []

        for row in self.response.rows:
            # Boundary check
            if self.column_idx >= len(row.values):
                continue

            tv = row.values[self.column_idx]

            # Skip if null
            if tv.is_null:
                continue

            # Check type mismatch
            if tv.type != PropertyType.PATH:
                raise TypeError(
                    f"Type mismatch: column '{self.alias}' (index {self.column_idx}) "
                    f"contains {tv.type}, expected PATH"
                )

            val = tv.to_python()
            if isinstance(val, GqldbPath):
                # Convert GqldbNode/GqldbEdge to Node/Edge
                nodes = [Node(id=n.id, labels=n.labels, properties=n.properties) for n in val.nodes]
                edges = [Edge(id=e.id, label=e.label, from_node_id=e.from_node_id,
                              to_node_id=e.to_node_id, properties=e.properties) for e in val.edges]
                paths.append(Path(nodes=nodes, edges=edges))

        return paths

    def as_table(self) -> Table:
        """Convert the aliased column to table format.

        If the column contains a GqldbTable value (from the table() function),
        unwraps it into proper Table headers and rows with type inference.

        Returns:
            Table structure.
        """
        from .types.data_types import GqldbTable as GqldbTableType

        # Check if the first non-null value is a GqldbTable — if so, unwrap it
        for row in self.response.rows:
            if self.column_idx < len(row.values):
                tv = row.values[self.column_idx]
                if not tv.is_null and tv.type == PropertyType.TABLE:
                    val = tv.to_python()
                    if isinstance(val, GqldbTableType):
                        return self._unwrap_gqldb_table(val)
            break  # only check first row

        # Fallback: generic single-column table
        headers = [Header(name=self.alias, type=PropertyType.UNSET)]
        rows = []
        for row in self.response.rows:
            if self.column_idx < len(row.values):
                rows.append([row.get(self.column_idx)])
            else:
                rows.append([None])
        return Table(name=self.alias, headers=headers, rows=rows)

    def _unwrap_gqldb_table(self, gtable) -> Table:
        """Unwrap a GqldbTable into a Table with proper headers using original column types."""
        headers = []
        for i, col_name in enumerate(gtable.columns):
            if i < len(gtable.column_types):
                col_type = PropertyType(gtable.column_types[i])
            else:
                col_type = PropertyType.UNSET
            headers.append(Header(name=col_name, type=col_type))

        return Table(name=self.alias, headers=headers, rows=gtable.rows)

    def as_attr(self) -> Attr:
        """Extract scalar or list attribute values from the aliased column.

        Returns:
            Attr with values from the column.
        """
        values = []
        attr_type = PropertyType.UNSET

        for row in self.response.rows:
            if self.column_idx < len(row.values):
                tv = row.values[self.column_idx]
                if attr_type == PropertyType.UNSET and not tv.is_null:
                    attr_type = tv.type
                values.append(tv.to_python())

        return Attr(name=self.alias, type=attr_type, values=values)

    def as_values(self) -> List[Any]:
        """Extract raw Python values from the aliased column.

        Returns:
            List of Python values.
        """
        values = []

        for row in self.response.rows:
            if self.column_idx < len(row.values):
                values.append(row.get(self.column_idx))

        return values
