"""
Merkle Tree Implementation

Provides Merkle tree construction and verification for blockchain blocks.
"""

import hashlib
from typing import List, Optional, Tuple
from dataclasses import dataclass


@dataclass
class MerkleProof:
    """Proof of inclusion in a Merkle tree"""
    leaf_hash: str
    proof_hashes: List[Tuple[str, str]]  # (hash, position: 'left' or 'right')
    root_hash: str


class MerkleTree:
    """
    Merkle tree for efficient data verification.

    Features:
    - Binary tree construction
    - Root hash calculation
    - Proof generation and verification
    - Efficient incremental updates
    """

    def __init__(self, data_items: List[str]):
        """
        Construct Merkle tree from data items.

        Args:
            data_items: List of data strings to include in tree
        """
        if not data_items:
            raise ValueError("Cannot create Merkle tree from empty data")

        self._leaves = [self._hash(item) for item in data_items]
        self._root = self._build_tree(self._leaves)

    @staticmethod
    def _hash(data: str) -> str:
        """Hash a data item"""
        return hashlib.sha256(data.encode()).hexdigest()

    @staticmethod
    def _combine_hashes(left: str, right: str) -> str:
        """Combine two hashes"""
        combined = left + right
        return hashlib.sha256(combined.encode()).hexdigest()

    def _build_tree(self, hashes: List[str]) -> str:
        """
        Recursively build Merkle tree and return root hash.

        Args:
            hashes: List of hashes at current level

        Returns:
            Root hash
        """
        if len(hashes) == 1:
            return hashes[0]

        # If odd number of hashes, duplicate the last one
        if len(hashes) % 2 == 1:
            hashes = hashes + [hashes[-1]]

        # Combine pairs of hashes
        next_level = []
        for i in range(0, len(hashes), 2):
            combined = self._combine_hashes(hashes[i], hashes[i + 1])
            next_level.append(combined)

        return self._build_tree(next_level)

    @property
    def root(self) -> str:
        """Get Merkle root hash"""
        return self._root

    def generate_proof(self, data_item: str) -> Optional[MerkleProof]:
        """
        Generate proof of inclusion for a data item.

        Args:
            data_item: Data item to prove inclusion of

        Returns:
            MerkleProof if item exists, None otherwise
        """
        leaf_hash = self._hash(data_item)

        try:
            index = self._leaves.index(leaf_hash)
        except ValueError:
            return None  # Item not in tree

        proof_hashes = self._generate_proof_hashes(self._leaves, index)

        return MerkleProof(
            leaf_hash=leaf_hash,
            proof_hashes=proof_hashes,
            root_hash=self._root
        )

    def _generate_proof_hashes(
        self,
        hashes: List[str],
        index: int
    ) -> List[Tuple[str, str]]:
        """
        Generate proof hashes for an item at index.

        Returns:
            List of (hash, position) tuples
        """
        if len(hashes) == 1:
            return []

        # Duplicate last hash if odd number
        if len(hashes) % 2 == 1:
            hashes = hashes + [hashes[-1]]

        proof = []

        # Determine sibling hash
        if index % 2 == 0:
            # Even index, sibling is to the right
            sibling_hash = hashes[index + 1]
            position = 'right'
        else:
            # Odd index, sibling is to the left
            sibling_hash = hashes[index - 1]
            position = 'left'

        proof.append((sibling_hash, position))

        # Move to next level
        next_level = []
        for i in range(0, len(hashes), 2):
            combined = self._combine_hashes(hashes[i], hashes[i + 1])
            next_level.append(combined)

        next_index = index // 2
        proof.extend(self._generate_proof_hashes(next_level, next_index))

        return proof

    @staticmethod
    def verify_proof(proof: MerkleProof, data_item: str) -> bool:
        """
        Verify a Merkle proof.

        Args:
            proof: MerkleProof to verify
            data_item: Original data item

        Returns:
            True if proof is valid, False otherwise
        """
        # Hash the data item
        current_hash = MerkleTree._hash(data_item)

        # Verify it matches the leaf hash in proof
        if current_hash != proof.leaf_hash:
            return False

        # Walk up the tree using proof hashes
        for sibling_hash, position in proof.proof_hashes:
            if position == 'left':
                current_hash = MerkleTree._combine_hashes(sibling_hash, current_hash)
            else:
                current_hash = MerkleTree._combine_hashes(current_hash, sibling_hash)

        # Check if we arrived at the root
        return current_hash == proof.root_hash


def create_merkle_root(data_items: List[str]) -> str:
    """
    Convenience function to create Merkle root from data items.

    Args:
        data_items: List of data strings

    Returns:
        Merkle root hash
    """
    if not data_items:
        return hashlib.sha256(b"").hexdigest()

    tree = MerkleTree(data_items)
    return tree.root
