"""
Layer 3: Canonical Payload Hashing via Bencode
-----------------------------------------------
Produces deterministic SHA-256 hashes of API request/response content.

Flow: raw JSON bytes → parse → bencode (sorted keys, one encoding per value) → SHA-256

The payload_hash produced here becomes a field INSIDE the chain entry's
JSON payload. The chain (Layer 1) hashes it the same way it hashes every
other field. This module does NOT touch the chain hash format.

Type mapping (JSON → bencode):
    str   → bencode string   (<length>:<contents>)
    int   → bencode integer  (i<number>e)
    float → bencode string   (repr(value))
    bool  → bencode integer  (i1e / i0e)
    None  → bencode string   ("null")
    list  → bencode list     (l...e)
    dict  → bencode dict     (d...e, keys sorted by raw bytes)

Bencode format (BEP-0003/0052):
    strings:  <length>:<contents>   (length in ASCII decimal)
    integers: i<number>e
    lists:    l<item>...e
    dicts:    d<key><value>...e     (keys sorted by raw byte value)
"""

import hashlib
import json
from typing import Any


# ---------------------------------------------------------------------------
# Bencode encoder
# ---------------------------------------------------------------------------

def _bencode_str(s: str) -> bytes:
    """Encode a string as bencode: <length>:<contents>."""
    encoded = s.encode("utf-8")
    return f"{len(encoded)}:".encode("ascii") + encoded


def _bencode_bytes(b: bytes) -> bytes:
    """Encode raw bytes as bencode: <length>:<contents>."""
    return f"{len(b)}:".encode("ascii") + b


def _bencode_int(n: int) -> bytes:
    """Encode an integer as bencode: i<number>e."""
    return f"i{n}e".encode("ascii")


def bencode(obj: Any) -> bytes:
    """
    Recursively bencode a Python object.

    Handles: str, bytes, int, float, bool, None, list, dict.
    Dicts are encoded with keys sorted by their raw byte representation.
    """
    # bool MUST be checked before int (bool is a subclass of int in Python)
    if isinstance(obj, bool):
        return _bencode_int(1 if obj else 0)
    if isinstance(obj, int):
        return _bencode_int(obj)
    if isinstance(obj, str):
        return _bencode_str(obj)
    if isinstance(obj, bytes):
        return _bencode_bytes(obj)
    if isinstance(obj, float):
        # Bencode has no float type. Encode as string representation.
        return _bencode_str(repr(obj))
    if obj is None:
        return _bencode_str("null")
    if isinstance(obj, (list, tuple)):
        parts = [bencode(item) for item in obj]
        return b"l" + b"".join(parts) + b"e"
    if isinstance(obj, dict):
        # Keys MUST be sorted by raw byte value (bencode spec)
        parts = []
        for key in sorted(obj.keys(), key=lambda k: k.encode("utf-8")):
            parts.append(_bencode_str(key))
            parts.append(bencode(obj[key]))
        return b"d" + b"".join(parts) + b"e"

    raise TypeError(f"Cannot bencode type: {type(obj).__name__}")


# ---------------------------------------------------------------------------
# Canonical hashing
# ---------------------------------------------------------------------------

def canonical_hash_from_bytes(raw_json: bytes) -> str:
    """
    Parse JSON bytes, bencode canonically, return SHA-256 hex digest.

    This is the primary entry point used by the middleware.
    Raises json.JSONDecodeError if input is not valid JSON.
    Raises TypeError if JSON contains non-serializable types.
    """
    parsed = json.loads(raw_json)
    canonical = bencode(parsed)
    return hashlib.sha256(canonical).hexdigest()


def canonical_hash_from_parsed(obj: Any) -> str:
    """Bencode a pre-parsed object and return SHA-256 hex digest."""
    canonical = bencode(obj)
    return hashlib.sha256(canonical).hexdigest()


def verify_hash(raw_json: bytes, expected_hash: str) -> bool:
    """Verify that JSON bytes produce the expected canonical hash."""
    return canonical_hash_from_bytes(raw_json) == expected_hash


# ---------------------------------------------------------------------------
# Bdecode (for round-trip verification in tests)
# ---------------------------------------------------------------------------

def _bdecode_str(data: bytes, idx: int) -> tuple:
    """Decode a bencode string starting at idx. Returns (string, next_idx)."""
    colon = data.index(b":", idx)
    length = int(data[idx:colon])
    start = colon + 1
    return data[start:start + length].decode("utf-8"), start + length


def _bdecode_int(data: bytes, idx: int) -> tuple:
    """Decode a bencode integer starting at idx. Returns (int, next_idx)."""
    end = data.index(b"e", idx)
    return int(data[idx + 1:end]), end + 1


def _bdecode_list(data: bytes, idx: int) -> tuple:
    """Decode a bencode list starting at idx. Returns (list, next_idx)."""
    idx += 1  # skip 'l'
    items = []
    while data[idx:idx + 1] != b"e":
        item, idx = _bdecode_next(data, idx)
        items.append(item)
    return items, idx + 1


def _bdecode_dict(data: bytes, idx: int) -> tuple:
    """Decode a bencode dict starting at idx. Returns (dict, next_idx)."""
    idx += 1  # skip 'd'
    result = {}
    while data[idx:idx + 1] != b"e":
        key, idx = _bdecode_str(data, idx)
        val, idx = _bdecode_next(data, idx)
        result[key] = val
    return result, idx + 1


def _bdecode_next(data: bytes, idx: int) -> tuple:
    """Decode the next bencode value at idx. Returns (value, next_idx)."""
    tag = data[idx:idx + 1]
    if tag == b"i":
        return _bdecode_int(data, idx)
    if tag == b"l":
        return _bdecode_list(data, idx)
    if tag == b"d":
        return _bdecode_dict(data, idx)
    if tag.isdigit():
        return _bdecode_str(data, idx)
    raise ValueError(f"Invalid bencode tag at position {idx}: {tag!r}")


def bdecode(data: bytes) -> Any:
    """Decode a complete bencode value from bytes."""
    result, end = _bdecode_next(data, 0)
    if end != len(data):
        raise ValueError(f"Trailing data at position {end}")
    return result
