"""Normalize explicitly declared dates; read-only CSV input, write a separate output."""
import csv,datetime,decimal,hashlib,json,sys,pathlib
D=decimal.Decimal

def date_value(raw,kind):
 if not raw.strip():raise ValueError('blank_date')
 if kind=='ISO':return datetime.datetime.strptime(raw,'%Y-%m-%d').date().isoformat()
 if kind=='DDMM':return datetime.datetime.strptime(raw,'%d/%m/%Y').date().isoformat()
 if kind=='EXCEL1900':
  n=D(raw)
  if not n.is_finite() or n!=n.to_integral_value() or n<1 or n>2958465 or n==60:raise ValueError('invalid_excel_serial')
  return (datetime.date(1899,12,31)+datetime.timedelta(days=int(n)-(1 if n>60 else 0))).isoformat()
 raise ValueError('undeclared_date_format')

def normalize(source,output,rejects):
 seen=set(); clean=[]; bad=[];reasons={};count=0
 with open(source,newline='') as f:
  for line,r in enumerate(csv.DictReader(f),2):
   count+=1;reason=''
   try:
    key=r['ID'].strip()
    if not key:raise ValueError('missing_id')
    try:amount=D(r['Amount'].strip())
    except decimal.InvalidOperation:raise ValueError('invalid_amount')
    if not amount.is_finite() or amount*100!=(amount*100).to_integral_value():raise ValueError('invalid_amount')
    try:date=date_value(r['Date'],r['DateFormat'])
    except (decimal.InvalidOperation,OverflowError):raise ValueError('invalid_date')
    if r['Currency']!='USD':raise ValueError('invalid_currency')
    if key in seen:raise ValueError('duplicate_valid_id')
    seen.add(key);clean.append([key,str(int(amount*100)),date,'USD',r['Note']])
   except ValueError as e:
    reason=str(e) if str(e) in ['blank_date','missing_id','invalid_amount','invalid_excel_serial','undeclared_date_format','invalid_currency','duplicate_valid_id'] else 'invalid_date'
    reasons[reason]=reasons.get(reason,0)+1;bad.append([line,reason]+[r[k] for k in ['ID','Amount','Date','DateFormat','Currency','Note']])
 for p,header,rows in [(output,['ID','AmountCents','DateISO','Currency','Note'],clean),(rejects,['SourceLine','RejectReason','ID','Amount','Date','DateFormat','Currency','Note'],bad)]:
  with open(p,'w',newline='') as f:w=csv.writer(f);w.writerow(header);w.writerows(rows)
 return {'input_rows':count,'clean_rows':len(clean),'reject_rows':len(bad),'reject_reasons':reasons,'clean_amount_cents':sum(int(r[1]) for r in clean),'planned_500_row_writes':(len(clean)+499)//500,'partial_note_rows_preserved':sum(not r[4] for r in clean),'clean_sha256':hashlib.sha256(pathlib.Path(output).read_bytes()).hexdigest()}
if __name__=='__main__':print(json.dumps(normalize(*sys.argv[1:4]),indent=2))
