from __future__ import annotations
import collections, hashlib, json, os, sqlite3, urllib.request
from pathlib import Path
DB=Path('/home/agent/jarvis_runtime/finance-system/data/finance.sqlite3')
ROOT=Path('/home/agent/jarvis_runtime/finance-system/sprint16.1-uat')
PAYLOAD=ROOT/'real-preview-payload-private.json'
RESPONSE=ROOT/'sprint16.1-real-preview-v2-private.json'
EVIDENCE=ROOT/'sprint16.1-readonly-uat-v2-private.json'
SOURCES=[Path('/home/agent/.hermes/private/finance/sprint16')/x for x in ('raiffeisen.csv','akb_transactions.csv','visa.csv','migros.csv')]
def sha_file(p):
 h=hashlib.sha256();
 with p.open('rb') as f:
  for chunk in iter(lambda:f.read(1024*1024),b''):h.update(chunk)
 return h.hexdigest()
def table_digest(c,t):
 h=hashlib.sha256(); cols=[r[1] for r in c.execute(f"PRAGMA table_info('{t}')")]
 h.update(t.encode())
 for row in c.execute(f'SELECT * FROM "{t}" ORDER BY rowid'):
  h.update(json.dumps(list(row),ensure_ascii=False,default=str,separators=(',',':')).encode())
 return h.hexdigest()
def snapshot():
 c=sqlite3.connect(f'file:{DB}?mode=ro',uri=True)
 tables=[r[0] for r in c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")]
 s={'tables':{t:{'count':c.execute(f'SELECT COUNT(*) FROM "{t}"').fetchone()[0],'digest':table_digest(c,t)} for t in tables},'fk':list(c.execute('PRAGMA foreign_key_check')),'integrity':c.execute('PRAGMA integrity_check').fetchone()[0]}; c.close(); return s
before=snapshot(); source_before={str(p):sha_file(p) for p in SOURCES}
data=PAYLOAD.read_bytes(); req=urllib.request.Request('http://127.0.0.1:18061/api/budget/household/imports/preview',data=data,headers={'Content-Type':'application/json'},method='POST')
with urllib.request.urlopen(req,timeout=1800) as resp: body=resp.read(); status=resp.status
fd=os.open(RESPONSE,os.O_CREAT|os.O_EXCL|os.O_WRONLY,0o600)
with os.fdopen(fd,'wb') as f:f.write(body)
result=json.loads(body)
after=snapshot(); source_after={str(p):sha_file(p) for p in SOURCES}
changed=[t for t in before['tables'] if before['tables'][t]!=after['tables'][t]]
rows=result.get('rows',[]); clusters=result.get('merchant_clusters',[])
unresolved=[r for r in rows if r.get('user_state')=='decision_needed' and r.get('disposition') not in ('duplicate_file','duplicate_source_row','pending','superseded_pending')]
individual=[{'review_no':i,'source_type':r.get('source_type'),'transaction_date':r.get('transaction_date'),'transaction_semantics':r.get('transaction_semantics'),'disposition':r.get('disposition'),'classification':r.get('classification'),'proposed_category_name':r.get('proposed_category_name'),'requires_review':r.get('requires_review')} for i,r in enumerate(unresolved,1)]
cluster_summary=[{'cluster_no':i,'merchant_family':c.get('merchant_family'),'transaction_semantics':c.get('transaction_semantics'),'count':c.get('count'),'category_decision_allowed':c.get('category_decision_allowed'),'category_name':c.get('category_name'),'selected_count':c.get('selected_count'),'excluded_count':c.get('excluded_count')} for i,c in enumerate(clusters,1)]
proposal_categories=collections.Counter(str(r.get('proposed_category_name') or 'unbenannt') for r in rows if r.get('user_state')=='proposal_ready')
safe_categories=collections.Counter(str(r.get('proposed_category_name') or 'unbenannt') for r in rows if r.get('user_state')=='proposal_ready' and not r.get('requires_review'))
evidence={'http_status':status,'db_before':before,'db_after':after,'changed_tables':changed,'source_hashes_unchanged':source_before==source_after,'source_hashes_before':source_before,'source_hashes_after':source_after,'response_path':str(RESPONSE),'counts':result.get('counts'),'review_threshold':result.get('review_threshold'),'readiness_checks':result.get('readiness_checks'),'technically_confirmable':result.get('technically_confirmable'),'business_ready_for_confirm':result.get('business_ready_for_confirm'),'errors':result.get('errors'),'individual_review_bookings':individual,'merchant_clusters':cluster_summary,'proposal_categories':dict(proposal_categories),'safe_categories':dict(safe_categories),'confirm_called':False,'real_import_performed':False}
fd=os.open(EVIDENCE,os.O_CREAT|os.O_EXCL|os.O_WRONLY,0o600)
with os.fdopen(fd,'w') as f:json.dump(evidence,f,ensure_ascii=False,indent=2)
print(json.dumps({'http_status':status,'private_response':str(RESPONSE),'private_evidence':str(EVIDENCE),'modes':[oct(RESPONSE.stat().st_mode&0o777),oct(EVIDENCE.stat().st_mode&0o777)],'db_changed_tables':changed,'db_unchanged':not changed,'source_files_unchanged':source_before==source_after,'fk_findings_before_after':[len(before['fk']),len(after['fk'])],'integrity_before_after':[before['integrity'],after['integrity']],'counts':result.get('counts'),'review_threshold':result.get('review_threshold'),'readiness_checks':result.get('readiness_checks'),'technically_confirmable':result.get('technically_confirmable'),'business_ready_for_confirm':result.get('business_ready_for_confirm'),'errors':result.get('errors'),'individual_review_count':len(individual),'merchant_cluster_count':len(clusters),'proposal_categories':dict(proposal_categories),'safe_categories':dict(safe_categories),'confirm_called':False},ensure_ascii=False,indent=2))
