"""Bounded public-fixture transaction/PSBT codec, not a wallet or consensus parser."""
import base64
import hashlib

def sha256d(data):return hashlib.sha256(hashlib.sha256(data).digest()).digest()
def compact(n):
    if n<253:return bytes([n])
    for tag,size,limit in [(253,2,65535),(254,4,4294967295),(255,8,18446744073709551615)]:
        if n<=limit:return bytes([tag])+n.to_bytes(size,'little')
    raise ValueError('Oversized length')
class Reader:
    def __init__(self,data):
        if len(data)>1000000:raise ValueError('Fixture byte budget')
        self.data=data;self.at=0
    def take(self,n):
        if n<0 or self.at+n>len(self.data):raise ValueError('Truncated fixture')
        out=self.data[self.at:self.at+n];self.at+=n;return out
    def size(self):
        tag=self.take(1)[0]
        if tag<253:return tag
        n=int.from_bytes(self.take({253:2,254:4,255:8}[tag]),'little')
        if n<{253:253,254:65536,255:4294967296}[tag] or n>1000000:raise ValueError('Length encoding/budget')
        return n
    def variable(self):return self.take(self.size())
def transaction(raw):
    r=Reader(bytes.fromhex(raw) if isinstance(raw,str) else raw)
    version=r.take(4);witness=False
    if r.data[r.at:r.at+1]==b'\x00':
        if r.take(2)!=b'\x00\x01':raise ValueError('Only ordinary transparent witnesses supported')
        witness=True
    n=r.size()
    if not 1<=n<=100:raise ValueError('Input budget')
    ins=[]
    for _ in range(n):ins.append(dict(outpoint=r.take(36),script=r.variable(),sequence=r.take(4),witness=[]))
    n=r.size()
    if not 1<=n<=100:raise ValueError('Output budget')
    outs=[dict(atoms=int.from_bytes(r.take(8),'little'),script=r.variable()) for _ in range(n)]
    if witness:
        for row in ins:
            count=r.size()
            if count>100:raise ValueError('Witness budget')
            row['witness']=[r.variable() for _ in range(count)]
    locktime=r.take(4)
    if r.at!=len(r.data):raise ValueError('Unexpected transaction extension')
    return dict(version=version,inputs=ins,outputs=outs,locktime=locktime)
def output_bytes(row):return row['atoms'].to_bytes(8,'little')+compact(len(row['script']))+row['script']
def serialize(tx,with_witness=True):
    witness=with_witness and any(x['witness'] for x in tx['inputs'])
    result=tx['version']+(b'\x00\x01' if witness else b'')+compact(len(tx['inputs']))
    result+=b''.join(x['outpoint']+compact(len(x['script']))+x['script']+x['sequence'] for x in tx['inputs'])
    result+=compact(len(tx['outputs']))+b''.join(output_bytes(x) for x in tx['outputs'])
    if witness:result+=b''.join(compact(len(x['witness']))+b''.join(compact(len(v))+v for v in x['witness']) for x in tx['inputs'])
    return result+tx['locktime']
def details(tx):
    raw=serialize(tx);stripped=serialize(tx,False);weight=len(stripped)*3+len(raw)
    return dict(txid=sha256d(stripped)[::-1].hex(),wtxid=sha256d(raw)[::-1].hex(),size=len(raw),weight=weight,vsize=(weight+3)//4)
def read_map(r):
    out={}
    while True:
        key=r.variable()
        if not key:return out
        if key in out:raise ValueError('Duplicate PSBT key')
        out[key]=r.variable()
        if len(out)>1000:raise ValueError('PSBT map budget')
def psbt(value):
    raw=base64.b64decode(value,validate=True);r=Reader(raw)
    if r.take(5)!=b'psbt\xff':raise ValueError('PSBT magic')
    glob=read_map(r)
    if b'\x00' not in glob:raise ValueError('Missing unsigned transaction')
    tx=transaction(glob[b'\x00'])
    if any(x['script'] or x['witness'] for x in tx['inputs']):raise ValueError('Expected unsigned PSBT transaction')
    ins=[read_map(r) for _ in tx['inputs']];outs=[read_map(r) for _ in tx['outputs']]
    if r.at!=len(raw):raise ValueError('PSBT trailing bytes')
    return dict(global_map=glob,inputs=ins,outputs=outs,tx=tx,bytes=len(raw))
def bip143(tx,index,input_atoms,pubkey_hash):
    if len(pubkey_hash)!=20:raise ValueError('P2WPKH keyhash required')
    row=tx['inputs'][index];script=b'\x76\xa9\x14'+pubkey_hash+b'\x88\xac'
    return sha256d(tx['version']+sha256d(b''.join(x['outpoint'] for x in tx['inputs']))+
                   sha256d(b''.join(x['sequence'] for x in tx['inputs']))+row['outpoint']+
                   compact(len(script))+script+input_atoms.to_bytes(8,'little')+row['sequence']+
                   sha256d(b''.join(output_bytes(x) for x in tx['outputs']))+tx['locktime']+b'\x01\x00\x00\x00')
