# ML-DSA-65 KeyGen_internal — FIPS 204, Algorithm 6
# Pure Python port of btcq/src/crypto/mldsa_keygen.cpp
#
# Parameters: n=256, q=8380417, k=6, l=5, eta=4, d=13
#
# Produces byte-identical keys to the C reference, compatible with
# liboqs 0.12.0 and pqcrypto ML-DSA-65 sign/verify.

import hashlib

# ---------------------------------------------------------------------------
# Domain parameters (ML-DSA-65)
# ---------------------------------------------------------------------------
N = 256       # polynomial degree
Q = 8380417   # modulus
K = 6         # rows in A (public key dimension)
L = 5         # columns in A (secret key dimension)
ETA = 4       # secret coefficient bound
D = 13        # dropped bits for t0/t1 split

# Encoding sizes (bytes)
PK_BYTES = 1952
SK_BYTES = 4032
POLY_ETA_BYTES = 128   # N/2, eta=4: 4 bits per coeff
POLY_T1_BYTES = 320    # N*10/8
POLY_T0_BYTES = 416    # N*13/8

# Montgomery constants
# R = 2^32, QINV = Q^{-1} mod 2^32
QINV = 58728449
R_MOD_Q = 4193792  # 2^32 mod Q

MASK32 = 0xFFFFFFFF  # for 32-bit wrapping


# ---------------------------------------------------------------------------
# Montgomery arithmetic
# ---------------------------------------------------------------------------
def montgomery_reduce(a: int) -> int:
    """a * R^{-1} mod Q, result in (-Q, Q)."""
    # Lower 32 bits of a, treated as signed 32-bit
    a_lo = a & MASK32
    if a_lo >= (1 << 31):
        a_lo -= (1 << 32)
    # t = (int32_t)a * QINV, wrapped to 32 bits then sign-extended
    t = (a_lo * QINV) & MASK32
    if t >= (1 << 31):
        t -= (1 << 32)
    return (a - t * Q) >> 32


def mont_mul(a: int, b: int) -> int:
    """a * b * R^{-1} mod Q."""
    return montgomery_reduce(a * b)


def caddq(a: int) -> int:
    """Reduce to [0, Q-1]."""
    return a + (Q if a < 0 else 0)


def reduce32(a: int) -> int:
    """Reduce to (-Q/2, Q/2]."""
    t = (a + (1 << 22)) >> 23
    return a - t * Q


# ---------------------------------------------------------------------------
# NTT twiddle-factor table
# ---------------------------------------------------------------------------
def _bitrev8(x: int) -> int:
    x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4)
    x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2)
    x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1)
    return x & 0xFF


def _init_zetas() -> list:
    zetas = [0] * N
    for k in range(N):
        r = _bitrev8(k)
        zp = pow(1753, r, Q)
        zm = (zp * R_MOD_Q) % Q
        if zm > Q // 2:
            zm -= Q
        zetas[k] = zm
    return zetas


_ZETAS = _init_zetas()


# ---------------------------------------------------------------------------
# Forward NTT (Cooley-Tukey, in-place)
# ---------------------------------------------------------------------------
def _poly_ntt(a: list) -> None:
    k = 0
    length = 128
    while length >= 1:
        start = 0
        while start < N:
            k += 1
            zeta = _ZETAS[k]
            for j in range(start, start + length):
                t = mont_mul(zeta, a[j + length])
                a[j + length] = a[j] - t
                a[j] = a[j] + t
            start += 2 * length
        length >>= 1


# ---------------------------------------------------------------------------
# Inverse NTT (Gentleman-Sande, in-place) + n^{-1} scaling
# ---------------------------------------------------------------------------
F_INV = 41978  # combined INTT + n^{-1} Montgomery correction


def _poly_invntt(a: list) -> None:
    k = N
    length = 1
    while length < N:
        start = 0
        while start < N:
            k -= 1
            zeta = -_ZETAS[k]
            for j in range(start, start + length):
                t = a[j]
                a[j] = t + a[j + length]
                a[j + length] = mont_mul(zeta, t - a[j + length])
            start += 2 * length
        length <<= 1
    for j in range(N):
        a[j] = montgomery_reduce(F_INV * a[j])


# ---------------------------------------------------------------------------
# Polynomial / vector operations
# ---------------------------------------------------------------------------
def _poly_add(a: list, b: list) -> list:
    return [a[i] + b[i] for i in range(N)]


def _poly_caddq(a: list) -> list:
    return [caddq(x) for x in a]


def _poly_pointwise(a: list, b: list) -> list:
    return [mont_mul(a[i], b[i]) for i in range(N)]


def _matrix_mult(A, s1hat):
    """r = A * s1hat in NTT domain. A is K x L, s1hat is L polys."""
    result = []
    for i in range(K):
        poly = [0] * N
        for c in range(N):
            s = 0
            for j in range(L):
                s += mont_mul(A[i][j][c], s1hat[j][c])
            poly[c] = reduce32(s)
        result.append(poly)
    return result


# ---------------------------------------------------------------------------
# ExpandA -- FIPS 204 Algorithm 32 (SHAKE128)
# ---------------------------------------------------------------------------
def _poly_uniform(rho: bytes, j: int, i: int) -> list:
    """Rejection-sample 256 coefficients in [0, Q) from SHAKE128(rho || j || i)."""
    h = hashlib.shake_128()
    h.update(rho)
    h.update(bytes([j, i]))
    # 840 bytes = 280 candidates; ~0.098% rejection rate
    # Use a generous buffer; if we need more we re-squeeze larger
    buf = h.digest(840)

    coeffs = []
    pos = 0
    while len(coeffs) < N:
        if pos + 3 > len(buf):
            # Need more bytes - get a larger chunk from scratch
            buf = h.digest(len(buf) + 840)
            # pos is still valid since digest() always starts from beginning
        val = buf[pos] | (buf[pos + 1] << 8) | (buf[pos + 2] << 16)
        val &= 0x7FFFFF  # 23 bits
        pos += 3
        if val < Q:
            coeffs.append(val)
    return coeffs


def _expand_A(rho: bytes):
    """Generate K x L matrix A in NTT domain."""
    A = []
    for i in range(K):
        row = []
        for j in range(L):
            row.append(_poly_uniform(rho, j, i))
        A.append(row)
    return A


# ---------------------------------------------------------------------------
# ExpandS -- FIPS 204 Algorithm 33 (SHAKE256)
# ---------------------------------------------------------------------------
def _poly_sample_eta4(rhoprime: bytes, nonce: int) -> list:
    """Sample 256 coefficients in [-4, 4] from SHAKE256(rhoprime || nonce_le16)."""
    h = hashlib.shake_256()
    h.update(rhoprime)
    h.update(bytes([nonce & 0xFF, (nonce >> 8) & 0xFF]))
    buf = h.digest(272)  # 136 * 2, generous for eta=4

    coeffs = []
    pos = 0
    while len(coeffs) < N:
        if pos >= len(buf):
            buf = h.digest(len(buf) + 136)
        b = buf[pos]
        pos += 1
        lo = b & 0x0F
        hi = b >> 4
        if lo < 9 and len(coeffs) < N:
            coeffs.append(ETA - lo)
        if hi < 9 and len(coeffs) < N:
            coeffs.append(ETA - hi)
    return coeffs


def _expand_S(rhoprime: bytes):
    """Sample secret vectors s1 (L polys) and s2 (K polys)."""
    s1 = [_poly_sample_eta4(rhoprime, i) for i in range(L)]
    s2 = [_poly_sample_eta4(rhoprime, L + i) for i in range(K)]
    return s1, s2


# ---------------------------------------------------------------------------
# Power2Round -- FIPS 204 Algorithm 31
# ---------------------------------------------------------------------------
def _poly_power2round(t: list):
    """Split t into (t1, t0) where t = t1 * 2^D + t0."""
    t1 = [0] * N
    t0 = [0] * N
    for i in range(N):
        r = t[i]
        t1[i] = (r + (1 << (D - 1)) - 1) >> D
        t0[i] = r - (t1[i] << D)
    return t1, t0


# ---------------------------------------------------------------------------
# Packing
# ---------------------------------------------------------------------------
def _pack_t1(a: list) -> bytes:
    """Pack t1: 10 bits per coefficient, 4 coeffs per 5 bytes."""
    r = bytearray(POLY_T1_BYTES)
    for i in range(N // 4):
        c0, c1, c2, c3 = a[4*i], a[4*i+1], a[4*i+2], a[4*i+3]
        r[5*i+0] = c0 & 0xFF
        r[5*i+1] = ((c0 >> 8) | (c1 << 2)) & 0xFF
        r[5*i+2] = ((c1 >> 6) | (c2 << 4)) & 0xFF
        r[5*i+3] = ((c2 >> 4) | (c3 << 6)) & 0xFF
        r[5*i+4] = (c3 >> 2) & 0xFF
    return bytes(r)


def _pack_s4(a: list) -> bytes:
    """Pack s poly (eta=4): 4 bits per coefficient, stored as eta - coeff."""
    r = bytearray(POLY_ETA_BYTES)
    for i in range(N // 2):
        r[i] = ((ETA - a[2*i]) | ((ETA - a[2*i+1]) << 4)) & 0xFF
    return bytes(r)


def _pack_t0(a: list) -> bytes:
    """Pack t0: 13 bits per coefficient, 8 coeffs per 13 bytes.
    Stored as 2^{d-1} - t0 (biased positive)."""
    r = bytearray(POLY_T0_BYTES)
    bias = 1 << (D - 1)  # 4096
    p = 0
    for i in range(N // 8):
        t = [(bias - a[8*i+j]) for j in range(8)]
        r[p+0]  = t[0] & 0xFF
        r[p+1]  = ((t[0] >> 8) | (t[1] << 5)) & 0xFF
        r[p+2]  = (t[1] >> 3) & 0xFF
        r[p+3]  = ((t[1] >> 11) | (t[2] << 2)) & 0xFF
        r[p+4]  = ((t[2] >> 6) | (t[3] << 7)) & 0xFF
        r[p+5]  = (t[3] >> 1) & 0xFF
        r[p+6]  = ((t[3] >> 9) | (t[4] << 4)) & 0xFF
        r[p+7]  = (t[4] >> 4) & 0xFF
        r[p+8]  = ((t[4] >> 12) | (t[5] << 1)) & 0xFF
        r[p+9]  = ((t[5] >> 7) | (t[6] << 6)) & 0xFF
        r[p+10] = (t[6] >> 2) & 0xFF
        r[p+11] = ((t[6] >> 10) | (t[7] << 3)) & 0xFF
        r[p+12] = (t[7] >> 5) & 0xFF
        p += 13
    return bytes(r)


# ---------------------------------------------------------------------------
# MLDSA65_KeyGen -- FIPS 204, Algorithm 6 (ML-DSA.KeyGen_internal)
# ---------------------------------------------------------------------------
def mldsa_keygen_from_seed(xi: bytes) -> tuple:
    """Generate ML-DSA-65 keypair from 32-byte seed xi.

    Returns (pk, sk) where pk is 1952 bytes and sk is 4032 bytes,
    byte-identical to the C reference implementation and compatible
    with pqcrypto/liboqs ML-DSA-65 sign/verify.
    """
    if len(xi) != 32:
        raise ValueError(f"Seed must be 32 bytes, got {len(xi)}")

    # Step 1: Expand seed -> (rho, rhoprime, K_seed) via SHAKE256
    h = hashlib.shake_256()
    h.update(xi)
    h.update(bytes([K, L]))  # k=6, l=5
    seed_out = h.digest(128)
    rho = seed_out[0:32]        # public matrix seed
    rhoprime = seed_out[32:96]  # secret key seed (64 bytes)
    K_seed = seed_out[96:128]   # signing randomness seed

    # Step 2: Generate public matrix A in NTT domain
    A = _expand_A(rho)

    # Step 3: Sample secret vectors
    s1, s2 = _expand_S(rhoprime)

    # Step 4: Compute t = INTT(A * NTT(s1)) + s2
    # Copy s1 for NTT (don't modify original -- needed for sk packing)
    s1hat = [list(p) for p in s1]
    for p in s1hat:
        _poly_ntt(p)

    t = _matrix_mult(A, s1hat)
    for p in t:
        _poly_invntt(p)

    # t = t + s2
    t = [_poly_add(t[i], s2[i]) for i in range(K)]
    # caddq to get coefficients in [0, Q-1]
    t = [_poly_caddq(p) for p in t]

    # Step 5: Power2Round split
    t1_list = []
    t0_list = []
    for i in range(K):
        t1_i, t0_i = _poly_power2round(t[i])
        t1_list.append(t1_i)
        t0_list.append(t0_i)

    # Step 6: Encode public key: pk = rho || packed_t1
    pk = bytearray(PK_BYTES)
    pk[0:32] = rho
    for i in range(K):
        offset = 32 + i * POLY_T1_BYTES
        pk[offset:offset + POLY_T1_BYTES] = _pack_t1(t1_list[i])
    pk = bytes(pk)

    # Step 7: tr = SHAKE256(pk, 64 bytes)
    h2 = hashlib.shake_256()
    h2.update(pk)
    tr = h2.digest(64)

    # Step 8: Encode secret key: sk = rho || K_seed || tr || s1 || s2 || t0
    sk = bytearray(SK_BYTES)
    p = 0
    sk[p:p+32] = rho;      p += 32
    sk[p:p+32] = K_seed;   p += 32
    sk[p:p+64] = tr;       p += 64
    for i in range(L):
        sk[p:p+POLY_ETA_BYTES] = _pack_s4(s1[i]); p += POLY_ETA_BYTES
    for i in range(K):
        sk[p:p+POLY_ETA_BYTES] = _pack_s4(s2[i]); p += POLY_ETA_BYTES
    for i in range(K):
        sk[p:p+POLY_T0_BYTES] = _pack_t0(t0_list[i]); p += POLY_T0_BYTES

    assert p == SK_BYTES
    sk = bytes(sk)

    return pk, sk
