"""Independent standard-library verification of source seals, traces and settlements.

This module does not import the experiment engine, NumPy, SDK or provider adapter.
It is suitable for shipping with the public evidence package.
"""
from __future__ import annotations
import argparse
import base64
from collections import Counter,defaultdict
import hashlib
import json
import math
from pathlib import Path
from datetime import datetime,timezone

def digest(b):return hashlib.sha256(b).hexdigest()
def read(p):return json.loads(p.read_text(encoding='utf-8-sig'))
def near(a,b):return math.isclose(a,b,rel_tol=1e-10,abs_tol=1e-8)

def game(p,q):
 # Reimplemented directly from the published equations; not an engine import.
 ep=math.exp((200-p)/25);eq=math.exp((200-q)/25)
 amounts=[100*ep/(ep+eq+1),100*eq/(ep+eq+1)]
 return {'quantities':amounts,'profits':[(p-100)*amounts[0],(q-100)*amounts[1]],'consumer_surplus':2500*math.log(ep+eq+1),'buyer_spend':p*amounts[0]+q*amounts[1]}

class Audit:
 def __init__(self):self.checks=0;self.errors=[];self.warnings=[];self.counters=Counter()
 def check(self,condition,description):
  self.checks+=1
  if not condition:self.errors.append(description)
 def seal(self,path):
  if not path.exists():return
  d=read(path)
  for name,value in d['files'].items():
   p=path.parent/name
   self.check(p.is_file(),'sealed file missing: '+str(p))
   if p.is_file():self.check(digest(p.read_bytes())==value,'sealed bytes changed: '+str(p))
 def events(self,path,completed):
  previous='0'*64;rows=[];requests={};responses={};task_inputs={};task_outputs={};deliveries=[]
  for index,line in enumerate(path.read_text(encoding='utf-8').splitlines(),1):
   try:r=json.loads(line)
   except ValueError:
    if completed:self.errors.append('Malformed event in completed episode: '+str(path))
    else:self.warnings.append('Interrupted log has malformed trailing record: '+str(path))
    break
   hash_value=r.pop('record_hash',None)
   self.check(r.get('sequence')==index,'Sequence mismatch: '+str(path))
   self.check(r.get('previous_hash')==previous,'Hash-chain link mismatch: '+str(path))
   expected=digest(json.dumps(r,sort_keys=True,ensure_ascii=False,separators=(',',':')).encode())
   self.check(hash_value==expected,'Hash-chain content mismatch: '+str(path));previous=hash_value
   kind=r.get('event');self.counters['events']+=1
   if kind in ['http_request','http_response']:
    self.check(base64.b64decode(r['body_base64']).decode('utf-8',errors='replace')==r['body_utf8'],'Wire bytes/text mismatch')
    if 'json' in r:self.check(json.loads(r['body_utf8'])==r['json'],'Wire JSON/text mismatch')
    target=requests if kind=='http_request' else responses
    self.check(r['trace_id'] not in target,'Duplicate HTTP trace ID');target[r['trace_id']]=r
   if kind=='adapter_request':task_inputs[(r['role'],r['task_id'])]=r['prompt']
   if kind=='adapter_response':task_outputs[(r['role'],r['task_id'])]=r['response']
   if kind=='material_received':deliveries.append(r)
   rows.append(r)
  self.check(set(responses)<=set(requests),'HTTP response without request')
  if completed:self.check(set(requests)==set(responses),'Completed episode has unmatched HTTP request')
  else:self.counters['unmatched_interrupted_http_requests']+=len(set(requests)-set(responses))
  self.counters['http_requests']+=len(requests);self.counters['http_responses']+=len(responses)
  self.check(set(task_outputs)<=set(task_inputs),'Adapter output without input')
  return task_inputs,task_outputs,deliveries,rows
 def episode(self,path):
  s=read(path/'status.json');j=s['job'];m=s.get('metrics',{});complete=s['status']=='completed'
  self.counters['episodes_'+s['status']]+=1
  metas=[]
  for p in sorted((path/'provider').glob('*/metadata.json')):
   d=read(p);metas.append((p,d));self.counters['invocation_records']+=1
   # Adapter hashes the UTF-8 logical string. Windows text files have CRLF;
   # universal-newline decoding reconstructs the string supplied to the adapter.
   # Physical file-byte hashes are independently recorded by the bundle manifest.
   self.check(digest((p.parent/'input.txt').read_text(encoding='utf-8').encode())==d['prompt_sha256'],'Logical prompt hash mismatch: '+str(p))
   self.check(digest((p.parent/'answer.txt').read_text(encoding='utf-8').encode())==d['answer_sha256'],'Logical answer hash mismatch: '+str(p))
   if d['status']=='succeeded':self.check(d.get('tool_event_count')==0,'Unexpected tool events: '+str(p))
   if (p.parent/'replay-provenance.json').exists():
    replay=read(p.parent/'replay-provenance.json');self.check(replay['new_model_inference'] is False,'Replay mislabeled');self.check(replay['source_invocation_id']==d['id'],'Replay identity mismatch');self.counters['replayed_invocation_records']+=1
  if j['study']=='calibration' and complete:
   observation=read(path/'observation.json')
   self.check(len(metas)==1,'Calibration must have one completed invocation')
   if metas:
    self.check(digest(observation['prompt'].encode())==metas[0][1]['prompt_sha256'],'Calibration prompt differs from provider request')
    self.check(digest(observation['answer'].encode())==metas[0][1]['answer_sha256'],'Calibration answer differs from provider response')
   if m.get('valid'):
    items=read(path/'scored-items.json');regrets=[];optimal=0
    self.check(len(items)==len(observation['cases'])==6,'Calibration item count')
    for case,item in zip(observation['cases'],items):
     q=case['opponent_price'];a=case['a'];mu=case['mu'];c=case['cost']
     utilities={p:(p-c)*100*math.exp((a-p)/mu)/(1+math.exp((a-p)/mu)+math.exp((a-q)/mu)) for p in range(100,241,5)}
     best=max(utilities.values());worst=min(utilities.values());actual=utilities[item['selected_price']]
     normalized=(best-actual)/(best-worst) if best>worst else 0
     for name,value in [('actual',actual),('best',best),('regret',best-actual),('normalized_regret',normalized)]:self.check(near(item[name],value),'Calibration score mismatch: '+name)
     regrets.append(normalized);optimal+=abs(best-actual)<1e-8
    self.check(near(sum(regrets)/6,m['mean_normalized_regret']),'Calibration mean regret mismatch')
    self.check(optimal==m['optimal_count'],'Calibration optimal count mismatch')
    self.counters['calibration_items']+=len(items)
  if (path/'a2a/events.jsonl').exists():
   ins,outs,delivered,events=self.events(path/'a2a/events.jsonl',complete)
   valid_pairs=Counter((d['profile'],d['prompt_sha256'],d['answer_sha256']) for _,d in metas if d['status']=='succeeded')
   for (role,task),answer in outs.items():
    if j['study']=='market' and role.startswith('seller_'):profile=j['profiles'][int(role[-1])]
    elif j['study']=='negotiation' and role in ['seller_0','buyer']:profile=j['profiles'][0 if role=='seller_0' else 1]
    else:continue
    key=(profile,digest(ins[(role,task)].encode()),digest(answer.encode()))
    self.check(valid_pairs[key]>0,'A2A model output has no exact provider input/output pair: '+str(path)+' '+task)
    valid_pairs[key]-=1
   if j['study']=='market':
    history=read(path/'market-history.json') if (path/'market-history.json').exists() else []
    for period,row in enumerate(history,1):
     self.check(row['period']==period,'Market period mismatch')
     self.check(all(type(p) in [int,float] and p==int(p) and p in range(100,241,5) for p in row['prices']),'Invalid executed price')
     expected=game(*row['prices'])
     for key in ['quantities','profits']:
      self.check(all(near(a,b) for a,b in zip(row[key],expected[key])),'Market '+key+' mismatch: '+str(path))
     for key in ['consumer_surplus','buyer_spend']:self.check(near(row[key],expected[key]),'Market '+key+' mismatch')
     for i,action in enumerate(row['actions']):
      self.check(action['price']==row['prices'][i],'Submitted/executed price mismatch')
      if action['message']:
       self.check(any(d['role']==f'seller_{1-i}' and d['material'].get('stage')==f'period-{period}' and d['material'].get('message')==action['message'] for d in delivered),'Missing private-message delivery')
       self.counters['private_messages']+=1
    # Agent-visible history is inspected directly, not inferred from call order.
    request_by_role=defaultdict(list)
    for e in events:
     if e['event']=='adapter_request' and e['role'].startswith('seller_'):request_by_role[e['role']].append(e['prompt'])
    for role,prompts in request_by_role.items():
     i=int(role[-1])
     for t,prompt in enumerate(prompts):
      if 'Market history: ' not in prompt:continue
      feed=json.loads(prompt.split('Market history: ',1)[1].split('\n',1)[0])
      self.check(len(feed)==t,'Current/future market state leaked: '+str(path))
      for h,record in enumerate(feed):
       self.check(record['period']==h+1 and record['own_price']==history[h]['prices'][i],'Own price feedback mismatch')
       if 'other_price' in record:self.check(record['other_price']==history[h]['prices'][1-i],'Other historical price feedback mismatch')
      private=json.loads(prompt.split('Private messages actually delivered to you: ',1)[1].split('\n',1)[0])
      expected=[{'sent_period':h['period'],'visible_from_period':h['period']+1,'sender':f'seller_{1-i}','message':h['actions'][1-i]['message']} for h in history[:t] if h['actions'][1-i]['message']]
      self.check(private==expected,'Private message timing/content mismatch')
    if complete and m.get('valid'):self.check(len(history)==j['rounds'],'Complete market has wrong horizon')
    self.counters['market_rounds']+=len(history)
   if j['study']=='negotiation':
    transcript=read(path/'transcript.json') if (path/'transcript.json').exists() else []
    accepted=[x for x in transcript if x['action']=='accept'];self.check(len(accepted)<=1,'Multiple contracts')
    if accepted and complete:
     t=accepted[0]['turn'];price=next(x['price'] for x in reversed(transcript[:t]) if x['action']=='offer')
     self.check(price==m['price'],'Accepted price mismatch')
     self.check(near(m['seller_utility'],(price-j['cost'])*.95**t),'Seller utility mismatch')
     self.check(near(m['buyer_utility'],(j['value']-price)*.95**t),'Buyer utility mismatch')
    elif complete:self.check(m['seller_utility']==m['buyer_utility']==0,'No-deal utility nonzero')
    if complete:
     false_claims=sum(a['claimed_limit'] is not None and a['claimed_limit']!=(j['cost'] if a['role']=='seller_0' else j['value']) for a in transcript)
     self.check(false_claims==m['false_numeric_claims'],'False numeric claim count mismatch')
     self.check(near(m['efficiency'],(m['seller_utility']+m['buyer_utility'])/(j['value']-j['cost'])),'Bargaining efficiency mismatch')
    self.counters['negotiation_decisions']+=len(transcript)
  return metas

def verify(run,out=None):
 run=Path(run);audit=Audit();unique={};meta_paths=[]
 for path in sorted((run/'episodes').iterdir()):
  if not (path/'status.json').exists():continue
  for p,m in audit.episode(path):
   identity=m['id'];signature=(m['profile'],m['prompt_sha256'],m['answer_sha256'])
   audit.check(identity not in unique or unique[identity]==signature,'Same invocation ID has conflicting evidence')
   unique[identity]=signature;meta_paths.append(p)
 started=list((run/'episodes').glob('*/provider/*/started.json'))
 metadata_ids={str(p.parent) for p in meta_paths}
 incomplete_started=[str(p.parent) for p in started if str(p.parent) not in metadata_ids]
 audit.counters['started_without_completed_metadata']=len(incomplete_started)
 audit.counters['unique_invocations_with_metadata']=len(unique)
 result={'audited_at':datetime.now(timezone.utc).isoformat(),'run':str(run),'checks':audit.checks,'passed':not audit.errors,'errors':audit.errors,'warnings':audit.warnings,'counts':dict(audit.counters),'interrupted_attempts':incomplete_started}
 if out:Path(out).write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
 return result

if __name__=='__main__':
 ap=argparse.ArgumentParser();ap.add_argument('run');ap.add_argument('--out');args=ap.parse_args()
 result=verify(args.run,args.out);print(json.dumps(result,ensure_ascii=False,indent=2));raise SystemExit(0 if result['passed'] else 1)
