#!/usr/bin/env python3
"""Macht aus den Rohdaten eines Laufs eine kompakte Datei fuer die Ergebnisseite.

    auswerten.py [ordner]        # Vorgabe: das Verzeichnis dieses Skripts

Gelesen werden:
    lauf/stufe-<n>.json   was k6 je Laststufe gemessen hat
    lauf/fenster.jsonl    wann welche Stufe lief
    verlauf.jsonl         was die Server in derselben Zeit gemeldet haben

Geschrieben wird ergebnis-kompakt.json - eine Datei, die eine Seite direkt
einbetten kann, ohne die Rohdaten (Megabytes) mitzuschleppen.

Die Wand wird nicht geraten, sondern nach zwei Regeln bestimmt:
  * die erste Stufe, auf der die Fehlerquote 1 % uebersteigt, ODER
  * die erste Stufe, auf der eine Artikelseite laenger als 2 s braucht
Die niedrigere von beiden gilt.
"""
import json
import re
import sys
from pathlib import Path

FEHLER_GRENZE = 0.005      # ab 0,5 % Fehlern ist es vorbei
SEITE_GRENZE = 2000        # ms fuer die Suche - ab hier springen Besucher ab
BESTELLUNG_GRENZE = 3000   # ms fuer die Bestellung - laenger haelt kaum jemand aus

SCHRITTE = [
    ('rz_1_startseite',   'Startseite',    True),
    ('rz_2_kategorie',    'Kategorie',     True),
    ('rz_3_suche',        'Suche',         False),
    ('rz_4_artikelseite', 'Artikelseite',  True),
    ('rz_5_anmelden',     'Anmelden',      False),
    ('rz_6_in_den_korb',  'In den Korb',   False),
    ('rz_7_warenkorb',    'Warenkorb',     False),
    ('rz_8_bestaetigung', 'Bestaetigung',  False),
    ('rz_9_bestellung',   'Bestellung',    False),
]


def wert(metriken: dict, name: str, feld: str):
    m = metriken.get(name) or {}
    v = (m.get('values') or {}).get(feld)
    if not isinstance(v, (int, float)):
        return None
    # NICHT pauschal runden: eine Fehlerquote von 0,0074 wurde dadurch zu
    # 0,0 - und die Auswertung meldete "keine Fehler", waehrend k6 selbst
    # 0,74 Prozent ausgewiesen hat. Zeiten in Millisekunden vertragen eine
    # Nachkommastelle, Quoten brauchen vier.
    return round(v, 1) if abs(v) >= 1 else round(v, 6)


def stufe_lesen(datei: Path) -> dict:
    d = json.loads(datei.read_text())
    m = d.get('metrics', {})
    vus = int(re.search(r'stufe-(\d+)', datei.name).group(1))
    return {
        'vus': vus,
        'schritte': {
            name: {
                'p50': wert(m, schluessel, 'med'),
                'p95': wert(m, schluessel, 'p(95)'),
                'anzahl': wert(m, schluessel, 'count'),
            }
            for schluessel, name, _ in SCHRITTE
        },
        'bestellungen': int(wert(m, 'rz_bestellungen', 'count') or 0),
        'fehlerquote': round((wert(m, 'rz_fehler', 'rate') or 0), 4),
        'anfragen': int(wert(m, 'http_reqs', 'count') or 0),
        'anfragenJeSekunde': round(wert(m, 'http_reqs', 'rate') or 0, 1),
        'durchlaeufe': int(wert(m, 'iterations', 'count') or 0),
    }


def server_je_stufe(ordner: Path, fenster: list) -> dict:
    datei = ordner / 'verlauf.jsonl'
    if not datei.exists():
        return {}
    punkte = [json.loads(z) for z in datei.read_text().splitlines() if z.strip()]
    ergebnis = {}
    for f in fenster:
        drin = [p for p in punkte if f['von'] <= p['zeit'] <= f['bis']]
        fe = [p for p in drin if p['art'] == 'frontend' and p.get('erreichbar')]
        db = [p for p in drin if p['art'] == 'datenbank' and p.get('erreichbar')]
        if not fe:
            continue
        # Je Zeitpunkt die Arbeiter ueber beide Knoten summieren, dann das
        # Maximum ueber die Stufe - nicht den Mittelwert: die Frage ist, ob
        # die Grenze ERREICHT wurde, nicht wie es im Schnitt aussah.
        je_zeit = {}
        for p in fe:
            je_zeit.setdefault(p['zeit'], [0, 0])
            je_zeit[p['zeit']][0] += p['arbeiter']
            je_zeit[p['zeit']][1] += p['arbeiterMax']
        arbeiter_max = max((a for a, _ in je_zeit.values()), default=0)
        grenze = max((g for _, g in je_zeit.values()), default=0)
        treffer = [p['varnishTreffer'] for p in fe]
        fehl = [p['varnishFehl'] for p in fe]
        t = (max(treffer) - min(treffer)) if treffer else 0
        f_ = (max(fehl) - min(fehl)) if fehl else 0
        ergebnis[f['vus']] = {
            'arbeiter': arbeiter_max,
            'arbeiterMax': grenze,
            'arbeiterProzent': round(100 * arbeiter_max / grenze) if grenze else None,
            'lastMax': round(max((p['last'] for p in fe), default=0), 2),
            'lastJeKernMax': round(max((p.get('lastJeKern', 0) for p in fe), default=0), 2),
            'kerne': max((p.get('kerne', 0) for p in fe), default=0),
            'varnishQuote': round(100 * t / (t + f_)) if (t + f_) else None,
            'dbVerbindungen': max((p['verbindungen'] for p in db), default=0),
            'dbBremst': round(max((p['bremst'] for p in db), default=0), 5),
        }
    return ergebnis


def main() -> int:
    ordner = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent
    lauf = ordner / 'lauf'
    dateien = sorted(lauf.glob('stufe-*.json'), key=lambda p: int(re.search(r'(\d+)', p.name).group(1)))
    if not dateien:
        print('Keine Stufen gefunden in', lauf, file=sys.stderr)
        return 1

    stufen = [stufe_lesen(d) for d in dateien]

    fenster = []
    f_datei = lauf / 'fenster.jsonl'
    if f_datei.exists():
        fenster = [json.loads(z) for z in f_datei.read_text().splitlines() if z.strip()]
    server = server_je_stufe(ordner, fenster)
    for s in stufen:
        s['server'] = server.get(s['vus'])

    # Die Wand: erste Stufe, die eine der Grenzen reisst.
    #
    # NICHT an der Artikelseite messen. Der Test ruft eine feste Auswahl von
    # Artikeln auf; nach kurzer Zeit liegen die alle in Varnish, und die
    # Seite faellt auf wenige Millisekunden - sie misst dann den
    # Zwischenspeicher, nicht den Shop. Echte Besucher verteilen sich auf
    # ueber 6.000 Artikel, dort ist die Trefferquote deutlich niedriger.
    #
    # Aussagekraeftig sind die Schritte, die NIE aus dem Cache kommen:
    # Suche, Warenkorb, Bestellung. Und die Fehlerquote.
    wand = None
    grund = None
    for s in stufen:
        bestellung = (s['schritte'].get('Bestellung') or {}).get('p95')
        suche = (s['schritte'].get('Suche') or {}).get('p95')
        if s['fehlerquote'] > FEHLER_GRENZE:
            wand, grund = s['vus'], f"Fehlerquote {s['fehlerquote']*100:.2f} %"
            break
        if bestellung and bestellung > BESTELLUNG_GRENZE:
            wand, grund = s['vus'], f"Bestellung dauert {bestellung/1000:.1f} s"
            break
        if suche and suche > SEITE_GRENZE:
            wand, grund = s['vus'], f"Suche dauert {suche/1000:.1f} s"
            break

    letzte_gute = None
    for s in stufen:
        if wand is not None and s['vus'] >= wand:
            break
        letzte_gute = s['vus']

    lage = {}
    lage_datei = ordner / 'lage.json'
    if lage_datei.exists():
        roh = json.loads(lage_datei.read_text())
        lage = {'basis': roh.get('basis'), 'artikel': len(roh.get('artikel', [])),
                'kunden': len(roh.get('kunden', [])), 'stand': roh.get('stand')}

    heraus = {
        'stufen': stufen,
        'wand': wand,
        'wandGrund': grund,
        'letzteGuteStufe': letzte_gute,
        'grenzen': {'fehlerProzent': FEHLER_GRENZE * 100, 'sucheMs': SEITE_GRENZE,
                    'bestellungMs': BESTELLUNG_GRENZE, 'artikelseiteMs': SEITE_GRENZE},
        'lage': lage,
    }
    ziel = ordner / 'ergebnis-kompakt.json'
    ziel.write_text(json.dumps(heraus, indent=2, ensure_ascii=False))

    print(f'{len(stufen)} Stufen ausgewertet -> {ziel}')
    print()
    print('  Stufe          Suche p95   Bestellung p95    Fehler   Arbeiter   Varnish')
    print('  ' + '-' * 72)
    for s in stufen:
        a = (s['schritte'].get('Suche') or {}).get('p95')
        b = (s['schritte'].get('Bestellung') or {}).get('p95')
        sv = s.get('server') or {}
        print('  %5d   %14s   %14s   %6.2f%%   %7s   %6s' % (
            s['vus'],
            f'{a:.0f} ms' if a else '-',
            f'{b:.0f} ms' if b else '-',
            s['fehlerquote'] * 100,
            f"{sv.get('arbeiter','-')}/{sv.get('arbeiterMax','-')}",
            f"{sv.get('varnishQuote')} %" if sv.get('varnishQuote') is not None else '-',
        ))
    print()
    if wand:
        print(f'  Wand bei {wand} gleichzeitigen Einkaeufern ({grund}).')
        print(f'  Letzte unauffaellige Stufe: {letzte_gute}.')
    else:
        print('  Keine Wand erreicht - der Lauf ging nicht hoch genug.')
    return 0


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