import csv, io, math, re, statistics
from collections import Counter
REQUIRED=['Pasien_ID','Hak_Kelas','Diagnosis_Primer','Jml_Diag_Sekunder','Hari_Rawat_LOS','Indeks_Biaya']
CODE_RE=re.compile(r'^[A-Z][0-9]{2}(?:\.?[0-9A-Z]{1,2})?$',re.I)
# These are source-data interpretations to flag, not ICD verification.
KNOWN_LABEL_PROBLEMS={'P034':'P03.4 merujuk pada kondisi fetus/newborn, bukan prosedur sesar pada ibu.', 'O342':'Kode O34.2 tidak boleh disamakan dengan O80.'}
def code_of(value):
    v=str(value).strip().upper().split('-')[0].replace('.','')
    return v

def parse_csv(content):
    text=content.decode('utf-8-sig') if isinstance(content,bytes) else content
    reader=csv.DictReader(io.StringIO(text))
    if not reader.fieldnames: raise ValueError('CSV kosong atau header tidak ditemukan.')
    missing=[x for x in REQUIRED if x not in reader.fieldnames]
    if missing: raise ValueError('Kolom wajib tidak ditemukan: '+', '.join(missing))
    rows=[]; issues=[]; seen=set(); total=0
    for ln,row in enumerate(reader,2):
        if not any(str(v or '').strip() for v in row.values()): continue
        total+=1
        if total>20000: raise ValueError('Batas demonstrasi 20.000 baris per file.')
        id_=str(row.get('Pasien_ID') or '').strip()
        if not id_: issues.append({'line':ln,'type':'error','message':'Pasien_ID kosong'});continue
        if id_ in seen: issues.append({'line':ln,'type':'error','message':'ID klaim duplikat: '+id_});continue
        seen.add(id_)
        try:
            los=float(row['Hari_Rawat_LOS']); ds=float(row['Jml_Diag_Sekunder']); cost=float(row['Indeks_Biaya'])
            if not all(math.isfinite(x) for x in (los,ds,cost)): raise ValueError()
            if not (1<=los<=365 and los.is_integer() and 0<=ds<=100 and ds.is_integer() and cost>=0):raise ValueError()
        except (ValueError,TypeError):
            issues.append({'line':ln,'type':'error','message':'LOS, diagnosis sekunder, atau biaya tidak valid'});continue
        dx=str(row['Diagnosis_Primer'] or '').strip(); code=code_of(dx)
        if not CODE_RE.match(code): issues.append({'line':ln,'type':'warning','message':'Format kode diagnosis perlu dicek: '+dx[:60]})
        if code in KNOWN_LABEL_PROBLEMS and '-' in dx: issues.append({'line':ln,'type':'warning','message':f'{id_}: '+KNOWN_LABEL_PROBLEMS[code]})
        if los>60:issues.append({'line':ln,'type':'warning','message':f'{id_}: LOS sangat panjang; periksa dokumen'})
        if cost>1:issues.append({'line':ln,'type':'warning','message':f'{id_}: Indeks biaya >1; pastikan skala indikator'})
        if not row.get('Hak_Kelas','').strip(): issues.append({'line':ln,'type':'warning','message':f'{id_}: hak kelas kosong'})
        rows.append({'id':id_,'kelas':str(row['Hak_Kelas']).strip(),'diagnosis':dx,'code':code,'ds':int(ds),'los':int(los),'biaya':round(cost,5)})
    return rows,{'input_rows':total,'accepted':len(rows),'rejected':total-len(rows),'issues':issues[:250],'issues_total':len(issues)}

def median(x):return statistics.median(x) if x else None

def analyze(rows):
    groups={}
    for r in rows:groups.setdefault(r['code'],[]).append(r)
    output=[]
    for r in rows:
        peers=[p for p in groups[r['code']] if p['id']!=r['id'] and p['kelas']==r['kelas']]
        tier='diagnosis + kelas'
        if len(peers)<3:
            peers=[p for p in groups[r['code']] if p['id']!=r['id']];tier='diagnosis saja (pembanding terbatas)'
        if len(peers)<3:peers=[];tier='tidak mencukupi'
        ref=median([p['los'] for p in peers]);delta=(r['los']-ref) if ref is not None else None
        # Demonstration heuristic, not clinical standard / fraud probability.
        los_component=min(45,max(0,(r['los']-4)*9))
        ds_component=min(25,r['ds']*5)
        cost_component=min(20,max(0,(r['biaya']-.6)*50))
        combined=10 if r['los']>4 and r['ds']>=3 else 0
        score=round(min(100,los_component+ds_component+cost_component+combined))
        risk='Tinggi' if score>=70 else ('Sedang' if score>=35 else 'Rendah')
        reasons=[]
        if los_component: reasons.append(f'LOS {r["los"]} hari melampaui ambang eksplorasi 4 hari (bukan batas klinis).')
        if ds_component: reasons.append(f'{r["ds"]} diagnosis sekunder tercatat; jenis dan bukti belum tersedia.')
        if cost_component: reasons.append(f'Indeks biaya {r["biaya"]:.2f} relatif tinggi pada skala dataset.')
        if combined:reasons.append('Kombinasi LOS dan jumlah diagnosis sekunder memicu pemeriksaan silang.')
        if not reasons:reasons=['Tidak ada pemicu utama menurut aturan demo.']
        if ref is not None:reasons.append(f'Median LOS {len(peers)} pembanding {tier}: {ref:g} hari; selisih {delta:+g} hari.')
        # 8x8 matrix preserves explicitly named feature blocks; normalized features repeated over tiles for visualization.
        feats=[min(1,r['los']/15),min(1,r['ds']/8),min(1,r['biaya']),min(1,score/100),min(1,max(0,r['los']-4)/10),min(1,r['ds']/10),1 if combined else 0,min(1,len(peers)/10)]
        fp=[]
        for y in range(8):
            for x in range(8): fp.append(round(max(0,min(1,feats[y]*(.55+.45*feats[x]))),3))
        out=dict(r,score=score,risk=risk,reasons=reasons,peer_count=len(peers),peer_median=ref,peer_delta=delta,peer_tier=tier,confidence='Pembanding tersedia (terbatas)' if peers else 'Rendah — pembanding tidak tersedia',fingerprint=fp,components={'LOS':round(los_component),'DxSek':round(ds_component),'Biaya':round(cost_component),'Kombinasi':combined})
        output.append(out)
    return sorted(output,key=lambda x:(-x['score'],x['id']))
