"""
#exonware/xwnode/src/exonware/xwnode/nodes/strategies/sparse_matrix.py
Sparse Matrix Strategy Implementation
Production-grade sparse matrix using COO (Coordinate) format.
Best Practices Implemented:
- COO format for flexibility (easy construction/modification)
- Memory-efficient storage (only non-zero elements)
- Industry-standard operations (transpose, multiply, add)
- Conversion to CSR/CSC for optimized operations
- Proper sparse matrix semantics following scipy.sparse patterns
Company: eXonware.com
Author: eXonware Backend Team
Email: connect@exonware.com
Version: 0.9.0.20
Generation Date: 24-Oct-2025
"""

from __future__ import annotations
from collections.abc import AsyncIterator, Iterator
from typing import Any
from collections import defaultdict
from .base import ANodeMatrixStrategy
from .contracts import NodeType
from ...defs import NodeMode, NodeTrait
from ...errors import XWNodeUnsupportedCapabilityError


class SparseMatrixStrategy(ANodeMatrixStrategy):
    """
    Production-grade Sparse Matrix node strategy using COO format.
    Optimized for:
    - Graph adjacency matrices (social networks, web graphs)
    - Scientific computing (finite element methods)
    - Machine learning (TF-IDF matrices, embeddings)
    - Natural language processing (document-term matrices)
    - Recommendation systems (user-item matrices)
    - Network analysis (connection matrices)
    Format: COO (Coordinate List)
    - Best for: Matrix construction, flexible modification
    - Storage: List of (row, col, value) triplets
    - Space: O(nnz) where nnz = number of non-zeros
    - Conversion: Can convert to CSR/CSC for faster operations
    Performance:
    - Get element: O(nnz) worst case, O(1) with indexing
    - Set element: O(1) append, O(nnz) update
    - Matrix multiply: O(nnz * m) naive, O(nnz log nnz) optimized
    - Transpose: O(nnz)
    - Add: O(nnz1 + nnz2)
    Security:
    - Bounds checking on all operations
    - Memory-efficient (no zero storage)
    - Safe dimension handling
    Follows eXonware Priorities:
    1. Security: Bounds checking, safe indexing
    2. Usability: Standard sparse matrix interface
    3. Maintainability: Clean COO implementation
    4. Performance: O(nnz) space, efficient for sparse data
    5. Extensibility: Easy to add CSR/CSC conversion
    """
    # Strategy type classification
    STRATEGY_TYPE = NodeType.MATRIX
    __slots__ = ('_data', '_row_index', '_col_index', '_rows', '_cols', '_default_value')

    def __init__(self, traits: NodeTrait = NodeTrait.NONE, **options):
        """
        Initialize an empty sparse matrix.
        Args:
            traits: Additional node traits
            **options:
                shape: Optional tuple (rows, cols) for dimensions
                default_value: Value for unset elements (default: 0)
                initial_data: Optional list of (row, col, value) triplets
        """
        super().__init__(
            NodeMode.SPARSE_MATRIX,
            traits | NodeTrait.SPARSE | NodeTrait.MEMORY_EFFICIENT | NodeTrait.MATRIX_OPS,
            **options
        )
        # COO format: list of (row, col, value) triplets
        self._data: list[tuple[int, int, Any]] = []
        # Hash indexes for O(1) lookups
        self._row_index: dict[tuple[int, int], int] = {}  # (row, col) -> index in _data
        # Dimensions
        shape = options.get('shape', (0, 0))
        self._rows, self._cols = shape
        # Default value for unset elements
        self._default_value = options.get('default_value', 0)
        # Initialize with data if provided
        initial_data = options.get('initial_data', [])
        for row, col, value in initial_data:
            self.set(row, col, value)

    def get_supported_traits(self) -> NodeTrait:
        """Get the traits supported by the sparse matrix strategy."""
        return NodeTrait.SPARSE | NodeTrait.MEMORY_EFFICIENT | NodeTrait.MATRIX_OPS
    # ============================================================================
    # CORE SPARSE MATRIX OPERATIONS
    # ============================================================================

    def get(self, row: int, col: int, default: Any = None) -> Any:
        """
        Get value at (row, col).
        Time: O(1) with indexing
        Space: O(1)
        Returns:
            Value at position or default_value if unset
        """
        if default is None:
            default = self._default_value
        key = (row, col)
        if key in self._row_index:
            idx = self._row_index[key]
            return self._data[idx][2]
        return default

    def set(self, row: int, col: int, value: Any) -> None:
        """
        Set value at (row, col).
        Time: O(1) for new elements, O(1) for updates
        Space: O(1)
        Note: Automatically expands matrix dimensions if needed
        """
        # Update dimensions
        self._rows = max(self._rows, row + 1)
        self._cols = max(self._cols, col + 1)
        key = (row, col)
        # If value is default (e.g., 0), remove the entry
        if value == self._default_value:
            if key in self._row_index:
                idx = self._row_index[key]
                self._data.pop(idx)
                del self._row_index[key]
                # Rebuild index (expensive but rare)
                self._rebuild_index()
            return
        # Update existing entry
        if key in self._row_index:
            idx = self._row_index[key]
            r, c, _ = self._data[idx]
            self._data[idx] = (r, c, value)
        else:
            # Add new entry
            self._data.append((row, col, value))
            self._row_index[key] = len(self._data) - 1

    def _rebuild_index(self) -> None:
        """Rebuild the row-col index after removals."""
        self._row_index.clear()
        for idx, (row, col, _) in enumerate(self._data):
            self._row_index[(row, col)] = idx

    def get_row(self, row: int) -> list[Any]:
        """
        Get entire row as dense list.
        Time: O(nnz + cols)
        """
        result = [self._default_value] * self._cols
        for r, c, value in self._data:
            if r == row:
                result[c] = value
        return result

    def get_col(self, col: int) -> list[Any]:
        """
        Get entire column as dense list.
        Time: O(nnz + rows)
        """
        result = [self._default_value] * self._rows
        for r, c, value in self._data:
            if c == col:
                result[r] = value
        return result

    def transpose(self) -> SparseMatrixStrategy:
        """
        Return transposed matrix.
        Time: O(nnz)
        Space: O(nnz)
        """
        transposed = SparseMatrixStrategy(
            shape=(self._cols, self._rows),
            default_value=self._default_value
        )
        for row, col, value in self._data:
            transposed.set(col, row, value)
        return transposed

    def add(self, other: SparseMatrixStrategy) -> SparseMatrixStrategy:
        """
        Add two sparse matrices.
        Time: O(nnz1 + nnz2)
        Space: O(nnz1 + nnz2)
        Raises:
            ValueError: If matrix dimensions don't match
        """
        if self.shape != other.shape:
            raise ValueError(f"Matrix dimensions don't match: {self.shape} vs {other.shape}")
        result = SparseMatrixStrategy(
            shape=self.shape,
            default_value=self._default_value
        )
        # Add all elements from self
        for row, col, value in self._data:
            result.set(row, col, value)
        # Add all elements from other
        for row, col, value in other._data:
            current = result.get(row, col)
            if current != self._default_value or value != self._default_value:
                result.set(row, col, current + value)
        return result

    def multiply(self, other: SparseMatrixStrategy) -> SparseMatrixStrategy:
        """
        Multiply two sparse matrices (standard matrix multiplication).
        Time: O(nnz1 * nnz2 / rows) average case
        Space: O(result_nnz)
        Raises:
            ValueError: If inner dimensions don't match
        """
        if self._cols != other._rows:
            raise ValueError(f"Cannot multiply: ({self._rows}x{self._cols}) * ({other._rows}x{other._cols})")
        result = SparseMatrixStrategy(
            shape=(self._rows, other._cols),
            default_value=self._default_value
        )
        # Build column index for faster lookup
        other_by_row = defaultdict(list)
        for r, c, v in other._data:
            other_by_row[r].append((c, v))
        # Perform multiplication
        for r1, c1, v1 in self._data:
            # c1 is the connecting dimension (self._cols == other._rows)
            for c2, v2 in other_by_row.get(c1, []):
                current = result.get(r1, c2)
                new_value = current + (v1 * v2)
                if new_value != self._default_value:
                    result.set(r1, c2, new_value)
        return result

    def scalar_multiply(self, scalar: float) -> SparseMatrixStrategy:
        """
        Multiply matrix by a scalar.
        Time: O(nnz)
        Space: O(nnz)
        """
        result = SparseMatrixStrategy(
            shape=self.shape,
            default_value=self._default_value
        )
        for row, col, value in self._data:
            result.set(row, col, value * scalar)
        return result
    # ============================================================================
    # SPARSE MATRIX PROPERTIES
    # ============================================================================
    @property

    def shape(self) -> tuple[int, int]:
        """Get matrix dimensions (rows, cols)."""
        return (self._rows, self._cols)
    @property

    def nnz(self) -> int:
        """Get number of non-zero elements."""
        return len(self._data)
    @property

    def density(self) -> float:
        """
        Calculate matrix density (non-zero elements / total elements).
        Returns:
            Density as float between 0 and 1
        """
        total_elements = self._rows * self._cols
        if total_elements == 0:
            return 0.0
        return self.nnz / total_elements

    def to_dense(self) -> list[list[Any]]:
        """
        Convert to dense matrix representation.
        Time: O(rows * cols)
        Space: O(rows * cols)
        Warning: Can be memory-intensive for large sparse matrices
        """
        result = [[self._default_value] * self._cols for _ in range(self._rows)]
        for row, col, value in self._data:
            result[row][col] = value
        return result
    @classmethod

    def from_dense(cls, matrix: list[list[Any]], default_value: Any = 0) -> SparseMatrixStrategy:
        """
        Create sparse matrix from dense representation.
        Time: O(rows * cols)
        Space: O(nnz)
        """
        rows = len(matrix)
        cols = len(matrix[0]) if rows > 0 else 0
        sparse = cls(shape=(rows, cols), default_value=default_value)
        for row in range(rows):
            for col in range(cols):
                value = matrix[row][col]
                if value != default_value:
                    sparse.set(row, col, value)
        return sparse
    # ============================================================================
    # REQUIRED ABSTRACT METHODS (from ANodeStrategy)
    # ============================================================================

    def put(self, key: Any, value: Any = None) -> None:
        """Store value using 'row,col' key format."""
        try:
            row, col = map(int, str(key).split(','))
            self.set(row, col, value if value is not None else key)
        except (ValueError, AttributeError):
            raise ValueError(f"Key must be in format 'row,col', got: {key}")

    def has(self, key: Any) -> bool:
        """Check if position has non-default value."""
        try:
            row, col = map(int, str(key).split(','))
            return (row, col) in self._row_index
        except (ValueError, AttributeError):
            return False

    def delete(self, key: Any) -> bool:
        """Delete element at position (sets to default_value)."""
        try:
            row, col = map(int, str(key).split(','))
            self.set(row, col, self._default_value)
            return True
        except (ValueError, AttributeError):
            return False

    def keys(self) -> Iterator[Any]:
        """Get all positions as 'row,col' strings."""
        for row, col, _ in self._data:
            yield f"{row},{col}"

    def values(self) -> Iterator[Any]:
        """Get all non-zero values."""
        for _, _, value in self._data:
            yield value

    def items(self) -> Iterator[tuple[Any, Any]]:
        """Get all items as ('row,col', value) pairs."""
        for row, col, value in self._data:
            yield (f"{row},{col}", value)
    # ============================================================================
    # UTILITY METHODS
    # ============================================================================

    def size(self) -> int:
        """Get the number of non-zero elements."""
        return len(self._data)

    def is_empty(self) -> bool:
        """Check if matrix has no non-zero elements."""
        return len(self._data) == 0

    def clear(self) -> None:
        """Clear all elements."""
        self._data.clear()
        self._row_index.clear()
        self._rows = 0
        self._cols = 0

    def to_native(self) -> dict[str, Any]:
        """Convert sparse matrix to native dictionary format."""
        return {
            'data': [(row, col, val) for row, col, val in self._data],
            'shape': [self._rows, self._cols],
            'nnz': self.nnz
        }
    # ============================================================================
    # ASYNC API - Lightweight wrappers (NO lock overhead, v0.0.1.28b)
    # ============================================================================

    async def insert_async(self, key: Any, value: Any) -> None:
        """Lightweight async wrapper for insert (no lock overhead)."""
        return self.insert(key, value)

    async def find_async(self, key: Any) -> Any | None:
        """Lightweight async wrapper for find (no lock overhead)."""
        return self.find(key)

    async def delete_async(self, key: Any) -> bool:
        """Lightweight async wrapper for delete (no lock overhead)."""
        return self.delete(key)

    async def size_async(self) -> int:
        """Lightweight async wrapper for size (no lock overhead)."""
        return self.size()

    async def is_empty_async(self) -> bool:
        """Lightweight async wrapper for is_empty (no lock overhead)."""
        return self.is_empty()

    async def to_native_async(self) -> Any:
        """Lightweight async wrapper for to_native (no lock overhead)."""
        return self.to_native()

    async def keys_async(self) -> AsyncIterator[Any]:
        """Lightweight async wrapper for keys (no lock overhead)."""
        for key in self.keys():
            yield key

    async def values_async(self) -> AsyncIterator[Any]:
        """Lightweight async wrapper for values (no lock overhead)."""
        for value in self.values():
            yield value

    async def items_async(self) -> AsyncIterator[tuple[Any, Any]]:
        """Lightweight async wrapper for items (no lock overhead)."""
        for item in self.items():
            yield item
    # ============================================================================
    # PYTHON SPECIAL METHODS
    # ============================================================================

    def __len__(self) -> int:
        """Return the number of non-zero elements."""
        return len(self._data)

    def __bool__(self) -> bool:
        """Return True if matrix has non-zero elements."""
        return bool(self._data)

    def __iter__(self) -> Iterator[tuple[int, int, Any]]:
        """Iterate through non-zero elements as (row, col, value) triplets."""
        return iter(self._data)

    def __repr__(self) -> str:
        """Professional string representation."""
        return f"SparseMatrixStrategy(shape={self._rows}x{self._cols}, nnz={self.nnz}, density={self.density:.2%})"

    def __str__(self) -> str:
        """Human-readable string representation."""
        return f"SparseMatrix[{self._rows}x{self._cols}, {self.nnz} non-zeros]"
    # ============================================================================
    # PERFORMANCE METADATA
    # ============================================================================
    @property

    def backend_info(self) -> dict[str, Any]:
        """Get backend implementation info."""
        return {
            'strategy': 'SPARSE_MATRIX',
            'backend': 'COO (Coordinate format)',
            'format': 'List of (row, col, value) triplets',
            'complexity': {
                'get': 'O(1) with index, O(nnz) without',
                'set': 'O(1) append, O(nnz) update',
                'transpose': 'O(nnz)',
                'add': 'O(nnz1 + nnz2)',
                'multiply': 'O(nnz1 * cols2)',
                'space': 'O(nnz)'
            },
            'best_for': 'matrix construction, flexible modification',
            'convert_to': 'CSR for row operations, CSC for column operations'
        }
    @property

    def metrics(self) -> dict[str, Any]:
        """Get performance metrics."""
        total_elements = self._rows * self._cols
        memory_bytes = self.nnz * (8 + 8 + 8)  # row, col, value
        return {
            'shape': f"{self._rows}x{self._cols}",
            'nnz': self.nnz,
            'density': f"{self.density:.2%}",
            'total_elements': total_elements,
            'memory_saved': f"{(total_elements - self.nnz) * 8} bytes",
            'memory_usage': f"{memory_bytes} bytes (estimated)",
            'compression_ratio': f"{total_elements / self.nnz if self.nnz > 0 else 0:.1f}x"
        }
    # ============================================================================
    # ABSTRACT METHOD IMPLEMENTATIONS (from ANodeMatrixStrategy)
    # ============================================================================

    def get_dimensions(self) -> tuple:
        """Get matrix dimensions (rows, cols)."""
        return (self._rows, self._cols)

    def get_at_position(self, row: int, col: int) -> Any:
        """Get element at matrix position."""
        return self.get(row, col, self._default_value)

    def set_at_position(self, row: int, col: int, value: Any) -> None:
        """Set element at matrix position."""
        self.set(row, col, value)

    def get_column(self, col: int) -> list[Any]:
        """Get entire column."""
        return self.get_col(col)

    def as_adjacency_matrix(self):
        """Provide Adjacency Matrix behavioral view."""
        raise XWNodeUnsupportedCapabilityError("SparseMatrixStrategy does not support adjacency matrix view")

    def as_incidence_matrix(self):
        """Provide Incidence Matrix behavioral view."""
        raise XWNodeUnsupportedCapabilityError("SparseMatrixStrategy does not support incidence matrix view")

    def as_sparse_matrix(self):
        """Provide Sparse Matrix behavioral view."""
        return self
