"""Static bounded fingerprints for original Power HQ and outer view sources."""
import hashlib
import importlib.util
import json
import struct
from pathlib import Path

spec = importlib.util.spec_from_file_location('audit', Path(__file__).with_name('Audit-GalaxyClientCompatibility.py'))
audit = importlib.util.module_from_spec(spec)
spec.loader.exec_module(audit)
old = audit.Image(Path('artifacts/ship-model-reference/client-copy/EliteDangerous64.exe'))
new = audit.Image(Path('D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe'))
rows = []
for before in (0x3A6CF20, 0x3A75C70, 0x140AA80, 0x3B42700, 0x3B264B0, 0x3B27690, 0x3B28170, 0x1418810, 0x3AF6C40, 0x3B3E5B0,
               0x3ACF250,0x3AF6A80,0x3AC40B0,0x6CCBE0):
    span = old.function(before)
    if span is None or span[0] != before:
        raise ValueError('Expected an exact original bounded body')
    row = audit.match(old, new, before, span[1]-span[0])
    if row['status'] != 'unique':
        raise ValueError('Original bounded outer function is not uniquely matched: '+hex(before))
    row['oldRva'] = hex(before)
    rows.append(row)

getters = []
for slot, expected in ((0xA0, 0x140EBA0), (0x148, 0x1415D30)):
    before = struct.unpack('<Q', old.data(0x51BB640+slot, 8))[0]-old.base
    after = struct.unpack('<Q', new.data(0x51BB600+slot, 8))[0]-new.base
    if before != expected:
        raise ValueError('Original service getter binding changed')
    length = next(i.address+i.size-old.base-before for i in old.instructions(before, 128) if i.mnemonic == 'ret')
    raw, mask, *_ = audit.signature(old, before, length)
    actual = new.data(after, length)
    if any(a != b for a, b, keep in zip(raw, actual, mask) if keep):
        raise ValueError('Pure original service getter changed')
    getters.append(dict(slot=hex(slot), oldRva=hex(before), newRva=hex(after), bytes=length, normalizedIdentical=True,
                        oldSha256=hashlib.sha256(raw).hexdigest(), newSha256=hashlib.sha256(actual).hexdigest()))
navigation_getters = []
for old_table, new_table, expected in ((0x552A9E8, 0x552A9B8, 0x21E8D70), (0x552AE90, 0x552AE60, 0x56FD10)):
    for slot in (0, 0x50):
        before = struct.unpack('<Q', old.data(old_table+slot, 8))[0]-old.base
        after = struct.unpack('<Q', new.data(new_table+slot, 8))[0]-new.base
        if slot == 0x50 and before != expected:
            raise ValueError('Original navigation centre getter changed')
        length = next(i.address+i.size-old.base-before for i in old.instructions(before, 128) if i.mnemonic == 'ret')
        raw, mask, *_ = audit.signature(old, before, length)
        actual = new.data(after, length)
        refs = audit.references(old, new, before, after, length)
        if any(a != b for a, b, keep in zip(raw, actual, mask) if keep) or (
                slot == 0 and not any(a == b == 0x5F05DD4 for a, b, _, _ in refs)):
            raise ValueError('Navigation getter or dynamic registered type ID changed')
        navigation_getters.append(dict(oldVtable=hex(old_table), newVtable=hex(new_table), slot=hex(slot),
                                       oldRva=hex(before), newRva=hex(after), bytes=length, normalizedIdentical=True,
                                       oldSha256=hashlib.sha256(raw).hexdigest(), newSha256=hashlib.sha256(actual).hexdigest(),
                                       references=[list(map(hex, r)) for r in refs]))
# Derived Activities getter slots are independent of its base uniform getter.
# The full branch-only selector is 0x8D bytes through its last return, not just
# the first leaf return that a generic unwind-less fingerprint would retain.
style_leaves=[]
for before,length in ((0x3B28160,13),(0x3B2F080,0x8D)):
    row=audit.match(old,new,before,length)
    if row['status']!='unique':raise ValueError('Original outer appearance leaf is ambiguous: '+hex(before))
    after=int(row['newRva'],16)
    original=old.data(before,length);actual=new.data(after,length)
    if original!=actual:raise ValueError('Original outer appearance full leaf body changed')
    style_leaves.append(dict(oldRva=hex(before),newRva=hex(after),bytes=length,fullBytesIdentical=True,
        oldSha256=hashlib.sha256(original).hexdigest(),newSha256=hashlib.sha256(actual).hexdigest()))
proof = dict(runtimeNeutral=True, canInstallHooks=False, runtimeValidated=False,
             scope='Original Power HQ/outer-view static evidence only. No game invocation or request.',
             oldImage=dict(path=str(old.path), sha256=hashlib.sha256(old.raw).hexdigest()),
             newImage=dict(path=str(new.path), sha256=hashlib.sha256(new.raw).hexdigest()),
             functions=rows, pureServiceGetters=getters, navigationGetters=navigation_getters,outerStyleLeaves=style_leaves,
             originalAvailabilityAssembly=[dict(rva=hex(i.address-old.base), instruction=i.mnemonic+' '+i.op_str)
                                           for i in old.instructions(0x140AA80, 135)])
path = Path('artifacts/galaxy-query-research/power-outer-sources/compatibility.json')
path.write_text(json.dumps(proof, indent=2), encoding='utf8')
print(json.dumps(dict(report=str(path), functions=[dict(old=row['oldRva'], new=row.get('newRva'), status=row['status']) for row in rows], getters=getters), indent=2))
