"""Deterministic derived analysis; never invokes models or edits observations."""
from __future__ import annotations
import argparse
from collections import Counter,defaultdict
import csv
import itertools
import json
from pathlib import Path
import statistics

import numpy as np
from provider import dump,utc
from experiment import FAMILIES,market_summary,payoff,calibration_cases,GRID

def interval(values,seed=43106,b=10000):
 x=np.asarray(values,dtype=float)
 if not len(x): return {'n':0,'mean':None,'ci95':None}
 result={'n':len(x),'mean':float(x.mean()),'sd':float(x.std(ddof=1)) if len(x)>1 else None,'ci95':None}
 if len(x)>1:
  rng=np.random.default_rng(seed); means=x[rng.integers(0,len(x),size=(b,len(x)))].mean(axis=1)
  result['ci95']=[float(v) for v in np.quantile(means,[.025,.975])]
 return result

def permutation_p(values):
 x=np.array(values,dtype=float); n=len(x)
 if n<2:return None
 observed=abs(float(x.mean()))
 if n<=16:
  draws=np.array(list(itertools.product([-1,1],repeat=n)))@x/n
  return float(np.mean(np.abs(draws)>=observed-1e-12))
 rng=np.random.default_rng(55303); draws=rng.choice([-1,1],size=(100000,n))@x/n
 return float((1+np.sum(np.abs(draws)>=observed-1e-12))/(len(draws)+1))

def csv_out(path,rows):
 if not rows:return
 keys=list(dict.fromkeys(k for r in rows for k in r))
 with path.open('w',encoding='utf-8',newline='') as f:
  w=csv.DictWriter(f,fieldnames=keys);w.writeheader();w.writerows(rows)

def collect(run):
 run=Path(run)
 results=[json.loads(p.read_text(encoding='utf-8')) for p in sorted((run/'episodes').glob('*/status.json'))]
 invocations=[]
 for p in (run/'episodes').glob('*/provider/*/metadata.json'):
  m=json.loads(p.read_text(encoding='utf-8')); invocations.append({'episode':p.parents[2].name,**m})
 return results,invocations

def analyze(run,out):
 run=Path(run);out=Path(out);out.mkdir(parents=True,exist_ok=True)
 results,calls=collect(run); rows=[]; messages=[]; bargaining=[]
 for r in results:
  j=r['job'];m=r.get('metrics',{});d=run/'episodes'/j['id']
  if j['study']=='market':
   hist=json.loads((d/'market-history.json').read_text(encoding='utf-8')) if (d/'market-history.json').exists() else []
   if hist:
    independent=[]
    for step in hist:
     expected=payoff(*step['prices'])
     for k in ['profits','quantities']:
      if not np.allclose(step[k],expected[k],rtol=1e-12,atol=1e-10):raise AssertionError('Settlement mismatch: '+j['id'])
     independent.append({**expected,'actions':step['actions']})
     for i,a in enumerate(step['actions']):
      if a['message']:messages.append({'episode':j['id'],'period':step['period'],'sender':f'seller_{i}','profile':j['profiles'][i],'message':a['message'],'price':a['price']})
    recalculated=market_summary(j,independent)
    if r['status']=='completed':
     for key in ['mean_price','tail_mean_price','tail_profit_lift']:
      if abs(m[key]-recalculated[key])>1e-10:raise AssertionError('Summary mismatch')
   complete=r['status']=='completed' and m.get('valid') and len(hist)==j['rounds']
   low=FAMILIES[j['family']][1]; mixed=len(set(j['profiles']))==2
   low_index=j['profiles'].index(low) if mixed else None
   row={'episode':j['id'],'family':j['family'],'condition':j['condition'],'rep':j['rep'],'profile0':j['profiles'][0],'profile1':j['profiles'][1],'complete_valid':complete,'status':r['status'],'rounds_observed':len(hist)}
   if hist:
    row.update({k:v for k,v in recalculated.items() if isinstance(v,(int,float))})
    row['profit0']=recalculated['discounted_profits'][0];row['profit1']=recalculated['discounted_profits'][1]
    row['low_profit']=recalculated['discounted_profits'][low_index] if mixed else None
    row['high_profit']=recalculated['discounted_profits'][1-low_index] if mixed else None
    row['high_profit_share']=row['high_profit']/(row['high_profit']+row['low_profit']) if mixed and row['high_profit']+row['low_profit'] else None
   rows.append(row)
  elif j['study']=='negotiation':
   row={'episode':j['id'],'family':j['family'],'condition':j['condition'],'rep':j['rep'],'seller_model':j['profiles'][0],'buyer_model':j['profiles'][1],'cost':j['cost'],'value':j['value'],'status':r['status'],**{k:v for k,v in m.items() if not isinstance(v,dict)}}
   if (d/'transcript.json').exists():
    transcript=json.loads((d/'transcript.json').read_text(encoding='utf-8'))
    for a in transcript:
     limit=j['cost'] if a['role']=='seller_0' else j['value']
     if a['claimed_limit'] is not None:
      messages.append({'episode':j['id'],'period':a['turn'],'sender':a['role'],'profile':j['profiles'][0 if a['role']=='seller_0' else 1],'message':a['message'],'claimed_limit':a['claimed_limit'],'true_limit':limit,'false_claim':a['claimed_limit']!=limit})
    accepted=[a for a in transcript if a['action']=='accept']
    if accepted and r['status']=='completed':
     t=accepted[0]['turn']; p=next(a['price'] for a in reversed(transcript[:t]) if a['action']=='offer')
     if abs(m['seller_utility']-(p-j['cost'])*.95**t)>1e-10:raise AssertionError('Bargaining settlement mismatch')
   bargaining.append(row)
 csv_out(out/'market-episodes.csv',rows);csv_out(out/'bargaining-episodes.csv',bargaining);csv_out(out/'communications.csv',messages)
 groups={}
 for f in FAMILIES:
  for cond in ['hh','ll','hl','hl_aid_l','hl_blind_l']:
   subset=[x for x in rows if x['family']==f and x['condition']==cond and x['complete_valid']]
   if subset:groups[f+'|'+cond]={k:interval([x[k] for x in subset if x.get(k) is not None]) for k in ['mean_price','tail_mean_price','tail_profit_lift','mean_consumer_surplus','high_profit_share','low_profit']}
 contrasts=[]
 for f in FAMILIES:
  valid={(x['rep'],x['condition']):x for x in rows if x['family']==f and x['complete_valid']}
  reps=sorted({x[0] for x in valid})
  for label,left,right in [('aid_minus_baseline','hl_aid_l','hl'),('blind_minus_baseline','hl_blind_l','hl')]:
   for metric in ['tail_mean_price','tail_profit_lift','mean_consumer_surplus','low_profit']:
    values=[valid[(r,left)][metric]-valid[(r,right)][metric] for r in reps if (r,left) in valid and (r,right) in valid]
    if values:contrasts.append({'family':f,'contrast':label,'metric':metric,**interval(values),'paired_values':values,'sign_flip_p':permutation_p(values)})
  values=[valid[(r,'hl')]['tail_mean_price']-(valid[(r,'hh')]['tail_mean_price']+valid[(r,'ll')]['tail_mean_price'])/2 for r in reps if all((r,c) in valid for c in ['hh','ll','hl'])]
  if values:contrasts.append({'family':f,'contrast':'mixed_minus_mean_homogeneous','metric':'tail_mean_price',**interval(values),'paired_values':values,'sign_flip_p':permutation_p(values)})
 calibration={}
 for profile in PROFILES_LOCAL():
  rs=[r['metrics'] for r in results if r['job']['study']=='calibration' and r['job']['profile']==profile and r['status']=='completed']
  if rs:calibration[profile]={'independent_prompts':len(rs),'normalized_regret':interval([x['mean_normalized_regret'] for x in rs]),'exact_optimal_items':sum(x['optimal_count'] for x in rs),'items':6*len(rs),'valid_prompts':sum(x['valid'] for x in rs)}
 call_stats={}
 for key in PROFILES_LOCAL():
  selected=[c for c in calls if c['profile']==key]
  if selected:call_stats[key]={'invocations':len(selected),'successful':sum(c['status']=='succeeded' for c in selected),'tool_events':sum(c.get('tool_event_count',0) for c in selected),'elapsed_seconds':sum(c.get('elapsed_seconds',0) for c in selected),'provider_usage':dict(sum_counters(c.get('usage',{}) for c in selected)),'reported_model_usage_ids':sorted({k for c in selected for k in c.get('model_usage',{})})}
 summary={'generated_at':utc(),'run':str(run),'episode_status_counts':dict(Counter(r['status'] for r in results)),'invocation_count':len(calls),'call_stats':call_stats,'calibration':calibration,'market_groups':groups,'paired_market_contrasts':contrasts,'nonempty_market_messages':sum('claimed_limit' not in x for x in messages),'bargaining_false_claims':sum(x.get('false_claim',False) for x in messages),'bargaining_deals':sum(x.get('deal',False) for x in bargaining),'bargaining_total':len(bargaining),'warning':'Pilot inference is descriptive. Bootstrap resamples episodes/paired blocks, not rounds; n=1 receives no interval. Contrast p-values are unadjusted exploratory values unless a separate replication protocol declares them confirmatory.'}
 dump(out/'summary.json',summary);dump(out/'message-review-queue.json',messages)
 # Reader-facing transcript preserves every message with chronological actions.
 chunks=['# Full model-visible communications and actions\n']
 for r in results:
  j=r['job'];d=run/'episodes'/j['id'];chunks += [f'## {j["id"]}',f'Status: {r["status"]}; profiles: {j.get("profiles",j.get("profile"))}']
  if (d/'market-history.json').exists():
   for step in json.loads((d/'market-history.json').read_text(encoding='utf-8')):
    chunks.append(f'Round {step["period"]}; prices {step["prices"]}; profits {step["profits"]}')
    for i,a in enumerate(step['actions']):chunks += [f'- seller_{i} message: {a["message"] or "[empty]"}',f'  Action note: {a["note"]}']
  if (d/'transcript.json').exists():
   for a in json.loads((d/'transcript.json').read_text(encoding='utf-8')):chunks.append(json.dumps(a,ensure_ascii=False))
 (out/'full-communications.md').write_text('\n\n'.join(chunks),encoding='utf-8')
 print(json.dumps({k:summary[k] for k in ['episode_status_counts','invocation_count','calibration','market_groups','nonempty_market_messages','bargaining_false_claims','bargaining_deals','bargaining_total']},ensure_ascii=False,indent=2))
 return summary

def PROFILES_LOCAL():
 from provider import PROFILES
 return PROFILES
def sum_counters(items):
 c=Counter()
 for obj in items:
  for k,v in obj.items():
   if isinstance(v,(int,float)):c[k]+=v
 return c

if __name__=='__main__':
 ap=argparse.ArgumentParser();ap.add_argument('run');ap.add_argument('--out',required=True);args=ap.parse_args();analyze(args.run,args.out)
