"""gen_vectors.py - Test vector generator for sample_cbd module.

Generates vectors for CBD sampling with eta=2 and eta=3 using the
Python reference SHA_3.PRF for SHAKE-256 expander output, and a local
CBD implementation that outputs signed 12-bit coefficients.
"""

import os
import random
import sys

# Add test_framework/lib to path for VectorGenerator base class
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'lib'))

# Add Python reference path
_REF_PATH = os.path.expanduser(
    "~/Dev/server_code/python_project/PQC_2025/A_ML_KEM_v0")
sys.path.insert(0, _REF_PATH)

import SHA_3

from vector_gen import VectorGenerator


def _random_bits(length):
    """Generate a random binary string (LSB-first) of given length."""
    val = random.getrandbits(length)
    bits = ''
    for i in range(length):
        bits += '1' if (val & (1 << i)) else '0'
    return bits


def _bits_to_hex_msb(bits_str):
    """Convert binary string (LSB-first: bits_str[0] = bit 0) to MSB-first hex.

    Returns hex string where leftmost char = most significant nibble.
    Each nibble maps bits[3:0] to a hex char (bit 3 = MSB of nibble).
    """
    num_bits = len(bits_str)
    result = []
    for i in range(num_bits - 4, -1, -4):
        nib = 0
        for j in range(4):
            pos = i + j
            if pos < num_bits and bits_str[pos] == '1':
                nib |= (1 << j)
        result.append('0123456789ABCDEF'[nib])
    return ''.join(result)


def _sample_poly_cbd(prf_bits, eta):
    """Centered Binomial Distribution on PRF output bits.

    Reads eta*2 bits per coefficient from the binary string (LSB-first).
    Each coefficient = sum of first eta bits - sum of last eta bits.
    Result is a signed integer in range [-eta, eta].

    Args:
        prf_bits: Binary string (LSB-first) from SHAKE-256 PRF.
        eta: 2 or 3.

    Returns:
        list of 256 signed integers.
    """
    step = eta * 2  # 4 for eta=2, 6 for eta=3
    half = eta       # 2 for eta=2, 3 for eta=3
    coeffs = []
    for i in range(256):
        pos_sum = 0
        neg_sum = 0
        for j in range(half):
            pos_sum += (1 if prf_bits[step * i + j] == '1' else 0)
        for j in range(half):
            neg_sum += (1 if prf_bits[step * i + half + j] == '1' else 0)
        coeffs.append(pos_sum - neg_sum)
    return coeffs


def _coeff_to_hex_12signed(val):
    """Convert signed value (range [-eta, eta]) to 12-bit hex string.

    Negative values are represented as 12-bit two's complement.
    e.g., -2 → 0xFFE → "FFE", 3 → 0x003 → "003".
    """
    masked = val & 0xFFF  # 12-bit unsigned representation
    return f'{masked:03X}'


class SampleCbdVectorGenerator(VectorGenerator):
    """Generates test vectors for the sample_cbd_sync module."""

    def generate_one(self, params: dict) -> dict:
        """Generate a single test vector.

        Args:
            params: dict with 'eta' key (2 or 3).

        Returns:
            dict with 'input' and 'expected' keys.
        """
        eta = params.get('eta', 2)

        # Generate random seed (256 bits) and nonce (8 bits)
        seed_bits = _random_bits(256)   # sigma, LSB-first
        nonce_bits = _random_bits(8)     # N, LSB-first

        # SHAKE-256 PRF: sigma || N → eta*64 bytes
        prf_bits = SHA_3.PRF(seed_bits, nonce_bits, eta)

        # CBD sampling: output signed 12-bit coefficients
        coeffs = _sample_poly_cbd(prf_bits, eta)

        # Convert seed and nonce to MSB-first hex for RTL input
        seed_hex = _bits_to_hex_msb(seed_bits)
        nonce_hex = _bits_to_hex_msb(nonce_bits)

        return {
            'input': {
                'seed_hex': seed_hex,
                'nonce_hex': nonce_hex,
                'eta': eta
            },
            'expected': {
                'coeffs': coeffs
            }
        }

    def write_hex_file(self, vectors: list[dict], filepath: str) -> None:
        """Write input vectors as "SEED_HEX NONCE_HEX ETA" hex format.

        Each line: "64HEXCHARS 2HEXCHARS ETA_DECIMAL".

        Args:
            vectors: List of vector dicts from generate_one().
            filepath: Path to write the hex file.
        """
        os.makedirs(os.path.dirname(filepath), exist_ok=True)
        with open(filepath, 'w') as f:
            for v in vectors:
                inp = v['input']
                f.write(f'{inp["seed_hex"]} {inp["nonce_hex"]} {inp["eta"]}\n')

    def write_expected_file(self, vectors: list[dict], filepath: str) -> None:
        """Write expected coefficients as hex strings, one per line.

        Each vector produces 256 lines of 3-char hex (12-bit signed).

        Args:
            vectors: List of vector dicts from generate_one().
            filepath: Path to write the expected hex file.
        """
        os.makedirs(os.path.dirname(filepath), exist_ok=True)
        with open(filepath, 'w') as f:
            for v in vectors:
                coeffs = v['expected']['coeffs']
                for c in coeffs:
                    f.write(f'{_coeff_to_hex_12signed(c)}\n')

    def compare_results(self, got: list[str], expected_file: str) -> bool:
        """Compare RTL output against expected values.

        Args:
            got: List of hex result strings from simulation.
            expected_file: Path to expected hex file.

        Returns:
            bool: True if all results match.
        """
        with open(expected_file, 'r') as f:
            expected = [line.strip() for line in f
                        if line.strip() and not line.startswith('#')]

        if len(got) != len(expected):
            print(f"  COUNT MISMATCH: got={len(got)}, expected={len(expected)}")
            return False

        for i, (g, e) in enumerate(zip(got, expected)):
            if g.upper() != e.upper():
                if i < 10:  # Only show first 10 mismatches
                    print(f"  MISMATCH[{i}]: got={g.upper()}, expected={e.upper()}")
                return False

        return True
