"""Nostr keypair generation and encoding (NIP-19 / BIP-340).

Provides pure-Python secp256k1 keypair generation and bech32 nsec/npub
encoding with no external dependencies beyond the Python stdlib.

Usage
-----
    from infrastructure.nostr.keypair import generate_keypair, load_keypair

    kp = generate_keypair()
    print(kp.npub)   # npub1…
    print(kp.nsec)   # nsec1…

    kp2 = load_keypair(privkey_hex="deadbeef...")
"""

from __future__ import annotations

import hashlib
import secrets
from dataclasses import dataclass

# ── secp256k1 curve parameters (BIP-340) ──────────────────────────────────────

_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
_GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
_GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
_G = (_GX, _GY)

Point = tuple[int, int] | None  # None represents the point at infinity


def _point_add(P: Point, Q: Point) -> Point:
    if P is None:
        return Q
    if Q is None:
        return P
    px, py = P
    qx, qy = Q
    if px == qx:
        if py != qy:
            return None
        # Point doubling
        lam = (3 * px * px * pow(2 * py, _P - 2, _P)) % _P
    else:
        lam = ((qy - py) * pow(qx - px, _P - 2, _P)) % _P
    rx = (lam * lam - px - qx) % _P
    ry = (lam * (px - rx) - py) % _P
    return rx, ry


def _point_mul(P: Point, n: int) -> Point:
    """Scalar multiplication via double-and-add."""
    R: Point = None
    while n > 0:
        if n & 1:
            R = _point_add(R, P)
        P = _point_add(P, P)
        n >>= 1
    return R


def _has_even_y(P: Point) -> bool:
    assert P is not None
    return P[1] % 2 == 0


def _x_bytes(P: Point) -> bytes:
    """Return the 32-byte x-coordinate of a point (x-only pubkey)."""
    assert P is not None
    return P[0].to_bytes(32, "big")


def _privkey_to_pubkey_bytes(privkey_int: int) -> bytes:
    """Derive the x-only public key from an integer private key."""
    P = _point_mul(_G, privkey_int)
    return _x_bytes(P)


# ── bech32 encoding (NIP-19 uses original bech32, not bech32m) ────────────────

_BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"


def _bech32_polymod(values: list[int]) -> int:
    GEN = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
    chk = 1
    for v in values:
        b = chk >> 25
        chk = (chk & 0x1FFFFFF) << 5 ^ v
        for i in range(5):
            chk ^= GEN[i] if ((b >> i) & 1) else 0
    return chk


def _bech32_hrp_expand(hrp: str) -> list[int]:
    return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]


def _convertbits(data: bytes, frombits: int, tobits: int, pad: bool = True) -> list[int]:
    acc = 0
    bits = 0
    ret: list[int] = []
    maxv = (1 << tobits) - 1
    for value in data:
        acc = ((acc << frombits) | value) & 0xFFFFFF
        bits += frombits
        while bits >= tobits:
            bits -= tobits
            ret.append((acc >> bits) & maxv)
    if pad and bits:
        ret.append((acc << (tobits - bits)) & maxv)
    elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
        raise ValueError("Invalid padding")
    return ret


def _bech32_encode(hrp: str, data: bytes) -> str:
    """Encode bytes as a bech32 string with the given HRP."""
    converted = _convertbits(data, 8, 5)
    combined = _bech32_hrp_expand(hrp) + converted
    checksum_input = combined + [0, 0, 0, 0, 0, 0]
    polymod = _bech32_polymod(checksum_input) ^ 1
    checksum = [(polymod >> (5 * (5 - i))) & 31 for i in range(6)]
    return hrp + "1" + "".join(_BECH32_CHARSET[d] for d in converted + checksum)


def _bech32_decode(bech32_str: str) -> tuple[str, bytes]:
    """Decode a bech32 string to (hrp, data_bytes).

    Raises ValueError on invalid encoding.
    """
    bech32_str = bech32_str.lower()
    sep = bech32_str.rfind("1")
    if sep < 1 or sep + 7 > len(bech32_str):
        raise ValueError(f"Invalid bech32: {bech32_str!r}")
    hrp = bech32_str[:sep]
    data_chars = bech32_str[sep + 1 :]
    data = []
    for c in data_chars:
        pos = _BECH32_CHARSET.find(c)
        if pos == -1:
            raise ValueError(f"Invalid bech32 character: {c!r}")
        data.append(pos)
    if _bech32_polymod(_bech32_hrp_expand(hrp) + data) != 1:
        raise ValueError("Invalid bech32 checksum")
    decoded = _convertbits(bytes(data[:-6]), 5, 8, pad=False)
    return hrp, bytes(decoded)


# ── NostrKeypair ──────────────────────────────────────────────────────────────


@dataclass(frozen=True)
class NostrKeypair:
    """A Nostr keypair with both hex and bech32 representations.

    Attributes
    ----------
    privkey_hex : str
        32-byte private key as lowercase hex (64 chars). Treat as a secret.
    pubkey_hex : str
        32-byte x-only public key as lowercase hex (64 chars).
    nsec : str
        Private key encoded as NIP-19 ``nsec1…`` bech32 string.
    npub : str
        Public key encoded as NIP-19 ``npub1…`` bech32 string.
    """

    privkey_hex: str
    pubkey_hex: str
    nsec: str
    npub: str

    @property
    def privkey_bytes(self) -> bytes:
        return bytes.fromhex(self.privkey_hex)

    @property
    def pubkey_bytes(self) -> bytes:
        return bytes.fromhex(self.pubkey_hex)


def generate_keypair() -> NostrKeypair:
    """Generate a fresh Nostr keypair from a cryptographically random seed.

    Returns
    -------
    NostrKeypair
        The newly generated keypair.
    """
    while True:
        raw = secrets.token_bytes(32)
        d = int.from_bytes(raw, "big")
        if 1 <= d < _N:
            break

    pub_bytes = _privkey_to_pubkey_bytes(d)
    privkey_hex = raw.hex()
    pubkey_hex = pub_bytes.hex()
    nsec = _bech32_encode("nsec", raw)
    npub = _bech32_encode("npub", pub_bytes)
    return NostrKeypair(privkey_hex=privkey_hex, pubkey_hex=pubkey_hex, nsec=nsec, npub=npub)


def load_keypair(
    *,
    privkey_hex: str | None = None,
    nsec: str | None = None,
) -> NostrKeypair:
    """Load a keypair from a hex private key or an nsec bech32 string.

    Parameters
    ----------
    privkey_hex:
        64-char lowercase hex private key.
    nsec:
        NIP-19 ``nsec1…`` bech32 string.

    Raises
    ------
    ValueError
        If neither or both parameters are supplied, or if the key is invalid.
    """
    if privkey_hex and nsec:
        raise ValueError("Supply either privkey_hex or nsec, not both")
    if not privkey_hex and not nsec:
        raise ValueError("Supply either privkey_hex or nsec")

    if nsec:
        hrp, raw = _bech32_decode(nsec)
        if hrp != "nsec":
            raise ValueError(f"Expected nsec bech32, got {hrp!r}")
        privkey_hex = raw.hex()

    assert privkey_hex is not None
    raw_bytes = bytes.fromhex(privkey_hex)
    if len(raw_bytes) != 32:
        raise ValueError(f"Private key must be 32 bytes, got {len(raw_bytes)}")

    d = int.from_bytes(raw_bytes, "big")
    if not (1 <= d < _N):
        raise ValueError("Private key out of range")

    pub_bytes = _privkey_to_pubkey_bytes(d)
    pubkey_hex = pub_bytes.hex()
    nsec_enc = _bech32_encode("nsec", raw_bytes)
    npub = _bech32_encode("npub", pub_bytes)
    return NostrKeypair(privkey_hex=privkey_hex, pubkey_hex=pubkey_hex, nsec=nsec_enc, npub=npub)


def pubkey_from_privkey(privkey_hex: str) -> str:
    """Derive the hex public key from a hex private key.

    Parameters
    ----------
    privkey_hex:
        64-char lowercase hex private key.

    Returns
    -------
    str
        64-char lowercase hex x-only public key.
    """
    return load_keypair(privkey_hex=privkey_hex).pubkey_hex


def _sha256(data: bytes) -> bytes:
    return hashlib.sha256(data).digest()
