#!/usr/bin/env python3
"""Stufe 5 der Pipeline: Server zuverlässig neu starten UND das Ergebnis prüfen.

⚠⚠ WARUM ES DIESES SKRIPT GIBT. `restart_server.bat` hat **zweimal still nicht
neu gestartet** (auch als `Start-Process cmd /c … -WindowStyle Minimized`) — der
alte Prozess lief weiter, und weil statische Dateien bei jedem Request frisch von
der Platte gelesen werden, **belegt eine korrekt ausgelieferte `app.js?v=N` GAR
NICHTS** über den geladenen Python-Code. Der Fehler fiel damals nur auf, weil ein
NEUES Snapshot-Feld fehlte (`hl_live.basis_stale`).

Dieses Skript macht daraus einen prüfbaren Ablauf:
  0. CODE pruefen (check_nfalle + pytest) — VOR dem Kill, damit ein
     laufender Server nicht fuer kaputten Code abgeschossen wird.
     ⚠ Ergaenzt 2026-08-07: die Pruefung hing bis dahin am COMMIT, nicht
     am Deployment. Eine nie committete Datei ging ueber
     `restart_server.bat` ungeprueft live.
  1. Prozess auf dem Port gezielt beenden — **nicht** per `*server.py*`-Muster,
     das trifft auch das Hyperliquid-Dashboard auf 8001 (real passiert: der
     HL-Server wurde bei jedem MT5-Neustart still mit-erschlagen und riss die
     Lücken in die Datensammlung).
  2. Neu starten und warten, bis `/api/snapshot` wirklich antwortet.
  3. **Genau EINE** Instanz je Port verifizieren.
  4. Optional `--feld <name>` prüfen: ist dieses Snapshot-Feld da, ist der
     PYTHON-Code nachweislich neu. Das ist der einzige belastbare Beweis.
  5. Das Log seit dem Start auf Fehler durchsehen.

Rückgabecode 0 nur, wenn ALLE Schritte durchlaufen — so lässt sich das Skript
verketten, statt das Ergebnis von Hand zu lesen.

Aufruf:
    python tools/deploy.py                      # Oil-Server (8000) neu starten
    python tools/deploy.py --feld rec_track     # zusätzlich Code-Neuheit belegen
    python tools/deploy.py --hl                 # HL-Dashboard (8001) mitnehmen
    python tools/deploy.py --nur-pruefen        # nichts anfassen, nur Zustand
    python tools/deploy.py --ohne-pruefung      # Notausgang: Schritt 0 aus
"""
import argparse
import json
import subprocess
import sys
import time
import urllib.request
from pathlib import Path

WURZEL = Path(__file__).resolve().parents[1]
PY = r"C:\Users\ah\AppData\Local\Programs\Python\Python312\python.exe"
HL = WURZEL.parent / "HyperLiquid-WTI Trader"
LOG = WURZEL / "oil_widget.log"
PORT, PORT_HL = 8000, 8001
START_TIMEOUT_S = 120


def ps(cmd: str) -> str:
    """PowerShell — für die Prozess-/Port-Abfragen der zuverlässigste Weg hier."""
    r = subprocess.run(["powershell", "-NoProfile", "-Command", cmd],
                       capture_output=True, text=True, timeout=60)
    return (r.stdout or "").strip()


def pids(port: int) -> list[int]:
    out = ps(f"(Get-NetTCPConnection -LocalPort {port} -State Listen "
             f"-ErrorAction SilentlyContinue | Select-Object -Expand "
             f"OwningProcess -Unique) -join ','")
    return [int(x) for x in out.split(",") if x.strip().isdigit()]


def snapshot(timeout: float = 5.0) -> dict | None:
    try:
        with urllib.request.urlopen(
                f"http://127.0.0.1:{PORT}/api/snapshot", timeout=timeout) as r:
            return json.loads(r.read().decode())
    except Exception:
        return None


def schritt(nr: int, text: str):
    print(f"  [{nr}] {text}")


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--feld", help="Snapshot-Feld, das den neuen Code belegt")
    ap.add_argument("--hl", action="store_true", help="HL-Dashboard (8001) mit")
    ap.add_argument("--nur-pruefen", action="store_true")
    ap.add_argument("--ohne-pruefung", action="store_true",
                    help="Code-Prüfung überspringen (Notausgang, s. Schritt 0)")
    a = ap.parse_args()

    print("=" * 74)
    print("  DEPLOY — Stufe 5 (Aktivierung) + Stufe 6 (Verifikation)")
    print("=" * 74)
    fehler: list[str] = []

    if a.nur_pruefen:
        for p in (PORT, PORT_HL):
            print(f"  Port {p}: {pids(p) or 'nichts'}")
        s = snapshot()
        print(f"  Snapshot: {'antwortet' if s else 'KEINE Antwort'}")
        return 0 if s else 1

    # ── 0) CODE prüfen, BEVOR der laufende Server angefasst wird ─────────
    # ⚠⚠ WARUM DAS HIER DAZUKAM (2026-08-07, User-Frage „wird die Pipeline bei
    # jedem Deployment ausgeführt?"). Antwort war NEIN: die Prüfung hing am
    # COMMIT, nicht am Deployment — und das sind zwei Ereignisse, die beliebig
    # auseinanderfallen. Eine geänderte, nie committete Datei ging über
    # `restart_server.bat` ungeprüft live.
    # ⚠ Dass der Server hochkommt, ist KEIN Ersatz: die beiden `log.`-NameError
    # in `core/trader.py` sassen MONATE in `except`-Zweigen (einer seit dem
    # Initial-Commit), und der Server startete jedes Mal sauber. Genau solche
    # Fehler findet `pytest`/`ruff`, nicht der Startvorgang.
    # ⚠ Die Prüfung läuft VOR dem Kill — ein laufender, funktionierender Server
    # darf nicht für kaputten Code abgeschossen werden.
    if not a.ohne_pruefung:
        schritt(0, "Code prüfen (check_nfalle + pytest) …")
        r = subprocess.run([PY, "-X", "utf8", str(WURZEL / "tools" / "pre_commit.py")],
                           cwd=WURZEL, capture_output=True, text=True)
        for z in (r.stdout or "").strip().splitlines():
            print(f"      {z}")
        if r.returncode != 0:
            print("  ❌ ABBRUCH — der laufende Server bleibt unangetastet.")
            print("     Beheben, oder bewusst übergehen: --ohne-pruefung")
            return 1
    else:
        print("  [0] Code-Prüfung ÜBERSPRUNGEN (--ohne-pruefung) ⚠")

    # ── 1) gezielt beenden ───────────────────────────────────────────────
    alt = pids(PORT)
    schritt(1, f"Port {PORT}: {alt or 'nichts'} → beenden")
    for pid in alt:
        ps(f"Stop-Process -Id {pid} -Force -ErrorAction SilentlyContinue")
    if alt:
        time.sleep(3)
    if a.hl and HL.exists():
        for pid in pids(PORT_HL):
            ps(f"Stop-Process -Id {pid} -Force -ErrorAction SilentlyContinue")
        time.sleep(2)
        ps(f"Start-Process -FilePath '{PY}' -ArgumentList '-X','utf8','server.py' "
           f"-WorkingDirectory '{HL}' -WindowStyle Hidden")
        schritt(1, f"HL-Dashboard ({PORT_HL}) neu gestartet")

    log_ab = LOG.stat().st_size if LOG.exists() else 0

    # ── 2) starten und auf echte Antwort warten ──────────────────────────
    schritt(2, "Server starten …")
    ps(f"Start-Process -FilePath '{PY}' -ArgumentList '-X','utf8','server.py' "
       f"-WorkingDirectory '{WURZEL}' -WindowStyle Minimized")
    t0 = time.time()
    snap = None
    while time.time() - t0 < START_TIMEOUT_S:
        time.sleep(3)
        snap = snapshot()
        if snap:
            break
    if not snap:
        print(f"  ❌ /api/snapshot antwortet nach {START_TIMEOUT_S} s nicht.")
        return 1
    schritt(2, f"läuft nach {time.time() - t0:.0f} s")

    # ── 3) genau EINE Instanz ────────────────────────────────────────────
    for p, noetig in ((PORT, True), (PORT_HL, a.hl)):
        ids = pids(p)
        if not noetig and not ids:
            continue
        if len(ids) == 1:
            schritt(3, f"Port {p}: genau eine Instanz (PID {ids[0]}) ✅")
        else:
            fehler.append(f"Port {p}: {len(ids)} Instanzen {ids}")
            schritt(3, f"Port {p}: ⚠ {len(ids)} Instanzen {ids}")

    # ── 4) beweist ein neues Feld, dass der PYTHON-Code neu ist? ─────────
    # ⚠ PUNKTPFADE seit 2026-08-10 (`m15_setup.ziel_grund`). Vorher ging nur die
    #   oberste Ebene (`a.feld in snap`) — die meisten neuen Felder liegen aber
    #   verschachtelt, und fuer die war der Neuheits-Beweis damit gar nicht
    #   fuehrbar. Genau das ist Schritt 4 aber: der EINZIGE belastbare Nachweis,
    #   dass wirklich neuer Python-Code laeuft (statische Dateien beweisen
    #   nichts — `restart_server.bat` hat zweimal still nicht neu gestartet).
    # ⚠ Ein Feld mit Wert `None` zaehlt als VORHANDEN — geprueft wird die
    #   Existenz des Schluessels, nicht sein Inhalt. Ein neues Feld ist beim
    #   ersten Snapshot oft noch leer.
    if a.feld:
        knoten, pfad_ok = snap, True
        for teil in a.feld.split("."):
            if isinstance(knoten, dict) and teil in knoten:
                knoten = knoten[teil]
            else:
                pfad_ok = False
                break
        if pfad_ok:
            schritt(4, f"Feld `{a.feld}` vorhanden → Python-Code ist neu ✅")
        else:
            fehler.append(f"Feld `{a.feld}` fehlt im Snapshot")
            schritt(4, f"⚠ Feld `{a.feld}` FEHLT — der alte Code läuft weiter!")
    else:
        schritt(4, "kein --feld angegeben ⚠ ohne das ist NICHT belegt, dass der "
                   "Python-Code neu ist (statische Dateien beweisen nichts)")

    # ── 5) Log seit dem Start ────────────────────────────────────────────
    treffer = []
    if LOG.exists():
        with LOG.open(encoding="utf-8", errors="replace") as fh:
            fh.seek(log_ab)
            for z in fh:
                if any(w in z for w in ("ERROR", "Traceback", "CRITICAL")):
                    treffer.append(z.rstrip()[:130])
    if treffer:
        fehler.append(f"{len(treffer)} Fehlerzeile(n) im Log")
        schritt(5, f"⚠ {len(treffer)} Fehlerzeile(n) seit dem Start:")
        for z in treffer[:5]:
            print(f"        {z}")
    else:
        schritt(5, "Log seit dem Start ohne ERROR/Traceback ✅")

    print("=" * 74)
    if fehler:
        print("  ERGEBNIS: ❌ " + " · ".join(fehler))
        return 1
    print("  ERGEBNIS: ✅ neu gestartet und verifiziert")
    return 0


if __name__ == "__main__":
    sys.exit(main())
