"""Read only the completed market/list buffer owned by our build-pinned plugin.

No calls, process writes, hooks, UI or transactions. Exports market quotes only.
"""
import argparse,ctypes as c,ctypes.wintypes as w,hashlib,json,re,struct
from pathlib import Path
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--pid',type=int,required=True);p.add_argument('--market',type=int,required=True);p.add_argument('--output',type=Path,required=True);a=p.parse_args()
game=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe')
plugin=game.parent/'dinput8.dll';local=Path('XFEEDToolBox/PluginPackage/dinput8.dll')
if hashlib.sha256(plugin.read_bytes()).digest()!=hashlib.sha256(local.read_bytes()).digest():raise ValueError('Plugin image changed')
mapping=Path('artifacts/plugin-build/dinput8.map').read_text(encoding='utf-8',errors='replace')
def symbol(name):
 rows=[line for line in mapping.splitlines() if '?'+name+'@navigation@xfe@@' in line]
 if len(rows)!=1:raise ValueError('Ambiguous owned plugin symbol')
 m=re.search(r'\s([0-9a-fA-F]{16})\s+navigation\.obj',rows[0]);return int(m.group(1),16)-0x180000000
k=c.WinDLL('kernel32',use_last_error=True);k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.ReadProcessMemory.argtypes=[w.HANDLE,c.c_void_p,c.c_void_p,c.c_size_t,c.POINTER(c.c_size_t)]
k.QueryFullProcessImageNameW.argtypes=[w.HANDLE,w.DWORD,w.LPWSTR,c.POINTER(w.DWORD)];k.CloseHandle.argtypes=[w.HANDLE]
class Module(c.Structure):
 _fields_=[('dwSize',w.DWORD),('th32ModuleID',w.DWORD),('th32ProcessID',w.DWORD),('GlblcntUsage',w.DWORD),('ProccntUsage',w.DWORD),('modBaseAddr',c.c_void_p),('modBaseSize',w.DWORD),('hModule',w.HMODULE),('szModule',w.WCHAR*256),('szExePath',w.WCHAR*260)]
k.CreateToolhelp32Snapshot.argtypes=[w.DWORD,w.DWORD];k.CreateToolhelp32Snapshot.restype=w.HANDLE
k.Module32FirstW.argtypes=[w.HANDLE,c.POINTER(Module)];k.Module32NextW.argtypes=[w.HANDLE,c.POINTER(Module)]
h=k.OpenProcess(0x410,False,a.pid)
if not h:raise c.WinError(c.get_last_error())
def read(at,size):
 b=c.create_string_buffer(size);done=c.c_size_t()
 if not k.ReadProcessMemory(h,at,b,size,c.byref(done)) or done.value!=size:raise c.WinError(c.get_last_error())
 return b.raw
try:
 name=c.create_unicode_buffer(32768);n=w.DWORD(len(name))
 if not k.QueryFullProcessImageNameW(h,0,name,c.byref(n)) or Path(name.value).resolve()!=game.resolve():raise ValueError('Wrong game process')
 snap=k.CreateToolhelp32Snapshot(0x18,a.pid);m=Module();m.dwSize=c.sizeof(m);found=k.Module32FirstW(snap,c.byref(m));base=0
 while found:
  if Path(m.szExePath).resolve()==plugin.resolve():base=m.modBaseAddr;break
  found=k.Module32NextW(snap,c.byref(m))
 k.CloseHandle(snap)
 if not base:raise ValueError('Owned plugin absent')
 work=read(base+symbol('commerceRead'),64);market,state=struct.unpack_from('<II',work,48)
 if market!=a.market or state not in (3,4):raise ValueError('No terminal matching original commerce response')
 at=base+symbol('commerceReadBody');header=read(at,32);length,capacity=struct.unpack_from('<QQ',header,16)
 if not 0<length<1024*1024 or capacity<length:raise ValueError('Invalid owned response bound')
 pointer=struct.unpack_from('<Q',header)[0] if capacity>=16 else at
 raw_response=read(pointer,length)
 try:response=json.loads(raw_response)
 except (ValueError,UnicodeError):
  import xml.etree.ElementTree as ET
  try:
   tree=ET.fromstring(raw_response)
   numeric={}
   for key in ('credits','costPerPartialUnit','costToRefuel','fuelLevel','fuelCapacity','reserveLevel','reserveCapacity'):
    found=list(tree.iter(key))
    if len(found)==1 and found[0].text and re.fullmatch(r'-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?',found[0].text):numeric[key]=float(found[0].text) if any(v in found[0].text for v in '.eE') else int(found[0].text)
   service_nodes=[]
   for node in tree.iter():
    if node.tag in ('prices','repair','hullCosts','shipIntegrityCosts','paintjobCosts'):
     service_nodes.append({'tag':node.tag,'children':[x.tag for x in node],
       'numeric':{x.tag:float(x.text) for x in node if x.text and re.fullmatch(r'-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?',x.text)}})
   diagnostic={'pid':a.pid,'marketId':market,'state':state,'bodyBytes':length,'format':'original-xml','rootTag':tree.tag,'rootChildren':[x.tag for x in tree],'serviceNodes':service_nodes,'numericServiceFields':numeric,'processAccess':'QUERY_INFORMATION | VM_READ','mutationsSent':0}
   with a.output.open('x',encoding='utf-8') as out:json.dump(diagnostic,out,indent=2)
   print(json.dumps(diagnostic));raise SystemExit(0)
  except ET.ParseError:pass
  diagnostic={'pid':a.pid,'marketId':market,'state':state,'bodyBytes':length,'firstByte':raw_response[0],
              'asciiFraction':sum(32<=v<127 or v in (9,10,13) for v in raw_response)/length,
              'knownPrefixes':{key:raw_response.startswith(value) for key,value in {'jsonObject':b'{','jsonArray':b'[','html':b'<','gzip':b'\x1f\x8b','utf8Bom':b'\xef\xbb\xbf','zlib':b'\x78\x9c'}.items()},
              'containsKnownFields':{key:(key.encode() in raw_response) for key in ('fuelLevel','credits','error','prices')},'processAccess':'QUERY_INFORMATION | VM_READ','mutationsSent':0}
  with a.output.open('x',encoding='utf-8') as out:json.dump(diagnostic,out,indent=2)
  print(json.dumps(diagnostic));raise SystemExit(0)
 if read(at,32)!=header or read(base+symbol('commerceRead'),64)!=work:raise ValueError('Owned response changed during read')
 commodities=response.get('commodities',{})
 rows=[]
 for key,item in commodities.items():
  row={field:item[field] for field in ('id','name','buyPrice','sellPrice','meanPrice','stock','demand','statusFlags','legality') if field in item};row['responseKey']=key
  rows.append(row)
 inventory=[{k:item[k] for k in ('commodity','qty','stolen','marked') if k in item} for item in response.get('inventory',[]) if isinstance(item,dict)]
 prices=response.get('prices',{})
 service_prices={key:prices[key] for key in ('fuel','repair','ammunition','drones') if key in prices}
 scalar_fields={key:response[key] for key in ('credits','error') if key in response and isinstance(response[key],(str,int,float,bool,type(None)))}
 result={'pid':a.pid,'marketId':market,'state':state,'responseFields':{key:type(value).__name__ for key,value in response.items()},'scalarFields':scalar_fields,'commodities':rows,'inventory':inventory,'servicePrices':service_prices,'priceFields':{key:type(value).__name__ for key,value in prices.items()},'commodityFields':list(next(iter(commodities.values()),{})),'inventoryFields':[list(x) if isinstance(x,dict) else type(x).__name__ for x in response.get('inventory',[])],
         'screenUse':False,'processAccess':'QUERY_INFORMATION | VM_READ','mutationsSent':0}
 with a.output.open('x',encoding='utf-8') as out:json.dump(result,out,indent=2)
 print(json.dumps({'marketId':market,'quotes':len(rows),'commodityFields':result['commodityFields'],'inventoryFields':result['inventoryFields'],'mutationsSent':0}))
 print(json.dumps([r for r in rows if str(r.get('name','')).lower() in ('biowaste','scrap','hydrogenfuel','drones','water','grain')]))
 print(json.dumps({'inventory':inventory}))
 print(json.dumps({'servicePrices':service_prices}))
 print(json.dumps({'responseFields':result['responseFields'],'scalarFields':scalar_fields,'priceFields':result['priceFields']}))
finally:k.CloseHandle(h)
