from __future__ import annotations

import dataclasses as _dc
import logging
import typing as t

from core.utils.time import maybe_normalize_unix_timestamp_seconds
from rpc import deps
from rpc.methods import method

try:
    from core.utils.deploy_metadata import (
        build_python_vm_package_deploy_metadata as _build_pyvm_deploy_metadata,
        copy_deploy_metadata_fields as _copy_deploy_metadata_fields,
    )
except Exception:  # pragma: no cover
    _build_pyvm_deploy_metadata = None  # type: ignore[assignment]
    _copy_deploy_metadata_fields = None  # type: ignore[assignment]

log = logging.getLogger(__name__)

# Prefer canonical encoders from core; fall back to CBOR if needed.
try:
    from core.encoding.canonical import \
        header_sign_bytes as _header_sign_bytes  # type: ignore
    from core.encoding.canonical import \
        tx_sign_bytes as _tx_sign_bytes  # type: ignore
except Exception:  # pragma: no cover
    _header_sign_bytes = None  # type: ignore
    _tx_sign_bytes = None  # type: ignore
    try:
        from core.encoding.cbor import dumps as _cbor_dumps  # type: ignore
    except Exception:  # pragma: no cover
        _cbor_dumps = None  # type: ignore

try:
    from core.utils.hash import sha3_256 as _sha3_256  # type: ignore
except Exception:  # pragma: no cover
    import hashlib

    def _sha3_256(b: bytes) -> bytes:  # type: ignore
        return hashlib.sha3_256(b).digest()


# -----------------------
# Helpers
# -----------------------


def _dcd(obj: t.Any) -> t.Any:
    """Dataclass → dict (deep)."""
    if _dc.is_dataclass(obj):
        return {k: _dcd(v) for k, v in _dc.asdict(obj).items()}
    if isinstance(obj, (list, tuple)):
        return [_dcd(x) for x in obj]
    if isinstance(obj, dict):
        return {k: _dcd(v) for k, v in obj.items()}
    return obj


def _hex(b: bytes | bytearray | None) -> str | None:
    return None if b is None else "0x" + bytes(b).hex()


_ZERO_HASH = "0x" + ("0" * 64)


def _fallback_roots() -> dict[str, str]:
    return {
        "stateRoot": _ZERO_HASH,
        "txsRoot": _ZERO_HASH,
        "receiptsRoot": _ZERO_HASH,
        "proofsRoot": _ZERO_HASH,
        "daRoot": _ZERO_HASH,
    }


def _fallback_block(chain_id: int) -> dict[str, t.Any]:
    return {
        "header": {
            "number": 0,
            "hash": _ZERO_HASH,
            "parentHash": _ZERO_HASH,
            "timestamp": 0,
            "chainId": chain_id,
            "thetaMicro": 0,
            "mixSeed": _ZERO_HASH,
            "nonce": _ZERO_HASH,
            "roots": _fallback_roots(),
        },
        "transactions": [],
        "receipts": [],
    }


def _b(hex_or_bytes: str | bytes | bytearray) -> bytes:
    if isinstance(hex_or_bytes, (bytes, bytearray)):
        return bytes(hex_or_bytes)
    s = hex_or_bytes.lower()
    if s.startswith("0x"):
        s = s[2:]
    return bytes.fromhex(s)


def _compute_header_hash(header: t.Any) -> str | None:
    try:
        if hasattr(header, "hash") and callable(getattr(header, "hash")):
            return _hex(bytes(header.hash()))
        if isinstance(header, dict) and header.get("hash") is not None:
            value = header.get("hash")
            if isinstance(value, (bytes, bytearray)):
                return _hex(value)
            if isinstance(value, str):
                return value
        if _header_sign_bytes is not None:
            data = _header_sign_bytes(header)
        elif _cbor_dumps is not None:
            data = _cbor_dumps(_dcd(header))
        else:  # pragma: no cover
            return None
        return _hex(_sha3_256(data))
    except Exception:
        return None


def _compute_tx_hash(tx: t.Any) -> str | None:
    """
    Compute transaction hash from a Tx object.
    
    Tries multiple methods in order:
    1. tx.hash() or tx.txid() method (preferred, pre-computed)
    2. Canonical sign bytes encoding
    3. CBOR encoding of full tx structure
    
    Returns None only if all methods fail.
    """
    # Try direct hash method first (most efficient)
    if hasattr(tx, "hash") and callable(getattr(tx, "hash")):
        try:
            h = tx.hash()
            if isinstance(h, bytes):
                return _hex(h)
            elif isinstance(h, str):
                return h if h.startswith("0x") else "0x" + h
        except Exception:
            pass
    
    # Try txid method (alias)
    if hasattr(tx, "txid") and callable(getattr(tx, "txid")):
        try:
            h = tx.txid()
            if isinstance(h, bytes):
                return _hex(h)
            elif isinstance(h, str):
                return h if h.startswith("0x") else "0x" + h
        except Exception:
            pass
    
    # Try canonical sign bytes encoding
    try:
        if _tx_sign_bytes is not None:
            data = _tx_sign_bytes(tx)
            return _hex(_sha3_256(data))
    except Exception:
        pass
    
    # Try CBOR encoding
    try:
        if _cbor_dumps is not None:
            data = _cbor_dumps(_dcd(tx))
            return _hex(_sha3_256(data))
    except Exception:
        pass
    
    # Final fallback: return None (caller should handle)
    return None


def _lookup_deploy_metadata_by_hash(tx_hash: str | None) -> dict | None:
    if not isinstance(tx_hash, str) or not tx_hash:
        return None
    raw = tx_hash[2:] if tx_hash.startswith("0x") else tx_hash
    try:
        tx_hash_b = bytes.fromhex(raw)
    except Exception:
        return None

    ctx = deps.get_ctx()
    bdb = getattr(ctx, "block_db", None)
    if bdb is None:
        return None
    getter = getattr(bdb, "get_deploy_metadata_by_tx_hash", None)
    if not callable(getter):
        return None
    try:
        out = getter(tx_hash_b)
    except Exception:
        return None
    return out if isinstance(out, dict) else None


def _tx_view(
    tx: t.Any,
    *,
    block_number: int | None = None,
    tx_index: int | None = None,
    block_hash: str | None = None,
) -> dict[str, t.Any]:
    """Project a Tx dataclass/object into a JSON-friendly view."""
    # Persisted txs are Tx dataclasses whose fields live on .unsigned (with
    # to/amount/data nested under .unsigned.payload). Unwrap before reading.
    inner = getattr(tx, "unsigned", None) or tx
    payload = getattr(inner, "payload", None)

    # Read common fields defensively
    _from = getattr(inner, "sender", getattr(inner, "frm", getattr(inner, "from_", None)))
    to = getattr(inner, "to", None)
    if to is None and payload is not None:
        to = getattr(payload, "to", None)
    valid_after = getattr(inner, "valid_after", getattr(inner, "validAfter", None))
    valid_until = getattr(inner, "valid_until", getattr(inner, "validUntil", None))
    salt = getattr(inner, "salt", None)
    fork_id = getattr(inner, "fork_id", getattr(inner, "forkId", None))
    gas = getattr(inner, "gas_limit", getattr(inner, "gas", None))
    tip = getattr(inner, "tip", getattr(inner, "gas_price", None))
    value = getattr(inner, "value", None)
    if value is None and payload is not None:
        value = getattr(payload, "amount", None)
    if value is None:
        value = 0
    kind = getattr(inner, "kind", getattr(inner, "tx_kind", None))
    access_list = getattr(tx, "access_list", None)
    data = getattr(inner, "data", None)
    if data is None and payload is not None:
        data = getattr(payload, "data", None)

    v = {
        "hash": _compute_tx_hash(tx),
        "from": _hex(_from) if isinstance(_from, (bytes, bytearray)) else _from,
        "to": _hex(to) if isinstance(to, (bytes, bytearray)) else to,
        "gas": int(gas) if gas is not None else None,
        "tip": int(tip) if tip is not None else None,
        "validAfter": int(valid_after) if valid_after is not None else None,
        "validUntil": int(valid_until) if valid_until is not None else None,
        "salt": _hex(salt) if isinstance(salt, (bytes, bytearray)) else salt,
        "forkId": int(fork_id) if fork_id is not None else None,
        "value": int(value) if value is not None else None,
        "kind": kind,
        "data": _hex(data) if isinstance(data, (bytes, bytearray)) else data,
        "accessList": access_list,
    }

    tx_hash = v.get("hash")
    deploy_meta = _lookup_deploy_metadata_by_hash(tx_hash if isinstance(tx_hash, str) else None)
    if deploy_meta is None and callable(_build_pyvm_deploy_metadata):
        try:
            deploy_meta = _build_pyvm_deploy_metadata(
                tx,
                tx_hash=tx_hash if isinstance(tx_hash, str) else None,
                block_hash=block_hash,
                block_number=block_number,
                tx_index=tx_index,
            )
        except Exception:
            deploy_meta = None
    if deploy_meta is not None and callable(_copy_deploy_metadata_fields):
        try:
            _copy_deploy_metadata_fields(v, deploy_meta, include_created_alias=True)
        except Exception:
            pass
    return {k: v for k, v in v.items() if v is not None}


def _log_view(log: t.Any) -> dict[str, t.Any]:
    addr = getattr(log, "address", None)
    topics = getattr(log, "topics", None)
    data = getattr(log, "data", None)
    return {
        "address": addr,
        "topics": [
            (_hex(t) if isinstance(t, (bytes, bytearray)) else t)
            for t in (topics or [])
        ],
        "data": _hex(data) if isinstance(data, (bytes, bytearray)) else data,
    }


def _receipt_view(r: t.Any) -> dict[str, t.Any]:
    status = getattr(r, "status", None)
    gas_used = getattr(r, "gas_used", getattr(r, "gasUsed", None))
    logs = getattr(r, "logs", [])
    contract_addr = getattr(r, "contract_address", getattr(r, "contractAddress", None))
    return {
        "status": int(status) if status is not None else None,
        "gasUsed": int(gas_used) if gas_used is not None else None,
        "contractAddress": contract_addr,
        "logs": [_log_view(l) for l in (logs or [])],
    }


def _header_roots_view(roots: t.Any) -> dict[str, t.Any] | None:
    if not roots:
        return None
    if isinstance(roots, dict):
        out: dict[str, t.Any] = {}
        for k, v in roots.items():
            out[k] = _hex(v) if isinstance(v, (bytes, bytearray)) else v
        return out
    return None


def _block_view(
    block: t.Any,
    height: int | None,
    *,
    include_txs: bool,
    include_receipts: bool,
    chain_id_fallback: int | None = None,
) -> dict[str, t.Any]:
    """Block view with toggles for tx objects & receipts."""
    header = (
        getattr(block, "header", None) or getattr(block, "Header", None) or block
    )  # tolerate shapes
    txs = getattr(block, "txs", getattr(block, "transactions", [])) or []
    receipts = getattr(block, "receipts", []) or []

    parent_hash = getattr(header, "parent_hash", getattr(header, "parentHash", None))
    timestamp = getattr(header, "timestamp", None)
    chain_id = getattr(header, "chain_id", getattr(header, "chainId", None))
    theta_micro = getattr(header, "theta_micro", getattr(header, "thetaMicro", None))
    mix_seed = getattr(header, "mix_seed", getattr(header, "mixSeed", None))
    nonce = getattr(header, "nonce", None)
    roots = getattr(header, "roots", None)

    if not roots:
        derived_roots = {
            "stateRoot": getattr(header, "stateRoot", None),
            "txsRoot": getattr(header, "txsRoot", None),
            "receiptsRoot": getattr(header, "receiptsRoot", None),
            "proofsRoot": getattr(header, "proofsRoot", None),
            "daRoot": getattr(header, "daRoot", None),
        }
        derived_roots = {k: v for k, v in derived_roots.items() if v is not None}
        if derived_roots:
            roots = {
                k: (_hex(v) if isinstance(v, (bytes, bytearray)) else v)
                for k, v in derived_roots.items()
            }

    if isinstance(header, dict):
        parent_hash = header.get("parentHash", parent_hash)
        timestamp = header.get("timestamp", timestamp)
        chain_id = header.get("chainId", chain_id)
        theta_micro = header.get("thetaMicro", theta_micro)
        mix_seed = header.get("mixSeed", mix_seed)
        nonce = header.get("nonce", nonce)
        roots = header.get("roots", roots)

    computed_hash = _compute_header_hash(header)
    if computed_hash is None:
        computed_hash = getattr(header, "hash", None)
        if computed_hash is None and isinstance(header, dict):
            computed_hash = header.get("hash")

    if chain_id is None and chain_id_fallback is not None:
        chain_id = chain_id_fallback

    timestamp_seconds = maybe_normalize_unix_timestamp_seconds(timestamp)

    v: dict[str, t.Any] = {
        "number": int(height) if height is not None else None,
        "hash": computed_hash,
        "parentHash": (
            _hex(parent_hash)
            if isinstance(parent_hash, (bytes, bytearray))
            else parent_hash
        ),
        "timestamp": int(timestamp_seconds) if timestamp_seconds is not None else None,
        "chainId": int(chain_id) if chain_id is not None else None,
        "thetaMicro": int(theta_micro) if theta_micro is not None else None,
        "mixSeed": (
            _hex(mix_seed) if isinstance(mix_seed, (bytes, bytearray)) else mix_seed
        ),
        "nonce": _hex(nonce) if isinstance(nonce, (bytes, bytearray)) else nonce,
        "roots": _header_roots_view(roots),
    }

    roots_view = v.get("roots")
    if not roots_view or any(vv is None for vv in roots_view.values()):
        roots_view = _fallback_roots()
        v["roots"] = roots_view

    if include_txs:
        tx_objects = [
            _tx_view(
                tx,
                block_number=height,
                tx_index=idx,
                block_hash=computed_hash if isinstance(computed_hash, str) else None,
            )
            for idx, tx in enumerate(txs)
        ]
        # Set both 'transactions' and 'txs' to same list (true alias, not copy)
        # This is intentional for efficiency - the dict is only used for JSON serialization
        v["transactions"] = tx_objects
        v["txs"] = tx_objects  # Alias for backward compatibility
    else:
        # Only hashes - filter out None values to avoid [null] in JSON
        tx_hashes = []
        for tx in txs:
            h = _compute_tx_hash(tx)
            if h is not None:
                tx_hashes.append(h)
            else:
                # Log warning but don't fail the request
                log.warning(f"Failed to compute hash for transaction in block; skipping from hash list")
        v["transactions"] = tx_hashes
        v["txs"] = tx_hashes  # Alias for backward compatibility

    if include_receipts:
        # Only include receipts field when explicitly requested (unlike transactions
        # which are always present, either as hashes or full objects)
        receipts_list = [_receipt_view(r) for r in receipts]
        v["receipts"] = receipts_list

    header_view = {
        "number": v.get("number"),
        "hash": v.get("hash"),
        "parentHash": v.get("parentHash"),
        "timestamp": v.get("timestamp"),
        "chainId": v.get("chainId"),
        "thetaMicro": v.get("thetaMicro"),
        "mixSeed": v.get("mixSeed"),
        "nonce": v.get("nonce"),
        "roots": v.get("roots"),
    }
    v["header"] = {k: val for k, val in header_view.items() if val is not None}

    # drop None keys
    return {k: val for k, val in v.items() if val is not None}


def _construct_pending_block(include_txs: bool = False, include_receipts: bool = False) -> dict:
    """
    Construct a synthetic 'pending' block with pending mempool transactions.
    
    This is used for eth_getBlockByNumber("pending", ...) queries to show
    what transactions are waiting to be mined.
    """
    try:
        # Get current head for parent info
        head = deps.get_head()
        head_height = int(head.get("height", 0)) if isinstance(head, dict) else 0
        head_hash = head.get("hash") if isinstance(head, dict) else None
        
        # Get pending transactions from mempool
        pending_txs: list[str] = []
        try:
            # Try to import mempool method
            from rpc.methods.mempool import mempool_get_pending
            pending_result = mempool_get_pending(verbose=False)
            if isinstance(pending_result, list):
                pending_txs = pending_result
        except Exception:
            # Mempool service not available, return empty pending block
            pass
        
        # Parse parent hash
        parent_hash_hex = "0x" + ("0" * 64)  # Default zero hash
        if head_hash:
            if isinstance(head_hash, str):
                parent_hash_hex = head_hash if head_hash.startswith("0x") else "0x" + head_hash
            elif isinstance(head_hash, bytes):
                parent_hash_hex = "0x" + head_hash.hex()
        
        # Get current timestamp
        try:
            ctx = deps.get_ctx()
            timestamp = int(ctx.get_time() if hasattr(ctx, "get_time") else 0)
        except Exception:
            import time
            timestamp = int(time.time())
        timestamp = (
            maybe_normalize_unix_timestamp_seconds(timestamp)
            if timestamp is not None
            else None
        )
        if timestamp is None:
            import time
            timestamp = int(time.time())
        
        # Build pending block structure
        pending_block = {
            "number": "0x" + format(head_height + 1, 'x'),  # Next block height
            "hash": None,  # Pending block has no hash yet
            "parentHash": parent_hash_hex,
            "timestamp": "0x" + format(timestamp, 'x'),
            "transactions": pending_txs if not include_txs else pending_txs,  # Full tx objects not yet supported
            "transactionsRoot": None,  # TBD when block is mined
            "stateRoot": None,  # TBD when block is mined
            "receiptsRoot": None,  # TBD when block is mined
            "miner": None,  # TBD when block is mined
            "difficulty": None,  # TBD when block is mined
            "totalDifficulty": None,
            "size": None,
            "gasLimit": "0x" + format(100_000_000_000, 'x'),  # Default gas limit
            "gasUsed": "0x0",  # No gas used yet
            "extraData": "0x",
            "nonce": None,
        }
        
        # Add receipts if requested (empty for pending)
        if include_receipts:
            pending_block["receipts"] = []
        
        return pending_block
        
    except Exception as e:
        log.warning(f"Failed to construct pending block: {e}")
        # Return minimal pending block on error
        return {
            "number": None,
            "hash": None,
            "transactions": [],
        }


def _normalize_block_number(n: t.Any) -> int | str:
    """
    Accepts: int, decimal string, hex string (0x…), or special keywords: 'latest'/'head'/'earliest'/'pending'
    Returns int for resolved block heights, or str 'pending' for pending block queries.
    """
    if isinstance(n, int):
        return n
    if isinstance(n, str):
        s = n.strip().lower()
        if s in (
            "latest",
            "head",
            "safe",
            "finalized",
        ):  # all map to current best for now
            head = deps.get_head()
            h = head.get("height") if isinstance(head, dict) else 0
            return int(h or 0)
        if s == "pending":
            # Return 'pending' as special marker for pending block construction
            return "pending"
        if s in ("earliest", "genesis"):
            return 0
        # hex?
        if s.startswith("0x"):
            return int(s, 16)
        return int(s)
    raise TypeError("block number must be int or string")


def _resolve_block_by_number(height: int) -> tuple[int | None, t.Any | None]:
    """
    Returns (height, block) if found, else (None, None).
    deps may expose different shapes; try a few.
    """
    # Try a direct call first
    if hasattr(deps, "get_block_by_height"):
        blk = deps.get_block_by_height(height)  # type: ignore
        if blk is None:
            return None, None
        return height, blk

    # Or via a state service
    if hasattr(deps, "state_service") and hasattr(
        deps.state_service, "get_block_by_height"
    ):
        blk = deps.state_service.get_block_by_height(height)  # type: ignore
        if blk is None:
            return None, None
        return height, blk

    # Fallback: try to walk from hash index if available
    if hasattr(deps, "get_block_hash_by_height") and hasattr(deps, "get_block_by_hash"):
        h = deps.get_block_hash_by_height(height)  # type: ignore
        if h is None:
            return None, None
        blk = deps.get_block_by_hash(h)  # type: ignore
        return height, blk

    return None, None


def _resolve_block_by_hash(h: str) -> tuple[int | None, t.Any | None]:
    """
    Returns (height, block) if found, else (None, None).
    """
    hb = _b(h)
    # Direct lookup
    if hasattr(deps, "get_block_by_hash"):
        blk = deps.get_block_by_hash(hb)  # type: ignore
        if blk is None:
            return None, None
        # Try to get height if present
        height = getattr(blk, "height", None)
        if height is None and hasattr(deps, "get_height_by_block_hash"):
            height = deps.get_height_by_block_hash(hb)  # type: ignore
        return (int(height) if height is not None else None), blk

    # Via state service
    if hasattr(deps, "state_service") and hasattr(
        deps.state_service, "get_block_by_hash"
    ):
        blk = deps.state_service.get_block_by_hash(hb)  # type: ignore
        if blk is None:
            return None, None
        height = getattr(blk, "height", None)
        if height is None and hasattr(deps.state_service, "get_height_by_block_hash"):
            height = deps.state_service.get_height_by_block_hash(hb)  # type: ignore
        return (int(height) if height is not None else None), blk

    return None, None


# -----------------------
# Methods
# -----------------------


@method(
    "chain.getBlockByNumber",
    desc="Get a block by number. Params: (number, includeTxObjects: bool=false, includeReceipts: bool=false)",
    aliases=("eth_getBlockByNumber", "block_getBlockByNumber"),
)
def chain_get_block_by_number(
    number: t.Union[int, str],
    includeTxObjects: bool = False,
    includeReceipts: bool = False,
    includeTx: bool | None = None,
) -> t.Optional[dict]:
    """
    number can be an int, hex string (0x…), decimal string, or 'latest'/'earliest'/'pending'.
    Returns a JSON object or null if not found.
    
    For 'pending', returns a synthetic block with pending transactions from mempool.
    """
    height = _normalize_block_number(number)
    
    # Handle pending block construction
    if height == "pending":
        return _construct_pending_block(
            include_txs=includeTx if includeTx is not None else includeTxObjects,
            include_receipts=includeReceipts,
        )
    
    h, blk = _resolve_block_by_number(height)
    if blk is None and height == 0:
        blk = _fallback_block(int(deps.get_chain_id()))
        h = 0
    if blk is None:
        return None
    include_txs = includeTx if includeTx is not None else includeTxObjects
    return _block_view(
        blk,
        h,
        include_txs=bool(include_txs),
        include_receipts=bool(includeReceipts),
        chain_id_fallback=int(deps.get_chain_id()),
    )


@method(
    "chain.getBlockByHash",
    desc="Get a block by hash. Params: (hash, includeTxObjects: bool=false, includeReceipts: bool=false)",
    aliases=("eth_getBlockByHash", "block_getBlockByHash"),
)
def chain_get_block_by_hash(
    blockHash: str | None = None,
    includeTxObjects: bool = False,
    includeReceipts: bool = False,
    includeTx: bool | None = None,
    hash: str | None = None,
) -> t.Optional[dict]:
    """
    blockHash must be a hex string (0x…).
    Returns a JSON object or null if not found.
    """
    bh = blockHash or hash
    if bh is None:
        raise ValueError("blockHash is required")

    chain_id_val = int(deps.get_chain_id())
    h, blk = _resolve_block_by_hash(bh)
    fallback_blk = _fallback_block(chain_id_val)
    fallback_view = _block_view(
        fallback_blk,
        0,
        include_txs=False,
        include_receipts=False,
        chain_id_fallback=chain_id_val,
    )
    fallback_hash = fallback_view.get("hash")
    if blk is None and bh.lower().strip() in {
        "0x",
        _ZERO_HASH.lower(),
        (fallback_hash or "").lower(),
    }:
        blk = fallback_blk
        h = 0 if h is None else h
    if blk is None:
        return None
    include_txs = includeTx if includeTx is not None else includeTxObjects
    return _block_view(
        blk,
        h,
        include_txs=bool(include_txs),
        include_receipts=bool(includeReceipts),
        chain_id_fallback=int(deps.get_chain_id()),
    )


@method(
    "debug.getRawBlock",
    desc="Return raw CBOR for a block/header. Params: (hash|height)",
    aliases=("debug_getRawBlock",),
)
def debug_get_raw_block(
    blockHash: str | None = None,
    height: int | str | None = None,
    hash: str | None = None,
) -> dict[str, t.Any]:
    bh = blockHash or hash
    blk = None
    h = None
    if bh is not None:
        h, blk = _resolve_block_by_hash(bh)
    elif height is not None:
        num = _normalize_block_number(height)
        h, blk = _resolve_block_by_number(num)
    else:
        raise ValueError("blockHash or height is required")
    if blk is None:
        raise ValueError("block not found")

    header = getattr(blk, "header", None)
    if header is None:
        raise ValueError("block missing header")

    if hasattr(blk, "to_cbor"):
        block_cbor = blk.to_cbor()
    elif _cbor_dumps is not None:
        block_cbor = _cbor_dumps(_dcd(blk))
    else:  # pragma: no cover
        raise ValueError("CBOR encoder unavailable")

    if hasattr(header, "to_cbor"):
        header_cbor = header.to_cbor()
    elif _cbor_dumps is not None:
        header_cbor = _cbor_dumps(_dcd(header))
    else:  # pragma: no cover
        raise ValueError("CBOR encoder unavailable")

    return {
        "height": int(h) if h is not None else None,
        "hash": _compute_header_hash(header),
        "blockCbor": _hex(block_cbor),
        "headerCbor": _hex(header_cbor),
    }
