"""SHA-256 utilities and shallow hashing via singledispatch.

Ports ADS4All's Hash.hs Shallow type class to Python using
singledispatch + the __ads_hash__ protocol.
"""

from __future__ import annotations

import hashlib
import struct
from functools import singledispatch
from typing import Protocol, runtime_checkable


@runtime_checkable
class ShallowHashable(Protocol):
    """Any type that participates in ADS must implement this."""

    def __ads_hash__(self) -> bytes: ...


def sha256(data: bytes) -> bytes:
    """SHA-256 hash returning raw 32 bytes."""
    return hashlib.sha256(data).digest()


def combine_hashes(*hashes: bytes) -> bytes:
    """Concatenate hashes then SHA-256 the result."""
    return sha256(b"".join(hashes))


EMPTY_HASH: bytes = sha256(b"")


def hash_to_hex(h: bytes) -> str:
    """Convert a hash to its hex representation."""
    return h.hex()


def hex_to_hash(s: str) -> bytes:
    """Convert a hex string back to hash bytes."""
    return bytes.fromhex(s)


@singledispatch
def shallow_hash(value: object) -> bytes:
    """Compute the shallow hash of a value.

    For objects implementing __ads_hash__, delegates to that method.
    For built-in types, uses type-tagged SHA-256.
    """
    if hasattr(value, "__ads_hash__"):
        return value.__ads_hash__()  # type: ignore[union-attr]
    raise TypeError(
        f"Cannot shallow-hash {type(value).__name__}. "
        f"Implement __ads_hash__ or register with @shallow_hash.register."
    )


# CRITICAL: bool must be registered BEFORE int because bool is a subclass of int.
# singledispatch checks registrations in MRO order, so we register bool first
# to prevent bool values from hitting the int handler.
@shallow_hash.register(bool)
def _hash_bool(value: bool) -> bytes:
    return sha256(b"bool:" + (b"T" if value else b"F"))


@shallow_hash.register(int)
def _hash_int(value: int) -> bytes:
    return sha256(b"int:" + str(value).encode())


@shallow_hash.register(str)
def _hash_str(value: str) -> bytes:
    return sha256(b"str:" + value.encode())


@shallow_hash.register(float)
def _hash_float(value: float) -> bytes:
    return sha256(b"float:" + struct.pack("!d", value))


@shallow_hash.register(bytes)
def _hash_bytes(value: bytes) -> bytes:
    return sha256(b"bytes:" + value)


@shallow_hash.register(type(None))
def _hash_none(value: None) -> bytes:
    return sha256(b"None")


@shallow_hash.register(tuple)
def _hash_tuple(value: tuple) -> bytes:  # type: ignore[type-arg]
    return combine_hashes(b"tuple", *(shallow_hash(v) for v in value))


@shallow_hash.register(list)
def _hash_list(value: list) -> bytes:  # type: ignore[type-arg]
    if not value:
        return sha256(b"list:empty")
    return combine_hashes(b"list", *(shallow_hash(v) for v in value))
