"""ECDSA key-pair generation, signing, and verification helpers.

Async shims:
  ecdsa_sign_async(signing_key, msg, **kwargs)
  ecdsa_verify_async(verifying_key, sig, msg, **kwargs)
  sha256_hex_async(data)

ECDSA secp256k1 sign/verify takes ~2-10ms per call -- enough to stall
the event loop on hot paths (per-request namebump verify, per-message
sidewire AppPacket sign/verify).  These wrappers run the crypto on
the default thread pool executor so async callers can `await` and
keep the loop free.  Same return shape (and exception behaviour) as
the direct sync calls.

A SHA256 helper lives here too because sidewire's app_proto hot-path
hashes every inbound message before dispatch and was previously
running on the loop thread.
"""
import asyncio
import hashlib

from ecdsa import SECP256k1, SigningKey, util
from .utils import to_hs


class Signing:
    """ECDSA key-pair wrapper that exposes compact public-key bytes and hex."""

    def __init__(
        self,
        priv=None,
        pub=None,
        curve=SECP256k1,
    ):
        """Wrap an existing private/public key pair on the given curve."""
        self.curve = curve
        self.private_key = priv
        self.public_key = pub or priv.get_verifying_key()
        self.compact_public_key = self.public_key.to_string("compressed")
        self.public_key_hex = to_hs(self.compact_public_key)

    @staticmethod
    def keypair(curve=SECP256k1):
        """Generate a fresh key-pair on curve and return a Signing instance."""
        priv = SigningKey.generate(curve=curve)
        pub = priv.get_verifying_key()
        return Signing(priv, pub, curve)


def ecdsa_sign_sync(signing_key, msg, **kwargs):
    """Wrap signing_key.sign(msg, **kwargs); top-level so executors that
    require picklable targets (some Windows / pypy configurations) have
    a stable function reference.
    """
    return signing_key.sign(msg, **kwargs)


def ecdsa_verify_sync(verifying_key, sig, msg, **kwargs):
    """Wrap verifying_key.verify(sig, msg, **kwargs); see note on
    ecdsa_sign_sync re: picklable executor targets.  Propagates
    BadSignatureError unchanged.
    """
    return verifying_key.verify(sig, msg, **kwargs)


async def ecdsa_sign_async(signing_key, msg, **kwargs):
    """Default-executor-offloaded ECDSA sign.

    Same signature and return value as signing_key.sign().  Use on
    hot-path async code (per-request, per-message) so the loop isn't
    blocked for the ~2-10ms each sign takes.
    """
    loop = asyncio.get_event_loop()
    if kwargs:
        return await loop.run_in_executor(
            None, lambda: ecdsa_sign_sync(signing_key, msg, **kwargs),
        )
    return await loop.run_in_executor(
        None, ecdsa_sign_sync, signing_key, msg,
    )


async def ecdsa_verify_async(verifying_key, sig, msg, **kwargs):
    """Default-executor-offloaded ECDSA verify.

    Same signature, return value, and exception behaviour as
    verifying_key.verify() -- BadSignatureError on a bad signature
    propagates through the awaited future.  Use on hot-path async
    handlers (per-request server verify, per-message client verify).
    """
    loop = asyncio.get_event_loop()
    if kwargs:
        return await loop.run_in_executor(
            None, lambda: ecdsa_verify_sync(verifying_key, sig, msg, **kwargs),
        )
    return await loop.run_in_executor(
        None, ecdsa_verify_sync, verifying_key, sig, msg,
    )


def sha256_hex_sync(data):
    """Picklable executor target for SHA256 hex digest of `data`."""
    return hashlib.sha256(data).hexdigest()


async def sha256_hex_async(data):
    """Default-executor-offloaded SHA256 hex digest.

    Use on hot-path async code where the digest input is large or
    arrives at high rate (sidewire's app_proto.process_app_msg hashes
    every inbound MQTT payload for dedup).  Returns the hex-string
    digest; same as hashlib.sha256(data).hexdigest().
    """
    loop = asyncio.get_event_loop()
    return await loop.run_in_executor(None, sha256_hex_sync, data)


if __name__ == "__main__":
    kp = Signing.keypair()

    msg = b"my test msg."
    sig = kp.private_key.sign(msg, sigencode=util.sigencode_string)
