#!/usr/bin/env python3
"""Setzt die Messwerte in die Ergebnisseite ein.

    seite-bauen.py <vorlage.html> <ergebnis-kompakt.json> <ziel.html>

Die Vorlage enthaelt eine Stelle /*DATEN-ANFANG*/{}/*DATEN-ENDE*/ - genau
dort wird eingesetzt. So bleibt die Seite eine gewoehnliche HTML-Datei, die
sich auch ohne diesen Schritt oeffnen laesst, und die Messwerte stehen an
einer einzigen Stelle statt verstreut im Markup.
"""
import json
import sys
from datetime import datetime, timezone
from pathlib import Path

# Aus Google Analytics, 30 Tage, 42.806 Sitzungen, mittlere Verweildauer
# 3 m 22 s. Gleichzeitigkeit nach Little: Sitzungen je Sekunde mal Dauer.
# Der Freitagsfaktor 1,6 stammt aus dem Websale-Bericht, in dem jeder
# Freitag der Spitzentag der Woche war.
HEUTE = {
    'sitzungenProTag': 1427,
    'verweildauerSekunden': 202,
    'mittel': 3,
    'spitzeStunde': 9,
    'spitzeFreitag': 14,
}

# Der Trichter, wie das Skript ihn wuerfelt - und woher die Zahlen stammen.
TRICHTER = [
    {'name': 'Startseite',   'anteil': 100.0, 'cache': True},
    {'name': 'Suche',        'anteil': 80.0,  'cache': False},
    {'name': 'Kategorie',    'anteil': 55.0,  'cache': True},
    {'name': 'Artikelseite', 'anteil': 41.2,  'cache': True},
    {'name': 'In den Korb',  'anteil': 16.5,  'cache': False},
    {'name': 'Kasse',        'anteil': 10.1,  'cache': False},
    {'name': 'Bestellung',   'anteil': 8.6,   'cache': False},
]


def engpass_text(d: dict) -> dict:
    letzte = d['stufen'][-1]
    sv = letzte.get('server') or {}
    arbeiter = sv.get('arbeiterProzent')
    bremst = sv.get('dbBremst') or 0

    if arbeiter is not None and arbeiter >= 85:
        return {
            'titel': 'Die PHP-Arbeiter sind der Engpass – nicht die Suche',
            'text': 'Die Suche bricht als Erste ein, weil sie die teuerste Seite ist, die nie aus '
                    'dem Zwischenspeicher kommt. Schuld ist sie aber nicht: die Suchmaschine hat '
                    'während des gesamten Laufs keine einzige Anfrage abgewiesen und ihre '
                    'Warteschlange blieb leer. Voll waren die PHP-Arbeiter – zwölf je Frontend, '
                    'vierundzwanzig im Verbund. Alles, was an Varnish vorbeigeht, wartet auf einen '
                    'davon. Auch die Datenbank hat nie gebremst; Bestellungen liefen noch bei '
                    'vierhundert gleichzeitigen Einkäufern in gut einer Sekunde durch.',
        }
    if bremst > 0.05:
        return {
            'titel': 'Die Datenbank bremst',
            'text': 'Ein Galera-Knoten hat den Verbund angehalten, um nicht zurückzufallen. '
                    'Das trifft alle Frontends gleichzeitig und lässt sich nicht durch mehr '
                    'Webserver lösen.',
        }
    return {
        'titel': 'Kein einzelner Engpass sichtbar',
        'text': 'Weder die Arbeiter noch die Datenbank waren am Anschlag. Die Wartezeiten '
                'steigen durch die Summe vieler kleiner Wege – dann hilft nur, die teuersten '
                'Seiten schneller zu machen, nicht mehr Maschinen.',
    }


def main() -> int:
    if len(sys.argv) < 4:
        print(__doc__, file=sys.stderr)
        return 1
    vorlage, quelle, ziel = (Path(a) for a in sys.argv[1:4])
    d = json.loads(quelle.read_text(encoding='utf-8'))

    letzte = d['stufen'][-1]
    d['gemessenAm'] = datetime.now(timezone.utc).astimezone().isoformat()
    d['heute'] = HEUTE
    d['trichter'] = TRICHTER
    d['engpass'] = engpass_text(d)
    d['methode'] = [
        ['Werkzeug', 'k6, Trichter als Skript – jeder simulierte Besucher mit eigener Sitzung'],
        ['Gegen', d['lage'].get('basis', '–') + ' über den Load Balancer, also denselben Weg wie ein echter Besucher'],
        ['Stufen', ', '.join(str(s['vus']) for s in d['stufen']) + ' gleichzeitige Einkäufer, je 90 Sekunden mit Pause dazwischen'],
        ['Bestellungen', f"{sum(s['bestellungen'] for s in d['stufen'])} echte Bestellungen im Shop angelegt und anschließend restlos entfernt"],
        ['Verhalten', 'Abbruchquoten aus Google Analytics, 30 Tage, 42.806 Sitzungen'],
        ['Altersprüfung', 'Testkunden waren registriert und geprüft – so wie Stammkunden, die wiederkommen'],
        ['Grenzwerte', f"Wand, sobald die Fehlerquote {d['grenzen']['fehlerProzent']:.0f} % übersteigt "
                       f"oder eine Artikelseite länger als {d['grenzen']['artikelseiteMs'] / 1000:.0f} Sekunden braucht"],
        ['Nicht gemessen', 'PayPal (Sandbox, würde PayPal messen), Versand von E-Mails (abgeschaltet), '
                           'die Altersprüfung selbst (Dokumentenerkennung ist ein eigener Fall)'],
        ['Einschränkung', 'Der Test verteilt sich auf %d Artikel, echte Besucher auf über 6.000. '
                          'Dadurch lag die Cache-Trefferquote im Test höher als im Alltag – die '
                          'gemessene Grenze ist also eher die optimistische Lesart.'
                          % d['lage'].get('artikel', 0)],
    ]

    # ------------------------------------------------- Drittes Frontend
    gut = d['letzteGuteStufe'] or 0
    # Von gleichzeitigen Einkaeufern auf Sitzungen am Tag zurueckrechnen:
    # Gleichzeitigkeit mal 3600 durch Verweildauer ergibt Sitzungen in der
    # Spitzenstunde; die Spitzenstunde traegt rund 11 Prozent des Tages.
    spitzenstunde = round(gut * 3600 / HEUTE['verweildauerSekunden'])
    proTag = round(spitzenstunde / 0.11)
    faktor = round(proTag / HEUTE['sitzungenProTag'])
    # Mit Reserve: nicht bis zur Kante fahren.
    schwelle = int(gut * 0.85)

    d['drittes'] = {
        'leit': f'Zwei Frontends tragen {gut} gleichzeitige Einkäufer. Ab wann das nicht mehr reicht, '
                f'lässt sich in Besucherzahlen umrechnen – und die kennt ihr.',
        'karten': [
            {'wert': str(gut), 'name': 'gleichzeitige Einkäufer',
             'zusatz': 'Was zwei Frontends tragen, bevor die Suche zwei Sekunden überschreitet.'},
            {'wert': f'{spitzenstunde:,}'.replace(',', '.'), 'name': 'Sitzungen in der Spitzenstunde',
             'zusatz': 'Dieselbe Zahl, umgerechnet über die mittlere Verweildauer von 3 m 22 s.'},
            {'wert': f'{proTag:,}'.replace(',', '.'), 'name': 'Sitzungen am Tag',
             'zusatz': 'Wenn die Spitzenstunde wie heute rund 11 % des Tages trägt.'},
            {'wert': f'{faktor}\u00d7', 'name': 'so viel wie heute',
             'zusatz': f"Heute sind es {HEUTE['sitzungenProTag']:,} Sitzungen am Tag.".replace(',', '.')},
        ],
        'titel': f'Die Marke: ab rund {schwelle} gleichzeitigen Einkäufern vorbereiten',
        'absaetze': [
            f'Das ist die gemessene Grenze minus 15 % Reserve. Wer bis {gut} wartet, merkt den Engpass '
            f'erst, wenn Kunden ihn schon merken.',
            'Der Engpass ist die Rechenleistung der Frontends: zwei Kerne je Maschine. Die Arbeiterzahl '
            'haben wir bereits von zwölf auf vierundzwanzig je Knoten erhöht – das hat im Bereich zwischen '
            'hundert und hundertfünfzig geholfen, die harte Grenze aber nicht verschoben. Mehr Arbeiter '
            'verteilen dieselbe CPU nur auf mehr Prozesse.',
            'Ein drittes Frontend bringt zwei weitere Kerne, also rund die Hälfte mehr Kapazität. Die '
            'Alternative wäre, die vorhandenen Maschinen von zwei auf vier Kerne zu vergrößern – das '
            'kostet etwa dasselbe, bringt aber keine zusätzliche Ausfallsicherheit. Ein dritter Knoten '
            'teilt allerdings auch den Zwischenspeicher: jede Maschine hält ihren eigenen Varnish, die '
            'Trefferquote je Knoten sinkt also zunächst.',
            'Die dritte Möglichkeit ist die billigste und wurde noch nicht ausprobiert: die Suche selbst '
            'günstiger machen. Sie ist die einzige Seite, die unter Last einbricht – Bestellungen liefen '
            'selbst bei sechshundert gleichzeitigen Einkäufern noch in unter zwei Sekunden durch.',
        ],
    }

    html = vorlage.read_text(encoding='utf-8')
    marke_auf, marke_zu = '/*DATEN-ANFANG*/', '/*DATEN-ENDE*/'
    a = html.index(marke_auf) + len(marke_auf)
    b = html.index(marke_zu)
    ziel.write_text(html[:a] + json.dumps(d, ensure_ascii=False, indent=2) + html[b:], encoding='utf-8')

    print(f'{ziel} geschrieben ({ziel.stat().st_size // 1024} KB)')
    print(f"  Stufen: {', '.join(str(s['vus']) for s in d['stufen'])}")
    print(f"  Wand  : {d['wand'] or 'nicht erreicht'}")
    return 0


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