"""Verify only the original Thargoid panel's bounded old/new PE bodies.

Static file evidence; this does not attach, call a client function, request
metadata, install hooks, or alter the shared executable/hash profile.
"""
import hashlib
import importlib.util
import json
import struct
import difflib
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'))
functions = []
for rva in (0x3A0E740, 0x3A05660, 0x3A2B090, 0x3AA9F20, 0x3A161C0, 0x3A57E40):
    span = old.function(rva)
    size = span[1] - rva if span else 128
    row = audit.match(old, new, rva, size)
    if row['status'] != 'unique':
        raise ValueError('Unclosed original Thargoid function: ' + hex(rva) + ': ' + row['status'])
    row['oldRva'] = hex(rva)
    functions.append(row)

ctor = functions[0]
references = audit.references(old, new, 0x3A0E740, int(ctor['newRva'], 16), ctor['length'])
tables = {}
for before in (0x55276E8, 0x5527718, 0x5527818, 0x5527840, 0x5527868, 0x55278C0):
    candidates = {b for a, b, _, _ in references if a == before}
    if len(candidates) != 1:
        raise ValueError('Unclosed constructor vtable binding')
    tables[before] = candidates.pop()

slots = []
for offset, expected in ((0, 0x3A4C0C0), (0x18, 0x3A565B0), (0x20, 0xEC00F0), (0x28, 0x3A57E40)):
    before = struct.unpack('<Q', old.data(0x55278C0 + offset, 8))[0] - old.base
    after = struct.unpack('<Q', new.data(tables[0x55278C0] + offset, 8))[0] - new.base
    if before != expected:
        raise ValueError('Original DS interface changed')
    # These three getters are leaf routines without pdata entries. Verify all
    # their real instructions through RET, including exact member offsets.
    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('Thargoid published getter changed')
    refs = audit.references(old, new, before, after, length)
    if offset == 0 and not any(a == b == 0x5F5428C for a, b, _, _ in refs):
        raise ValueError('Dynamic original component ID moved')
    slots.append(dict(slot=hex(offset), oldRva=hex(before), newRva=hex(after), bytes=length,
                      oldSha256=hashlib.sha256(raw).hexdigest(), newSha256=hashlib.sha256(actual).hexdigest(),
                      normalizedIdentical=True))

cg_slots = []
for offset, expected in ((0, 0x3A4C090), (0x18, 0x11D3BF0)):
    before = struct.unpack('<Q', old.data(0x5527840 + offset, 8))[0] - old.base
    after = struct.unpack('<Q', new.data(tables[0x5527840] + offset, 8))[0] - new.base
    if before != expected:
        raise ValueError('Original shared CG interface 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 (
            offset == 0 and not any(a == b == 0x5FA10F0 for a, b, _, _ in refs)):
        raise ValueError('Shared original CG getter or dynamic ID changed')
    cg_slots.append(dict(slot=hex(offset), oldRva=hex(before), newRva=hex(after), bytes=length,
                         oldSha256=hashlib.sha256(raw).hexdigest(), newSha256=hashlib.sha256(actual).hexdigest(),
                         normalizedIdentical=True, references=[list(map(hex, r)) for r in refs]))

caller = next(f for f in functions if f['oldFunctionRange'][0] == '0x3a2b090')
writer_candidates = {b for a, b, _, _ in audit.references(old, new, 0x3A2B090, int(caller['newRva'], 16), caller['length']) if a == 0x3A6E3E0}
if len(writer_candidates) != 1:
    raise ValueError('Original parser call no longer uniquely bound')
writer = writer_candidates.pop()
writer_spans = [image.function(rva) for image, rva in ((old, 0x3A6E3E0), (new, writer))]
writer_members = []
writer_instructions = []
for image, span in zip((old, new), writer_spans):
    instructions = image.instructions(span[0], span[1]-span[0])
    writer_instructions.append(instructions)
    writer_members.append([dict(mnemonic=i.mnemonic, operands=i.op_str) for i in instructions
                           if any(op.type == audit.X86_OP_MEM and image.dis.reg_name(op.mem.base) == 'r13'
                                  and 0x318 <= op.mem.disp <= 0x528 for op in i.operands)])
if writer_members[0] != writer_members[1]:
    raise ValueError('Published Thargoid member writes changed')
shape_differences = []
for kind, a, b, c, d in difflib.SequenceMatcher(a=[i.mnemonic for i in writer_instructions[0]],
                                             b=[i.mnemonic for i in writer_instructions[1]], autojunk=False).get_opcodes():
    if kind != 'equal':
        shape_differences.append(dict(kind=kind, old=[dict(rva=hex(i.address-old.base), text=i.mnemonic+' '+i.op_str)
                                                    for i in writer_instructions[0][a:b]],
                                     new=[dict(rva=hex(i.address-new.base), text=i.mnemonic+' '+i.op_str)
                                          for i in writer_instructions[1][c:d]]))
output = dict(scope='Offline bounded Thargoid DS compatibility evidence; runtime values and live freshness unverified',
              runtimeNeutral=True, canInstallHooks=False,
              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=functions, vtables={hex(a): hex(b) for a, b in tables.items()},
              publishedInterfaceSlots=slots, componentIdGlobal='0x5f5428c', sharedCommunityGoalInterfaceSlots=cg_slots,
              changedParser=dict(oldRva='0x3a6e3e0', newRva=hex(writer), method='unique-call-from-completely-matched-response-handler',
                                 wholeBodyIdentical=False, note='Compiler stack locals moved and a duplicate JSON cleanup call+lea was removed. No whole-body equivalence claim; adapter does not call it.',
                                 oldInstructionCount=len(writer_instructions[0]), newInstructionCount=len(writer_instructions[1]),
                                 instructionShapeDifferences=shape_differences,
                                 publishedMemberInstructionsIdentical=True, publishedMemberInstructions=writer_members[0]))
path = Path('artifacts/galaxy-query-research/thargoid-account-sources/compatibility.json')
path.write_text(json.dumps(output, indent=2), encoding='utf8')
print(json.dumps(dict(report=str(path), functions=len(functions), vtables=output['vtables'], slots=slots), indent=2))
