"""Read-only N1 audit; standard library only. Rebuild with python3 analiz/eredivisie/lig_analiz.py."""
from pathlib import Path
import sqlite3, csv, json, math, statistics, hashlib
from collections import Counter, defaultdict
ROOT=Path(__file__).resolve().parents[2]; OUT=Path(__file__).resolve().parent
CATS=['1/2','2/1','1/X','2/X','X/1','X/2','X/X']
HB=[1,1.5,1.8,2.2,2.8,4,6,1000]; DB=[1,3.2,3.6,4,4.5,5.5,1000]; AB=HB

def dump(name,rows):
 if not rows:
  (OUT/name).write_text('id,reason\n',encoding='utf-8-sig');return
 with (OUT/name).open('w',encoding='utf-8-sig',newline='') as f:
  w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
def ci(k,n):
 if not n:return (0,1)
 p=k/n;z=1.95996398454;den=1+z*z/n
 c=(p+z*z/(2*n))/den;r=z*math.sqrt(p*(1-p)/n+z*z/(4*n*n))/den
 return c-r,c+r
def fisher(k,n,K,N):
 # One-sided hypergeometric enrichment against the disjoint league complement.
 lo=max(k,0,K-(N-n));hi=min(n,K)
 def lc(a,b):return math.lgamma(a+1)-math.lgamma(b+1)-math.lgamma(a-b+1)
 logs=[lc(K,x)+lc(N-K,n-x)-lc(N,n) for x in range(lo,hi+1)]
 if not logs:return 1.
 mx=max(logs);return min(1.,math.exp(mx)*sum(math.exp(v-mx) for v in logs))
def holm(rows):
 running=0
 for i,r in enumerate(sorted(rows,key=lambda x:x['p'])):
  running=max(running,r['p']*(len(rows)-i));r['p_holm']=min(1.,running)
def result(h,a):return '1' if h>a else '2' if h<a else 'X'
def bucket(x,b):return next((i for i in range(len(b)-1) if b[i]<=x<b[i+1]),-1)
def profile(key):
 labels=[]
 for name,b,i in zip(['MS1','MSX','MS2'],[HB,DB,AB],key):
  lo=f'{b[i]:.2f}'.replace('.',',');hi=f'{b[i+1]:.2f}'.replace('.',',')
  labels.append(f'{name} ≥ {lo}' if b[i+1]==1000 else f'{lo} ≤ {name} < {hi}')
 return ' · '.join(labels)
def pct(k,n):return f'{100*k/n:.2f}%' if n else '—'
def med(v):return statistics.median(v) if v else 0

def main():
 conn=sqlite3.connect(f'file:{ROOT / "football_data_v1.db"}?mode=ro',uri=True);conn.row_factory=sqlite3.Row
 raw=[dict(r) for r in conn.execute("select rowid as id,Date,Season,HomeTeam,AwayTeam,FTHG,FTAG,HTHG,HTAG,FTR,HTR,B365H,B365D,B365A from matches where Div='N1'")];conn.close()
 seen=set();valid=[];a=[];bad=[];mismatch=0;dups=0
 for r in raw:
  key=(r['Date'],r['HomeTeam'],r['AwayTeam'])
  if key in seen:dups+=1;bad.append(dict(r,reason='duplicate'));continue
  seen.add(key)
  scores=[r[c] for c in ['FTHG','FTAG','HTHG','HTAG']]
  if not all(isinstance(x,(int,float)) and math.isfinite(x) and x>=0 and x==int(x) for x in scores):bad.append(dict(r,reason='missing_or_invalid_score'));continue
  fh,fa,hh,ha=scores
  if hh>fh or ha>fa:bad.append(dict(r,reason='HT_exceeds_FT'));continue
  if r['Date']>'2026-10-25':bad.append(dict(r,reason='future'));continue
  r['HTFT']=result(hh,ha)+'/'+result(fh,fa)
  mismatch+=int(r['HTR']!={'1':'H','X':'D','2':'A'}[r['HTFT'][0]] or r['FTR']!={'1':'H','X':'D','2':'A'}[r['HTFT'][-1]])
  valid.append(r)
  odds=[r[c] for c in ['B365H','B365D','B365A']]
  if not all(isinstance(x,(int,float)) and math.isfinite(x) and x>1 for x in odds):continue
  r['key']=tuple(bucket(x,b) for x,b in zip(odds,[HB,DB,AB]))
  if -1 in r['key']:continue
  r['part']='discovery' if r['Date']<'2020-07-01' else 'validation' if r['Date']<'2023-07-01' else 'test'
  a.append(r)
 parts={p:[r for r in a if r['part']==p] for p in ['discovery','validation','test']}
 counts=Counter(r['HTFT'] for r in a);grid=defaultdict(list)
 for r in a:grid[r['key']].append(r)
 profiles=[]
 for key,g in sorted(grid.items()):
  for cat in CATS:
   row={'cat':cat,'profile_MS1_MSX_MS2':profile(key),'n':len(g),'k':sum(r['HTFT']==cat for r in g)}
   row['rate']=row['k']/row['n'];row['ci_low'],row['ci_high']=ci(row['k'],row['n'])
   for p in parts:
    sub=[r for r in g if r['part']==p];row[p+'_n']=len(sub);row[p+'_k']=sum(r['HTFT']==cat for r in sub)
   row['key']=key;profiles.append(row)
 selected=[]
 for cat in CATS:
  candidates=[r for r in profiles if r['cat']==cat and r['discovery_n']>=100 and r['discovery_k']>=5]
  if not candidates:continue
  chosen=max(candidates,key=lambda r:ci(r['discovery_k'],r['discovery_n'])[0]);chosen=chosen.copy()
  vn,vk=chosen['validation_n'],chosen['validation_k'];tn,tk=chosen['test_n'],chosen['test_k']
  vb=sum(r['HTFT']==cat for r in parts['validation'])/len(parts['validation'])
  chosen['validation_pass']=vn>=30 and vk>=3 and vk/vn>vb
  K=sum(r['HTFT']==cat for r in parts['test']);N=len(parts['test'])
  chosen['p']=fisher(tk,tn,K,N) if tn else 1;chosen['test_baseline']=K/N
  selected.append(chosen)
 holm(selected)
 for r in selected:r['supported']=r['validation_pass'] and r['test_n']>=30 and r['test_k']/r['test_n']>r['test_baseline'] and r['p_holm']<.05
 # Role-specific team frequencies. Every eligible test is included in one Holm family.
 teams=[]
 for side,col in [('Ev','HomeTeam'),('Deplasman','AwayTeam')]:
  groups=defaultdict(list)
  for r in a:groups[r[col]].append(r)
  for team,g in groups.items():
   if len(g)<80:continue
   for cat in CATS:
    n=len(g);k=sum(r['HTFT']==cat for r in g);N=len(a);K=counts[cat]
    # Descriptive odds adjustment: leave-team-out rate in the same 3D odds cell.
    expected=0; cell_counts=Counter(match['key'] for match in g)
    for cell, multiplicity in cell_counts.items():
     peers=[r for r in grid[cell] if r[col]!=team]
     expected+=multiplicity*(sum(r['HTFT']==cat for r in peers)+20*K/N)/(len(peers)+20)
    row=dict(cat=cat,role=side,team=team,n=n,k=k,rate=k/n,expected=expected,observed_expected=k/expected if expected else 0,p=fisher(k,n,K,N))
    row['ci_low'],row['ci_high']=ci(k,n)
    for p in parts:
     sub=[r for r in g if r['part']==p];row[p+'_n']=len(sub);row[p+'_k']=sum(r['HTFT']==cat for r in sub)
    teams.append(row)
 holm(teams)
 for r in teams:
  tbase=sum(m['HTFT']==r['cat'] for m in parts['test'])/len(parts['test'])
  r['recent_repeat']=r['test_n']>=30 and r['test_k']>=3 and r['test_k']/r['test_n']>tbase
  r['historical_flag']=r['p_holm']<.05 and r['observed_expected']>1.1
 dump('oran_profilleri.csv',[{k:v for k,v in r.items() if k!='key'} for r in profiles]);dump('takim_tum_sonuclar.csv',teams)
 dump('hedef_maclar.csv',[{k:v for k,v in r.items() if k!='key'} for r in a if r['HTFT'] in CATS]);dump('dislanan_kayitlar.csv',bad)
 seasonal=[]
 for season in sorted({r['Season'] for r in raw}):
  rr=[r for r in raw if r['Season']==season];gg=[r for r in a if r['Season']==season]
  seasonal.append(dict(season=season,raw_n=len(rr),odds_valid_n=len(gg),**{cat:sum(r['HTFT']==cat for r in gg) for cat in CATS}))
 dump('sezon_kapsami.csv',seasonal)
 report=[]
 def add(s=''):report.append(s)
 add('# Hollanda — Eredivisie: MS oranları ile İY/MS analizi');add('\nHazırlanma: 25 Ekim 2026. Yalnızca projedeki yerel veriler incelendi; dış kaynak doğrulaması yapılmadı. Diğer ligler bu rapora dahil edilmedi.\n')
 add('## Kaynak ve veri denetimi\n')
 add(f'- Ana kaynak: `football_data_v1.db`, `matches`, `Div=N1`. Ham kayıt: **{len(raw):,}**; geçerli İY/MS: **{len(valid):,}**; üç MS oranı da geçerli analiz örneklemi: **{len(a):,}**. Geçerli skoru olup oranı eksik/geçersiz: {len(valid)-len(a):,}.')
 add(f'- Oran örneklemi tarihleri: **{min(r["Date"] for r in a)} — {max(r["Date"] for r in a)}**. Skor verisi: {min(r["Date"] for r in valid)} — {max(r["Date"] for r in valid)}.')
 add(f'- Tekrarlanan tarih/ev/deplasman: {dups}; sonuç etiketi ile skordan hesaplanan sonuç uyuşmazlığı: {mismatch}. Sonuçlar tam sayı, negatif olmayan skorlarla yeniden hesaplandı; İY skoru MS skorunu aşan kayıtlar dışlandı.')
 add('- Her iki DB’nin lig ve İY skoru kapsamı kontrol edildi. Bu lig için uzun geçmişli, sağlayıcısı belirli B365 oranlarını içeren ilk DB kullanıldı. İkinci DB daha kısa kapsamlıdır veya bu ligi içermez; kaynaklar birleştirilmedi.')
 add('- Tek oran ailesi: **B365H / B365D / B365A**, sırasıyla MS1 / MSX / MS2. Kapanış alanları B365CH/CD/CA kullanılmadı. DB tek başına kesin fiyat alma zamanını kanıtlamaz; bunlara kesin açılış oranı denmiyor.')
 add('- Sezonlar eşit kapsamlı kabul edilmedi. Sezon bazında ham kayıt, geçerli oran ve hedef sonuç adetleri `sezon_kapsami.csv` içinde verildi; eksik veriler tamamlanmadı. Son DB tarihi bugünden öncedir; sonuçlar yerel tarihsel veriye dayanır, dış kaynak doğrulaması yapılmadı.')
 add('- `1` ev sahibi, `2` deplasman, `X` beraberliktir. **X/X dahildir.** 1/1 ve 2/2 hedef sonuçlardan çıkarıldı; yüzdelerin paydasında tüm geçerli maçlar tutuldu. “Mimli” yalnızca istatistiksel tekrar demektir, usulsüzlük iddiası değildir.\n')
 add('## Yedi sonucun sıklığı\n');add('| İY/MS | Adet / tüm maç | Sıklık | %95 Wilson aralığı | MS1 / MSX / MS2 medyanı |');add('|---|---:|---:|---|---|')
 for cat in CATS:
  g=[r for r in a if r['HTFT']==cat];k=len(g);lo,hi=ci(k,len(a));mm=' / '.join(f'{med([r[c] for r in g]):.2f}' for c in ['B365H','B365D','B365A'])
  add(f'| {cat} | {k} / {len(a)} | {pct(k,len(a))} | %{100*lo:.2f}–{100*hi:.2f} | {mm} |')
 add(f'\nYedi hedefin toplamı: {sum(counts[c] for c in CATS)} / {len(a)} ({pct(sum(counts[c] for c in CATS),len(a))}). Hariç tutulanlar: 1/1={counts["1/1"]}, 2/2={counts["2/2"]}. Medyanlar olay gerçekleşmiş maçları tarif eder; seçim kuralı değildir.\n')
 add('## Hangi oranlarda hangi sonuç görüldü?\n')
 add('Önceden tanımlı, çakışmayan üçlü oran hücreleri kullanıldı. Oranlar MS1, MSX ve MS2 adıyla yazılır. Örneğin “1,50 ≤ MS1 < 1,80”, 1,50 dahil ve 1,80 hariç demektir; “MS2 ≥ 6,00” ise 6,00 ve üzeridir. Her sonuç için en az 100 maçı olan hücrelerden en yüksek tarihsel sıklıklı üçü aşağıdadır. Bunlar keşif bulgularıdır; sonuç görüldükten sonra sıralandıkları için tahmin gücü kanıtı sayılmaz. Tam hücre dökümü ekte.\n')
 for cat in CATS:
  add(f'\n### {cat}\n');add('| MS1 / MSX / MS2 | Olay / maç | Sıklık | Lig ortalamasına oran |');add('|---|---:|---:|---:|')
  top=sorted([r for r in profiles if r['cat']==cat and r['n']>=100],key=lambda r:-r['rate'])[:3]
  for r in top:add(f'| {r["profile_MS1_MSX_MS2"]} | {r["k"]}/{r["n"]} | {pct(r["k"],r["n"])} | {r["rate"]/(counts[cat]/len(a)):.2f}× |')
 add('\n## Zaman ayrımında sınama\n')
 add('Keşif: 1 Temmuz 2020 öncesi; doğrulama: 1 Temmuz 2020–30 Haziran 2023; test: 1 Temmuz 2023 sonrası. Her sonuç için yalnız keşifte en az 100 maç/5 olay içeren hücreler arasından Wilson alt sınırı en yüksek tek hücre donduruldu. Doğrulama: ≥30 maç, ≥3 olay ve dönem lig ortalamasından yüksek oran. Testte ≥30 maç, artış ve yedi karşılaştırmaya Holm düzeltmeli tek yönlü Fisher p<0,05 arandı. Test dönemi hücre seçiminde kullanılmadı. Bu geniş hücre taraması dar oranların tüm olası analizlerini temsil etmez.\n')
 add('| Sonuç | MS1 / MSX / MS2 | Keşif | Doğrulama | Test | Test lig oranı | Holm p | Destek |');add('|---|---|---:|---:|---:|---:|---:|---|')
 for missing_cat in [cat for cat in CATS if cat not in {r['cat'] for r in selected}]:
  add(f'| {missing_cat} | Keşifte yeterli örneklemli aday yok | — | — | — | — | — | Sınanamadı |')
 for r in selected:
  s=[f'{r[p+"_k"]}/{r[p+"_n"]} ({pct(r[p+"_k"],r[p+"_n"])})' for p in parts]
  add(f'| {r["cat"]} | {r["profile_MS1_MSX_MS2"]} | '+ ' | '.join(s)+f' | %{100*r["test_baseline"]:.2f} | {r["p_holm"]:.4f} | {"Var" if r["supported"] else "Yok"} |')
 add('\nBu test yalnız tüm maçlara göre zenginleşmeyi ölçer; 1-X-2 fiyatlarının ötesinde ek tahmin gücünü veya bahis kârlılığını kanıtlamaz. İY/MS bahis oranları mevcut analizde olmadığı için getiri/ROI hesaplanamaz.\n')
 add('## Takımlar: hangi rolde, hangi sonuçta tekrar var?\n')
 add(f'En az 80 maçlık ev veya deplasman rolü ayrı değerlendirildi. Sıralama Wilson alt sınırına göredir. Her satırdaki yüzde takımın o roldeki **tüm** maçlarına göredir. Ham sayının tek başına uzun süre ligde kalmayı ödüllendirmesi önlendi. {len(teams)} takım/rol/sonuç testi tek Holm ailesinde düzeltildi.\n')
 add('“Orana göre O/B”: gözlenen olay / aynı üçlü oran hücrelerindeki diğer takım maçlarından beklenen olay. Beklenen hesapta 20 maç ağırlığında lig ortalamasına yumuşatma kullanıldı. Bu, dönem/rakip/kadro etkilerini kontrol eden bir model veya bağımsız anlamlılık testi değildir. “Tarihsel işaret” için düzeltilmiş ham frekans p<0,05 ve O/B>1,10 şartı var; “yakın tekrar” test döneminde ≥30 maç, ≥3 olay ve dönem lig ortalamasının üstünü ifade eder. Yakın tekrar bağımsız doğrulama sayılmaz; takım seçimi tüm geçmişten yapılmıştır.\n')
 for cat in CATS:
  add(f'\n### {cat} — öne çıkan takım/roller\n');add('| Takım | Rol | Olay / maç | Sıklık (%95 aralık) | Orana göre O/B | Holm p | Test dönemi | İşaret / yakın tekrar |');add('|---|---|---:|---|---:|---:|---|---|')
  rr=sorted([r for r in teams if r['cat']==cat and r['k']>=5],key=lambda r:-r['ci_low'])[:6]
  for r in rr:
   add(f'| {r["team"]} | {r["role"]} | {r["k"]}/{r["n"]} | %{100*r["rate"]:.2f} (%{100*r["ci_low"]:.2f}–{100*r["ci_high"]:.2f}) | {r["observed_expected"]:.2f}× | {r["p_holm"]:.4f} | {r["test_k"]}/{r["test_n"]} | {"Var" if r["historical_flag"] else "Yok"} / {"Var" if r["recent_repeat"] else "Yok"} |')
 add('\n## Takım rolünü okuma\n')
 add('- 1/2: ev sahibi önde girdiği maçı kaybetti; deplasman geriden kazandı. 2/1 bunun tersidir.\n- 1/X: ev sahibi üstünlüğünü kaybetti; deplasman geriden beraberlik aldı. 2/X bunun tersidir.\n- X/1: ev sahibi ikinci yarıda kazandı; X/2: deplasman ikinci yarıda kazandı. X/X: hem devre hem maç sonucu beraberlik.\n- Tablolardaki aynı sonuç iki takımın farklı hikâyesini anlatır; takımlar birleştirilerek “geri dönüşçü” diye etiketlenmedi.\n')
 add('## Kısa değerlendirme\n')
 supported=[r['cat'] for r in selected if r['supported']]
 add('Zaman ayrımlı oran testinden geçen sonuçlar: **'+(', '.join(supported) if supported else 'yok')+'**. Geçmeyen profiller tarihsel gözlem olarak kalır.\n')
 for cat in CATS:
  flags=sorted([r for r in teams if r['cat']==cat and r['historical_flag']],key=lambda r:-r['ci_low'])
  add(f'- **{cat}:** '+('; '.join(f'{r["team"]} ({r["role"]}, {r["k"]}/{r["n"]}, O/B {r["observed_expected"]:.2f}×, yakın tekrar {"var" if r["recent_repeat"] else "yok"})' for r in flags[:5]) if flags else 'katı tarihsel işaret ölçütünü karşılayan takım/rol yok.'))
 add('\nSeyrek 1/2 ve 2/1 sonuçlarında yüksek görünen yüzdeler birkaç maçla değişebilir. Geçmişte tekrar eden takım davranışı bugünkü kadro/teknik direktör için otomatik taşınmaz. Bu rapor maç seçimi veya kazanç garantisi değildir.\n')
 add('## Denetim dosyaları\n')
 for name,desc in [('oran_profilleri.csv','Tüm üçlü oran hücreleri ve yedi sonuç; dönem sayımları'),('takim_tum_sonuclar.csv','Tüm uygun takım/rol/sonuçlar; güven aralıkları ve düzeltmeler'),('hedef_maclar.csv','Yedi hedefin maç bazında tarih, takımlar, skorlar ve MS oranları'),('dislanan_kayitlar.csv','Skor/tekrar/tarih nedeniyle dışlanan kayıtlar'),('sezon_kapsami.csv','Sezon bazında kapsam ve sonuç sayımları'),('lig_analiz.py','Tekrar üretim kodu'),('denetim.json','Kaynak özeti ve test seçimleri')]:add(f'- [{name}]({OUT / name}): {desc}.')
 (OUT/'Rapor.md').write_text('\n'.join(report)+'\n',encoding='utf-8')
 audit=dict(raw=len(raw),valid_scores=len(valid),valid_odds=len(a),duplicates=dups,mismatches=mismatch,excluded=len(bad),parts={p:len(g) for p,g in parts.items()},counts=dict(counts),selected=selected,team_tests=len(teams),source_sha256=hashlib.file_digest((ROOT/'football_data_v1.db').open('rb'),'sha256').hexdigest())
 (OUT/'denetim.json').write_text(json.dumps(audit,ensure_ascii=False,indent=2),encoding='utf-8')
 assert len(a)==sum(counts.values())
 assert sum(r['n'] for r in profiles if r['cat']==CATS[0])==len(a)
 assert len(valid)+len(bad)==len(raw)
 print(json.dumps({k:v for k,v in audit.items() if k not in ['selected','source_sha256']},ensure_ascii=False,indent=2))
 print('SUPPORTED',supported)
 for cat in CATS:
  rr=sorted([r for r in teams if r['cat']==cat],key=lambda r:-r['ci_low'])[:3]
  print(cat,[(r['team'],r['role'],r['k'],r['n'],round(r['p_holm'],4),round(r['observed_expected'],2)) for r in rr])
if __name__=='__main__':main()
