"""Reproduce a descriptive study of decision routes in four selected tournaments.
No external network needed. Three frozen canonical CSVs + 15 official EURO2020 rows.
"""
from pathlib import Path
import csv, hashlib, json, re, shutil
from collections import Counter, defaultdict

HERE=Path(__file__).resolve().parent
ROOT=HERE.parents[2]
HERE.mkdir(parents=True,exist_ok=True)
EURO20='https://www.uefa.com/uefaeuro/history/news/0254-0d41684d1216-06773df7faed-1000--uefa-euro-2020-fixtures-and-results/'
EURO24='https://www.uefa.com/news/0275-151eb1c333ea-d30deec67b13-1000--euro-2024-match-schedule/'
WC22='https://www.uefa.com/news/0274-14ce5c537085-08c45b3067c4-1000--2022-world-cup-results-all-the-european-sides-games-after-fr/'
AFC23='https://assets.the-afc.com/Corporate_Page/Technical/Technical_Reports/AFC-Asian-Cup-Qatar-2023%E2%84%A2-Technical-Report.pdf'
SEED=[
 ('world-cup-2022-knockout-results.csv','world_cup_2022','ЧМ2022',WC22,'FIFA/UEFA official results, canonical source reviewed2026-10-06; UEFA consolidated page re-read2026-10-10.'),
 ('euro-2024-knockout-results.csv','euro_2024','Евро2024',EURO24,'UEFA consolidated knockout results fully re-read2026-10-10.'),
 ('asian-cup-2023-knockout-results.csv','asian_cup_2023','КубокАзии2023',AFC23,'Actual AFC official technical PDF downloaded2026-10-10; physical PDF pages65–66 results and knockout reports.'),
]
STAGES={'1/8 финала':'round_of_16','1/4 финала':'quarter_final','1/2 финала':'semi_final','Финал':'final','Матч за 3-е место':'third_place'}
FIELDS=['match_id','tournament_id','tournament_label','date','stage','team_a','team_b','score_90','score_120','penalty_score','winner','decision_route','extra_time_played','included_main_sample','exclusion_reason','source_url','source_basis','verified_on']

def write_csv(name,rows,fields=None):
    fields=fields or list(rows[0])
    with (HERE/name).open('w',encoding='utf-8-sig',newline='') as f:
        w=csv.DictWriter(f,fieldnames=fields);w.writeheader();w.writerows(rows)

def null(x): return '' if x in ['—','–','-',None] else x
def pair(s): return tuple(map(int,s.split(':')))
rows=[]; inputs=[]
for filename,tid,label,url,basis in SEED:
    frozen=HERE/('input_'+filename)
    source=ROOT/'hugo/static/data/results'/filename
    if not frozen.exists(): shutil.copyfile(source,frozen)
    inputs.append(dict(file=frozen.name,sha256=hashlib.sha256(frozen.read_bytes()).hexdigest(),primary_url=url))
    with frozen.open(encoding='utf-8-sig',newline='') as f:
        for n,r in enumerate(csv.DictReader(f),1):
            score90=null(r['После 90 минут']);score120=null(r['После ДВ (120 минут)']);pens=null(r['Пенальти'])
            route='penalties' if pens else ('extra_time' if score120 else 'regulation')
            stage=STAGES[r['Стадия']]
            source_url=url
            if tid=='world_cup_2022' and r['Команда A']=='Аргентина' and r['Команда B']=='Австралия':
                source_url='https://www.fifa.com/en/articles/world-cup-qatar-2022-julian-alvarez-argentina-manchester-city-australia-messi'
            if tid=='world_cup_2022' and r['Команда A']=='Бразилия' and r['Команда B']=='Республика Корея':
                source_url='https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/articles/brazil-team-profile-history?searchOverlay=1'
            rows.append(dict(match_id=f'{tid}_{n:02d}',tournament_id=tid,tournament_label=label,date=r['Дата'],stage=stage,team_a=r['Команда A'],team_b=r['Команда B'],score_90=score90,score_120=score120,penalty_score=pens,winner=r['Победитель'],decision_route=route,extra_time_played=int(bool(score120)),included_main_sample=int(stage!='third_place'),exclusion_reason='Separate third-place match, absent from other three tournaments' if stage=='third_place' else '',source_url=source_url,source_basis=basis,verified_on='2026-10-10'))

# Independently transcribed from the official UEFA consolidated EURO2020 page,
# read2026-10-10. score90 intentionally blank for extra-time games: the consolidated
# source displays after-extra-time score. Do not reconstruct missing goals/minutes.
E20=[
 ('2021-06-26','round_of_16','Уэльс','Дания','0:4','','','Дания'),
 ('2021-06-26','round_of_16','Италия','Австрия','','2:1','','Италия'),
 ('2021-06-27','round_of_16','Нидерланды','Чехия','0:2','','','Чехия'),
 ('2021-06-27','round_of_16','Бельгия','Португалия','1:0','','','Бельгия'),
 ('2021-06-28','round_of_16','Хорватия','Испания','','3:5','','Испания'),
 ('2021-06-28','round_of_16','Франция','Швейцария','','3:3','4:5','Швейцария'),
 ('2021-06-29','round_of_16','Англия','Германия','2:0','','','Англия'),
 ('2021-06-29','round_of_16','Швеция','Украина','','1:2','','Украина'),
 ('2021-07-02','quarter_final','Швейцария','Испания','','1:1','1:3','Испания'),
 ('2021-07-02','quarter_final','Бельгия','Италия','1:2','','','Италия'),
 ('2021-07-03','quarter_final','Чехия','Дания','1:2','','','Дания'),
 ('2021-07-03','quarter_final','Украина','Англия','0:4','','','Англия'),
 ('2021-07-06','semi_final','Италия','Испания','','1:1','4:2','Италия'),
 ('2021-07-07','semi_final','Англия','Дания','','2:1','','Англия'),
 ('2021-07-11','final','Италия','Англия','','1:1','3:2','Италия'),
]
for n,(date,stage,a,b,s90,s120,pens,winner) in enumerate(E20,1):
    rows.append(dict(match_id=f'euro_2020_{n:02d}',tournament_id='euro_2020',tournament_label='Евро2020(игры2021)',date=date,stage=stage,team_a=a,team_b=b,score_90=s90,score_120=s120,penalty_score=pens,winner=winner,decision_route='penalties' if pens else ('extra_time' if s120 else 'regulation'),extra_time_played=int(bool(s120)),included_main_sample=1,exclusion_reason='',source_url=EURO20,source_basis='New manual transcription from official UEFA consolidated results. Unreported regulation scores in extra-time games retained blank.',verified_on='2026-10-10'))

main=[r for r in rows if r['included_main_sample']]
assert len(rows)==61 and len(main)==60
assert len({r['match_id'] for r in rows})==61
assert len({(r['tournament_id'],r['date'],r['team_a'],r['team_b']) for r in rows})==61
for r in rows:
    assert r['winner'] in [r['team_a'],r['team_b']]
    assert r['decision_route'] in ['regulation','extra_time','penalties']
    for k in ['score_90','score_120','penalty_score']:
        assert not r[k] or re.fullmatch(r'\d+:\d+',r[k]),r
    if r['decision_route']=='penalties':
        assert r['score_120'] and pair(r['score_120'])[0]==pair(r['score_120'])[1]
        p=pair(r['penalty_score']);assert p[0]!=p[1]
        assert r['winner']==(r['team_a'] if p[0]>p[1] else r['team_b']),r
    elif r['decision_route']=='regulation':
        p=pair(r['score_90']);assert p[0]!=p[1]
        assert r['winner']==(r['team_a'] if p[0]>p[1] else r['team_b']),r
    else:
        p=pair(r['score_120']);assert p[0]!=p[1]
        assert r['winner']==(r['team_a'] if p[0]>p[1] else r['team_b']),r
    if r['extra_time_played'] and r['score_90']:
        assert pair(r['score_90'])[0]==pair(r['score_90'])[1]
        assert all(x>=y for x,y in zip(pair(r['score_120']),pair(r['score_90'])))

def summary(label, group):
    c=Counter(r['decision_route'] for r in group);n=len(group);et=c['extra_time']+c['penalties']
    return dict(group=label,matches=n,decided_90=c['regulation'],decided_extra_time=c['extra_time'],decided_penalties=c['penalties'],extra_time_played=et,extra_time_share_pct=round(100*et/n,4),penalties_share_all_pct=round(100*c['penalties']/n,4),penalties_share_extra_time_pct=round(100*c['penalties']/et,4) if et else None)

by_tournament=[summary(tid,[r for r in main if r['tournament_id']==tid]) for tid in ['euro_2020','world_cup_2022','asian_cup_2023','euro_2024']]
by_stage=[summary(stage,[r for r in main if r['stage']==stage]) for stage in ['round_of_16','quarter_final','semi_final','final']]
overall=summary('all_main',main)
assert all(r['matches']==15 for r in by_tournament)
assert [x['matches'] for x in by_stage]==[32,16,8,4]
assert (overall['decided_90'],overall['decided_extra_time'],overall['decided_penalties'])==(37,7,16)
all_including_third=summary('sensitivity_include_third_place',rows)

write_csv('knockout_matches.csv',rows,FIELDS)
write_csv('knockout_by_tournament.csv',by_tournament)
write_csv('knockout_by_stage.csv',by_stage)
write_csv('knockout_penalties.csv',[r for r in main if r['decision_route']=='penalties'],FIELDS)
coverage=Counter((r['tournament_id'],r['source_basis']) for r in rows)
passport=dict(title='Как решаются60матчейплей-офф:90минут,дополнительное время,пенальти',study_type='Original reproducible descriptive desk analysis of selected official results',as_of='2026-10-10',observation='One national-team knockout match',source_rows=61,main_analysis_rows=60,excluded_rows=[r for r in rows if not r['included_main_sample']],period_start=min(r['date'] for r in rows),period_end=max(r['date'] for r in rows),tournaments=['UEFAEURO2020 played2021','FIFAWorldCup2022','AFCAsianCup2023 played2024','UEFAEURO2024'],source_inputs=inputs,newly_transcribed_rows=15,new_transcription_source=EURO20,fields=FIELDS,missing_values=dict(score_90_missing=8,meaning='EURO2020 consolidated page gives after-extra-time score. Missing regulation scores stay empty and are not used in analysis.'),definitions=dict(regulation='Winner decided in regulation incl added time',extra_time='Extra time played, winner decided without shoot-out',penalties='Match stayed tied after extra time and winner decided in shoot-out',extra_time_played='Sum(extra_time,penalties); these are not independent outcomes',score_columns='team_a:team_b; score120 is cumulative, not extra-time-only; shoot-out never added to game goals'),formulae=dict(route_share='count(route)/count(included matches)',conditional_penalty_share='count(penalties)/count(extra_time_played)',rounding='Percentages calculated from exact integer counts, stored4decimals; presentation1decimal'),limits=['Purposive four-tournament sample, not all football and not random.','Each tournament contributes one edition only; no trend or causal claim.','No ranking, expected-goals, possession, travel, injury or fatigue inputs. Cannot explain why route differs.','Repeated national teams mean rows are not statistically independent; do not present universal probability or confidence intervals.','90/120 are regulation-period categories, not observed wall-clockduration including addedtime.','All four main samples have15matches and stages8+4+2+1; WorldCup third-place match excluded explicitly.','No club two-leg ties, youth, women, futsal or penalty-only competitions included.','MissingEURO2020 score90 retained; no invented goal timings.','This adds a new classification and derived indicators to existing canonical results; it is not a new sporting experiment.'],primary_sources=[EURO20,EURO24,WC22,AFC23],copyright_note='Store only factual match results and original calculations in publishableCSV. Captured primary HTML/PDF evidence is internal and must not be copied into site assets.')
goal_totals={tid:sum(sum(pair(r['score_120'] or r['score_90'])) for r in main if r['tournament_id']==tid) for tid in ['euro_2020','world_cup_2022','asian_cup_2023','euro_2024']}
shootout_scored=sum(sum(pair(r['penalty_score'])) for r in main if r['penalty_score'])
assert sum(goal_totals.values())==178
result=dict(passport=passport,overall=overall,by_tournament=by_tournament,by_stage=by_stage,goal_totals_after_play=goal_totals,total_goals_after_play=sum(goal_totals.values()),shootout_scored_separate_audit=shootout_scored,sensitivity_including_third_place=all_including_third,findings=[dict(claim='Дополнительное время понадобилось23из60матчей(38,3%).',numerator=23,denominator=60),dict(claim='Пенальти решили16из60матчей(26,7%) и16из23дополнительных периодов(69,6%).',numerator=16,denominators=[60,23]),dict(claim='Евро2020:8из15сдополнительным временем; в каждомдругомтурнире5из15.',counts=[8,5,5,5],denominator_per_tournament=15),dict(claim='Вчетвертьфиналах8из16потребовалидополнительного времени,6из16—пенальти.',numerators=[8,6],denominator=16)],chart_specs=[dict(id='decision-route-tournament',type='stacked_bar',data='knockout_by_tournament.csv',series=['decided_90','decided_extra_time','decided_penalties'],denominator='15matches each',note='Extra_time series excludes shootouts to avoid overlap.'),dict(id='extra-time-by-stage',type='bar',data='knockout_by_stage.csv',series=['extra_time_share_pct','penalties_share_all_pct'],denominator='Stage counts32,16,8,4; show n next to labels',note='Shares overlap: shootout is subset of extra_time_played.'),dict(id='conditional-denominator',type='comparison_cards',values=[dict(numerator=16,denominator=60,label='All knockout matches'),dict(numerator=16,denominator=23,label='Matches that went to extra time')],note='Same numerator, different denominator.'),dict(id='route-overall',type='donut',values=[37,7,16],labels=['90minutes','extra_time_without_shootout','shootout'],denominator=60)],related_urls=['/seriya-penalti/','/dva-matcha-dopolnitelnoe-vremya/','/stats-world-cup-2022-knockout-results/','/stats-euro-2024-knockout-results/','/stats-asian-cup-2023-knockout-results/','/instrumenty/tournament-bracket/','/pochemu-razgrom-ne-govorit-o-klasse/'])
(HERE/'knockout_analysis.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
(HERE/'knockout_qa.json').write_text(json.dumps(dict(status='pass',source_rows=61,analysis_rows=60,duplicates=0,inconsistent_winners=0,route_partition_valid=True,per_tournament15=True,stage_counts=[32,16,8,4],missing_regulation_scores=8,dataset_sha256=hashlib.sha256((HERE/'knockout_matches.csv').read_bytes()).hexdigest()),ensure_ascii=False,indent=2),encoding='utf-8')
pack_path=HERE.parent/'sports_factpack.json'
pack=json.loads(pack_path.read_text(encoding='utf-8'));pack['study']=dict(key='football-knockout-decision-routes',status='verified',dataset='research_data/knockout_matches.csv',analysis='research_data/knockout_analysis.json',qa='research_data/knockout_qa.json',reproduction_script='research_data/analyze_knockout.py',source_rows=61,analysis_rows=60,overall=overall,original_contribution='Cross-tournament classification, equal15match denominators, stage breakdown, conditional shootout denominator and sensitivity analysis.')
pack_path.write_text(json.dumps(pack,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps(dict(overall=overall,by_tournament=by_tournament,by_stage=by_stage,sensitivity=all_including_third),ensure_ascii=False,indent=2))
