#!/usr/bin/env python3
"""Reproduce the V5 technical export counts. No network access, no inference of outcomes.

Usage:
    python analyse_csv.py --input export.csv --output results
Python 3.9+; standard library only. The source file is never changed.
"""
from __future__ import annotations
import argparse
import collections
import csv
import hashlib
import io
import json
from datetime import datetime
from pathlib import Path

EXPECTED_HASH = 'b06ec9a54325bd4221b0869f9b9e34b0fcd1f36fc7e1a469d6e9455fc3a5854b'
REQUIRED = ['Beschwerde-ID','Link zur Meldung','Beschwerdedatum','Sender','Medium',
            'Titel der Sendung','Ausstrahlung','Beschwerdetext','Vorwuerfe (Kategorien)',
            'Anzahl geteilt','Rundfunkrat informiert (Anzahl)','Links zur Programmbeschwerde']


def number(row: dict[str, str], field: str) -> int:
    text = row[field].strip()
    try:
        n = int(text) if text else 0
    except ValueError as exc:
        raise ValueError(f"Invalid integer in {field}, ID={row['Beschwerde-ID']!r}: {text!r}") from exc
    if n < 0:
        raise ValueError(f"Negative count in {field}, ID={row['Beschwerde-ID']!r}")
    return n


def analyse(path: Path) -> tuple[dict, dict]:
    raw = path.read_bytes()
    reader = csv.DictReader(io.StringIO(raw.decode('utf-8-sig')), delimiter=';')
    fields = reader.fieldnames or []
    missing = [f for f in REQUIRED if f not in fields]
    if missing:
        raise ValueError('Missing required fields: ' + ', '.join(missing))
    rows = list(reader)
    if not rows:
        raise ValueError('No data rows found')
    months = collections.defaultdict(lambda: [0,0,0])
    senders = collections.defaultdict(lambda: [0,0,0])
    media = collections.Counter()
    categories = collections.Counter()
    anomaly = []
    texts = collections.Counter()
    blank_air = 0
    later_air = []
    links = 0
    for r in rows:
        if any(v is None for v in r.values()):
            raise ValueError(f"Incomplete CSV row near ID={r.get('Beschwerde-ID')!r}")
        when = datetime.strptime(r['Beschwerdedatum'], '%d.%m.%Y %H:%M')
        for group in [months[when.strftime('%Y-%m')], senders[r['Sender'] or 'unbekannt']]:
            group[0] += 1
            group[1] += number(r,'Anzahl geteilt')
            group[2] += number(r,'Rundfunkrat informiert (Anzahl)')
        media[r['Medium'] or 'unbekannt'] += 1
        categories.update(c.strip() for c in r['Vorwuerfe (Kategorien)'].split(',') if c.strip())
        texts[r['Beschwerdetext'].strip()] += 1
        links += bool(r['Links zur Programmbeschwerde'].strip())
        air = r['Ausstrahlung'].strip()
        if not air:
            blank_air += 1
        try:
            aired = datetime.strptime(air,'%d.%m.%Y %H:%M')
            if aired > when:
                later_air.append(r['Beschwerde-ID'])
                anomaly.append({'id':r['Beschwerde-ID'],'type':'airing_after_complaint','days':(aired-when).days})
        except ValueError:
            anomaly.append({'id':r['Beschwerde-ID'],'type':'airing_unparsed'})
    filters = {
      'ZDF_2026_02_15': lambda r:r['Sender']=='ZDF' and r['Ausstrahlung'].startswith('15.02.2026') and ('journal' in r['Titel der Sendung'].lower() or 'ice' in r['Beschwerdetext'].lower()),
      'ZDF_Schlagabtausch_2025_02_06': lambda r:r['Sender']=='ZDF' and 'schlagabtausch' in r['Titel der Sendung'].lower() and r['Ausstrahlung'].startswith('06.02.2025'),
      'Nuhr_2025_05_29': lambda r:'nuhr' in r['Titel der Sendung'].lower() and r['Ausstrahlung'].startswith('29.05.2025'),
    }
    selected_fields=['Beschwerde-ID','Beschwerdedatum','Sender','Titel der Sendung','Ausstrahlung','Anzahl geteilt','Rundfunkrat informiert (Anzahl)','Link zur Meldung']
    clusters={}
    for name,predicate in filters.items():
        rr=[r for r in rows if predicate(r)]
        clusters[name]={'n':len(rr),'shares':sum(number(r,'Anzahl geteilt') for r in rr),'council':sum(number(r,'Rundfunkrat informiert (Anzahl)') for r in rr),'entries':[{f:r[f] for f in selected_fields} for r in rr]}
    result={'sha256':hashlib.sha256(raw).hexdigest(),'n':len(rows),'distinct_ids':len({r['Beschwerde-ID'] for r in rows}),
            'shares':sum(number(r,'Anzahl geteilt') for r in rows),'council':sum(number(r,'Rundfunkrat informiert (Anzahl)') for r in rows),
            'council_cases':sum(number(r,'Rundfunkrat informiert (Anzahl)')>0 for r in rows),'media':dict(media),'senders':dict(sorted(senders.items())),
            'months':dict(sorted(months.items())),'categories':dict(categories.most_common()),'anomalies':anomaly,'clusters':clusters,'fields':fields}
    quality={'empty_broadcast_dates':blank_air,'postcomplaint_broadcast_dates':later_air,'identical_text_groups':sum(n>1 for t,n in texts.items()),
             'identical_text_additional_rows':sum(n-1 for t,n in texts.items() if n>1),'empty_complaint_text':texts.get('',0),'links_present':links}
    return result,quality


def main() -> int:
    ap=argparse.ArgumentParser(description=__doc__)
    ap.add_argument('--input',required=True,type=Path)
    ap.add_argument('--output',required=True,type=Path)
    args=ap.parse_args()
    try:
        result,quality=analyse(args.input)
        args.output.mkdir(parents=True,exist_ok=True)
        (args.output/'auswertung_v5.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
        (args.output/'qualitaet_v5.json').write_text(json.dumps(quality,ensure_ascii=False,indent=2),encoding='utf-8')
        print(f"Rows={result['n']}; shares={result['shares']}; council={result['council']}")
        print('SHA-256: '+result['sha256'])
        print('Matches documented V5 source: '+str(result['sha256']==EXPECTED_HASH))
        return 0
    except (OSError,UnicodeError,csv.Error,ValueError) as exc:
        ap.exit(2,'Analysis failed: '+str(exc)+'\n')

if __name__ == '__main__':
    raise SystemExit(main())
