"""Public-only BIP32 and witness-v0 address math for frozen regtest fixtures.

Python standard library. No private keys, seed, wallet file or network input.
Educational verifier; not an audited wallet or production cryptographic library.
"""
import hashlib
import hmac

P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
     0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
XPUB = bytes.fromhex("0488b21e")
TPUB = bytes.fromhex("043587cf")
ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
BECH32 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
FIXTURE_PARENT = "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"
FIXTURE_CHILD_ONE = "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ"


def double_sha(value):
    return hashlib.sha256(hashlib.sha256(value).digest()).digest()


def hash160(value):
    return hashlib.new("ripemd160", hashlib.sha256(value).digest()).digest()


def base58_encode(value):
    number = int.from_bytes(value, "big")
    text = ""
    while number:
        number, remainder = divmod(number, 58)
        text = ALPHABET[remainder] + text
    return "1" * (len(value) - len(value.lstrip(b"\0"))) + text


def base58_decode(text):
    number = 0
    for char in text:
        number = number * 58 + ALPHABET.index(char)
    value = number.to_bytes((number.bit_length() + 7) // 8, "big")
    return b"\0" * (len(text) - len(text.lstrip("1"))) + value


def add(a, b):
    if a is None:
        return b
    if b is None:
        return a
    if a[0] == b[0] and (a[1] + b[1]) % P == 0:
        return None
    slope = (3 * a[0] * a[0] * pow(2 * a[1], -1, P)) % P if a == b else ((b[1] - a[1]) * pow(b[0] - a[0], -1, P)) % P
    x = (slope * slope - a[0] - b[0]) % P
    return x, (slope * (a[0] - x) - a[1]) % P


def multiply(number, point):
    result = None
    while number:
        if number & 1:
            result = add(result, point)
        point = add(point, point)
        number >>= 1
    return result


def parse_public(encoded):
    if len(encoded) != 33 or encoded[0] not in (2, 3):
        raise ValueError("Only compressed public keys are allowed")
    x = int.from_bytes(encoded[1:], "big")
    if x >= P:
        raise ValueError("Public point x outside field")
    square = (x * x * x + 7) % P
    y = pow(square, (P + 1) // 4, P)
    if y * y % P != square:
        raise ValueError("Invalid public curve point")
    return x, y if y & 1 == encoded[0] & 1 else P - y


def serialize_public(point):
    return bytes([2 + (point[1] & 1)]) + point[0].to_bytes(32, "big")


def decode_extended(text):
    raw = base58_decode(text)
    if len(raw) != 82 or double_sha(raw[:-4])[:4] != raw[-4:]:
        raise ValueError("Extended public key length/checksum invalid")
    payload = raw[:-4]
    if payload[:4] not in (XPUB, TPUB):
        raise ValueError("Only xpub/tpub public versions are allowed")
    parse_public(payload[45:78])
    return payload


def encode_extended(payload):
    if len(payload) != 78 or payload[:4] not in (XPUB, TPUB):
        raise ValueError("Not a public78-byte extended key payload")
    parse_public(payload[45:78])
    return base58_encode(payload + double_sha(payload)[:4])


def as_tpub(text):
    return encode_extended(TPUB + decode_extended(text)[4:])


def child(text, index):
    if type(index) is not int or not 0 <= index < 0x80000000:
        raise ValueError("Public derivation permits only non-hardened indexes")
    parent = decode_extended(text)
    if parent[4] == 255:
        raise ValueError("Depth limit")
    data = parent[45:78] + index.to_bytes(4, "big")
    digest = hmac.new(parent[13:45], data, hashlib.sha512).digest()
    tweak = int.from_bytes(digest[:32], "big")
    if tweak >= N:
        raise ValueError("Invalid child; fixture does not advance index silently")
    key = add(multiply(tweak, G), parse_public(parent[45:78]))
    if key is None:
        raise ValueError("Invalid child point")
    payload = parent[:4] + bytes([parent[4] + 1]) + hash160(parent[45:78])[:4] + index.to_bytes(4, "big") + digest[32:] + serialize_public(key)
    return encode_extended(payload)


def derive(text, indexes):
    for index in indexes:
        text = child(text, index)
    return text


def polymod(values):
    generators = (0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
    checksum = 1
    for value in values:
        top = checksum >> 25
        checksum = ((checksum & 0x1ffffff) << 5) ^ value
        for bit, generator in enumerate(generators):
            if (top >> bit) & 1:
                checksum ^= generator
    return checksum


def witness_address(public_key, hrp="rltc"):
    program = hash160(public_key)
    accumulator = bits = 0
    groups = []
    for byte in program:
        accumulator = (accumulator << 8) | byte
        bits += 8
        while bits >= 5:
            bits -= 5
            groups.append((accumulator >> bits) & 31)
    if bits:
        groups.append((accumulator << (5 - bits)) & 31)
    values = [0] + groups
    expansion = [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp]
    checksum = polymod(expansion + values + [0] * 6) ^ 1
    return hrp + "1" + "".join(BECH32[v] for v in values + [(checksum >> (5 * (5 - index))) & 31 for index in range(6)])


def legacy_address(public_key):
    payload = bytes([111]) + hash160(public_key)
    return base58_encode(payload + double_sha(payload)[:4])


def address(text, indexes, script="wpkh"):
    public = decode_extended(derive(text, indexes))[45:78]
    if script == "wpkh":
        return witness_address(public)
    if script == "pkh":
        return legacy_address(public)
    raise ValueError("Unsupported fixture script")
