"""Sucht die \\n-Falle in allen Projektdateien — in BEIDE Richtungen.

Die Falle: beim Schreiben ueber ein Shell-Heredoc wird aus einem gemeinten `\\n`
ein ECHTER Zeilenumbruch mitten im String-Literal. In Python bricht das den
Compile (zweimal heute passiert), in JS ebenso — aber nicht ueberall faellt es
sofort auf.

Geprueft wird:
  A) Python: kompiliert jede Datei. Ein zerrissenes Literal faellt hier auf.
  B) JS: Zeilen mit ungerader Zahl UNMASKIERTER Anfuehrungszeichen ausserhalb
     von Kommentaren = angefangenes, nicht geschlossenes String-Literal.
  C) Umgekehrter Fall: literales `\\n` als TEXT dort, wo es niemand lesen will
     (HTML-Attribute, Markdown-Fliesstext) — das entsteht, wenn ein `\\\\n`
     zu viel escaped wurde.
  D) Verdaechtige Zeilenpaare: eine Zeile endet mit oeffnendem Quote, die
     naechste besteht nur aus einem Quote — die Signatur der beiden heutigen
     Faelle.
"""
import pathlib
import py_compile
import re
import subprocess
import sys
import tempfile

WURZEL = pathlib.Path(r"c:\Users\ah\Downloads\tradingrobot")
SKIP = {".git", "__pycache__", ".reminders", "node_modules", ".removed_backup"}
befunde = []


def dateien(*endungen):
    for p in WURZEL.rglob("*"):
        if any(s in p.parts for s in SKIP):
            continue
        if p.is_file() and p.suffix in endungen:
            yield p


# ── A) Python ────────────────────────────────────────────────────────────
print("=" * 88)
print("  A) PYTHON — kompiliert jede Datei (ein zerrissenes Literal faellt hier auf)")
print("=" * 88)
npy = 0
tmp = tempfile.mkdtemp()
for p in dateien(".py"):
    npy += 1
    try:
        py_compile.compile(str(p), cfile=f"{tmp}/x.pyc", doraise=True)
    except py_compile.PyCompileError as e:
        msg = str(e).strip().splitlines()[-1][:110]
        befunde.append(f"PY {p.relative_to(WURZEL)}: {msg}")
        print(f"  FEHLER  {p.relative_to(WURZEL)}")
        print(f"          {msg}")
print(f"  {npy} Dateien geprueft.")


# ── A2) pyflakes: was py_compile NICHT sieht ─────────────────────────────
# ⚠⚠ WARUM DIESE STUFE NOETIG IST. `py_compile` prueft nur die SYNTAX. Ein
# Tippfehler in einem Namen ist syntaktisch einwandfrei und fliegt erst zur
# Laufzeit — und dieses Projekt hat sehr viele breite `except Exception`-Bloecke
# (bewusst, als Fail-safe im Trading-Betrieb). Ein `NameError` darin ist damit
# UNSICHTBAR: keine Meldung, kein Logeintrag, nur ein Zweig, der nichts tut.
# Real gefunden beim ERSTEN Lauf ueberhaupt (2026-08-07):
#   · `core/trader.py` nutzte zweimal `log.` — den Namen gibt es dort nicht
#     (nur `log_trade`/`log_hist`). Einer davon seit dem Initial-Commit.
#   · Folge 1 (`_broker_offset_s`): der NameError lief in ein `except: pass`
#     und nahm die Zuweisung `self._boff = int(off)` mit → ein echter
#     Broker-Zeitzonen-Wechsel waere nie uebernommen worden.
#   · Folge 2 (`modify_sltp`): kein try drumherum → der Fehler lief bis in
#     `/api/sltp`; der Broker hatte SL/TP schon geaendert, der User bekam
#     trotzdem einen Fehler, und `trail.deactivate()` wurde uebersprungen.
# GETRENNT NACH SCHWEREGRAD: eine Pruefung, die dauerhaft 100 harmlose Zeilen
# ausgibt, wird ignoriert — und dann auch die eine wichtige.
# ⚠ `redefinition of unused` steht BEWUSST NICHT hier drin. pyflakes meldet damit
# auch das voellig legitime Muster `hook = None` gefolgt von einem bedingten
# `def hook(...)` — real in `backtest_flipclose2.py` und
# `backtest_adverse15_squeeze.py`, beide geprueft und korrekt. Zwei unvermeidbare
# Dauer-Treffer sind genau der Weg, auf dem eine Pruefung ignoriert wird, und mit
# ihr dann auch die eine echte Meldung. Wer die Klasse doch sehen will, ruft
# `python -m pyflakes <datei>` direkt auf.
SCHWER = ("undefined name", "undefined local", "syntax error")
print()
print("=" * 88)
print("  A2) LINTER — undefinierte Namen (was py_compile nicht sieht)")
print("=" * 88)
# ⚠ RUFF statt pyflakes (2026-08-07, Web-Recherche zum 2026er-Standard): gleiche
# Befunde, deutlich schneller, ein Werkzeug statt vieler. Bewusst NUR
# `ruff check` mit ENGER Regelauswahl — **kein `ruff format`**: das wuerde 161
# Dateien umformatieren, darunter alle Backtests, und damit die Migrations-Regel
# verletzen (deren Zahlen sind dokumentiert).
#   F821 = undefined name      ← die Klasse, die hier real weh tut
#   E999 = syntax error
#   F811 (redefinition) bleibt DRAUSSEN, s. Begruendung bei SCHWER oben.
# pyflakes bleibt als Rueckfall, damit die Pruefung auch ohne ruff laeuft.
#   ⚠⚠ NUR F821 — `E999` gibt es in ruff 0.16 NICHT MEHR. Wird es trotzdem
#   selektiert, bricht ruff KOMPLETT ab („Rule E999 was removed") mit Exit 2.
#   Genau das ist beim ersten Bau passiert, und schlimmer: der Rueckgabecode
#   wurde nicht geprueft, die Fehlermeldung ging nach stderr — die Stufe meldete
#   also „keine undefinierten Namen", waehrend sie GAR NICHTS geprueft hat. Ein
#   absichtlich eingebauter `F821`-Fehler kam glatt durch den Hook.
#   Syntaxfehler deckt ohnehin Stufe A (`py_compile`) ab.
#   ⚠ RUFF-EXIT-CODES: 0 = sauber · 1 = Befunde · 2 = Werkzeugfehler.
#   2 darf NIEMALS als „sauber" durchgehen.
_lint_ok = False
try:
    r = subprocess.run([sys.executable, "-m", "ruff", "check",
                        "--select", "F821", "--no-cache",
                        "--output-format", "concise", str(WURZEL)],
                       capture_output=True, text=True, timeout=180)
    if r.returncode not in (0, 1):
        print(f"  ⚠ ruff meldet einen WERKZEUGFEHLER (Exit {r.returncode}) — "
              f"Rueckfall auf pyflakes:")
        for z in (r.stderr or r.stdout).strip().splitlines()[:3]:
            print(f"      {z}")
        raise RuntimeError("ruff nicht nutzbar")
    _lint_ok = True
    treffer = [z for z in r.stdout.splitlines()
               if z.strip() and not z.startswith(("Found", "All checks", "warning:"))]
    for z in treffer:
        kurz = z.replace(str(WURZEL) + "\\", "").replace(str(WURZEL) + "/", "")
        befunde.append(f"RUFF {kurz}")
        print(f"  FEHLER  {kurz}")
    if not treffer:
        print("  keine undefinierten Namen (ruff, F821/E999).")
except Exception:
    pass

if not _lint_ok:
    try:
        import io

        from pyflakes.api import checkPath
        from pyflakes.reporter import Reporter

        aus, err = io.StringIO(), io.StringIO()
        rep = Reporter(aus, err)
        for p in dateien(".py"):
            checkPath(str(p), rep)
        zeilen = [z for z in (aus.getvalue() + err.getvalue()).splitlines()
                  if z.strip()]
        schwer = [z for z in zeilen if any(s in z for s in SCHWER)]
        for z in schwer:
            kurz = z.replace(str(WURZEL) + "\\", "").replace(str(WURZEL) + "/", "")
            befunde.append(f"PF {kurz}")
            print(f"  FEHLER  {kurz}")
        if not schwer:
            print("  keine undefinierten Namen (pyflakes-Rueckfall).")
    except ImportError:
        print("  ⚠ weder ruff noch pyflakes installiert — Stufe uebersprungen.")
        print("    Installation:  python -m pip install -r requirements-dev.txt")


# ── B) JavaScript ────────────────────────────────────────────────────────
def js_offene_strings(text: str):
    """Zeilen mit angefangenem, nicht geschlossenem ' oder " (Template-Literale
    duerfen mehrzeilig sein und werden uebersprungen)."""
    treffer = []
    in_tmpl = False
    for nr, z in enumerate(text.splitlines(), 1):
        # Backticks zaehlen -> Template-Literal-Zustand fortschreiben
        bt = len(re.findall(r"(?<!\\)`", z))
        if in_tmpl:
            in_tmpl ^= bool(bt % 2)
            continue
        if bt % 2:
            in_tmpl = True
            continue
        s = re.sub(r"//.*$", "", z)                 # Zeilenkommentar weg
        # ⚠ Quotes NICHT einfach zaehlen — das erzeugt Fehlalarme: '"' ist ein
        # gueltiges Literal, das EIN doppeltes Anfuehrungszeichen enthaelt (real
        # in app.js aufgetreten). Stattdessen zeichenweise laufen und den jeweils
        # AKTIVEN Quote-Typ verfolgen; Quotes der anderen Art zaehlen dann nicht.
        aktiv, esc = None, False
        for ch in s:
            if esc:
                esc = False
                continue
            if ch == "\\":
                esc = True
                continue
            if aktiv:
                if ch == aktiv:
                    aktiv = None
            elif ch in ('"', "'"):
                aktiv = ch
        if aktiv:
            treffer.append((nr, z.strip()[:100]))
    return treffer


print()
print("=" * 88)
print("  B) JAVASCRIPT — Syntax (node) + unabgeschlossene String-Literale")
print("=" * 88)


def node_pruefen(p: pathlib.Path):
    """Echte Syntaxpruefung mit `node --check`. Gibt die Fehlerzeile zurueck oder
    None; `False` heisst „Node nicht verfuegbar".

    ⚠ WARUM DAS DAZUKAM (2026-08-07). Die Klammern-Balance findet gemessen nur
    1 von 5 realistischen Fehlern — `const x = ;`, ein doppeltes `let`/`const`
    und ein `return` ausserhalb einer Funktion laufen glatt durch. Ein
    Syntaxfehler in `app.js` bricht aber den GESAMTEN Render: das Dashboard
    friert auf Altwerten ein. Bei 95 direkten Element-Zugriffen ist das kein
    theoretisches Risiko.

    ⚠ GEPRUEFT WIRD ALS MODUL (.mjs), NICHT ALS CommonJS. Als CJS haelt Node ein
    top-level `return` fuer legal — der Browser laedt `app.js` aber als
    klassisches Script, wo es ein Syntaxfehler ist. Der Modul-Modus liegt naeher
    am Browser; verifiziert, dass das echte `app.js` ihn ohne Anpassung besteht.

    ⚠ Kein npm, keine package.json, kein node_modules — `node --check` ist die
    Laufzeit selbst. Damit entsteht NICHT die Divergenz „Pruefumgebung ist nicht
    die Laufumgebung", an der dieses Projekt schon zweimal aufgelaufen ist:
    Node und Chrome benutzen denselben Parser (V8).

    ⚠ Fehlt Node, faellt die Stufe still auf die alte Prueflogik zurueck — eine
    frische Maschine soll den Hook nicht blockieren.
    """
    try:
        with tempfile.NamedTemporaryFile("w", suffix=".mjs", delete=False,
                                         encoding="utf-8") as f:
            f.write(p.read_text(encoding="utf-8", errors="replace"))
            tmp = f.name
        try:
            r = subprocess.run(["node", "--check", tmp],
                               capture_output=True, text=True, timeout=60)
        finally:
            try:
                pathlib.Path(tmp).unlink()
            except OSError:
                pass
    except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
        return False
    if r.returncode == 0:
        return None
    for z in (r.stderr or "").splitlines():
        if "Error" in z:
            return z.strip()
    return "node --check meldet einen Fehler"


for p in dateien(".js"):
    if "lightweight-charts" in p.name:
        continue
    rel = p.relative_to(WURZEL)
    nd = node_pruefen(p)
    if nd is False:
        print(f"  ⚠ node fehlt — nur Ersatzpruefung fuer {rel}")
    elif nd:
        befunde.append(f"JS {rel} (node)")
        print(f"  FEHLER  {rel}  {nd}")
        continue          # bei kaputter Syntax ist die Zeilenheuristik sinnlos
    tr = js_offene_strings(p.read_text(encoding="utf-8", errors="replace"))
    if tr:
        for nr, z in tr:
            befunde.append(f"JS {rel}:{nr}")
            print(f"  FEHLER  {rel}:{nr}  {z}")
    else:
        print(f"  OK      {rel}" + ("  (node)" if nd is None else ""))


# ── C) literales \n als TEXT ─────────────────────────────────────────────
print()
print("=" * 88)
print("  C) UMGEKEHRT — literales \\n im sichtbaren Text (HTML-Attribute, Markdown)")
print("=" * 88)
for p in list(dateien(".html")) + list(dateien(".md")):
    t = p.read_text(encoding="utf-8", errors="replace")
    roh = []
    for m in re.finditer(r'title="([^"]*)"', t):
        if "\\n" in m.group(1):
            roh.append(f"title=...{m.group(1)[:60]}")
    if p.suffix == ".md":
        for nr, z in enumerate(t.splitlines(), 1):
            if "\\n" in z and "`" not in z and "\\\\n" not in z:
                roh.append(f"Zeile {nr}: {z.strip()[:80]}")
    if roh:
        for r in roh[:5]:
            befunde.append(f"TXT {p.relative_to(WURZEL)}: {r}")
            print(f"  FEHLER  {p.relative_to(WURZEL)}  {r}")
    else:
        print(f"  OK      {p.relative_to(WURZEL)}")


# ── D) Signatur der heutigen Faelle ──────────────────────────────────────
# ── C2) .ps1 ohne BOM mit Nicht-ASCII = Zeitbombe ────────────────────────
# ⚠⚠ REAL PASSIERT (2026-08-07): `tools/autologon_einrichten.ps1` liess sich nicht
# ausfuehren — „Schliessende ) fehlt", „Zeichenfolge hat kein Abschlusszeichen".
# Ursache war KEIN Syntaxfehler: **Windows PowerShell 5.1 (`powershell.exe`) liest
# .ps1-Dateien als CP1252, wenn kein BOM da ist.** Aus dem Gedankenstrich „—"
# werden dann drei Zeichen (â€"), und das enthaltene " schliesst den String
# vorzeitig — Folgefehler bis zum Dateiende.
# ⚠ Warum es beim Schreiben nicht auffiel: `pwsh` 7 (mein Werkzeug) liest UTF-8
# OHNE BOM klaglos. Die Datei war also fuer mich fehlerfrei und fuer den Nutzer
# kaputt. Genau die Sorte Divergenz, die dieses Projekt als Deployment-Drift
# kennt — hier auf der Werkzeug-Ebene.
# Fix: UTF-8 MIT BOM schreiben. Gefunden wurde dabei ein zweiter Fall
# (`scripts/send_daily_report.ps1`, mit Umlauten).
print()
print("=" * 88)
print("  C2) POWERSHELL — .ps1 mit Nicht-ASCII braucht ein BOM (sonst CP1252)")
print("=" * 88)
for p in dateien(".ps1"):
    roh = p.read_bytes()
    bom = roh[:3] == b"\xef\xbb\xbf"
    try:
        txt = roh.decode("utf-8-sig")
    except UnicodeDecodeError:
        txt = roh.decode("cp1252", errors="replace")
    na = sorted({c for c in txt if ord(c) > 127})
    if na and not bom:
        befunde.append(f"PS1 {p.relative_to(WURZEL)}: Nicht-ASCII ohne BOM")
        print(f"  FEHLER  {p.relative_to(WURZEL)}  "
              f"{len(na)} Nicht-ASCII-Zeichen, KEIN BOM → bricht unter "
              f"powershell.exe 5.1")
    else:
        print(f"  OK      {p.relative_to(WURZEL)}"
              + ("  (BOM)" if bom else "  (rein ASCII)"))

print()
print("=" * 88)
print('  D) SIGNATUR — Zeile endet offen, naechste besteht nur aus einem Quote')
print("=" * 88)
sig = 0
for p in list(dateien(".py")) + list(dateien(".js")):
    zeilen = p.read_text(encoding="utf-8", errors="replace").splitlines()
    for i in range(len(zeilen) - 1):
        if zeilen[i + 1].strip() in ('"', "'", '",', "',"):
            sig += 1
            befunde.append(f"SIG {p.relative_to(WURZEL)}:{i+2}")
            print(f"  VERDAECHTIG  {p.relative_to(WURZEL)}:{i+2}")
print("  keine gefunden." if not sig else f"  {sig} Stellen.")

print()
print("=" * 88)
print("  ERGEBNIS: " + ("keine \\n-Falle gefunden" if not befunde
                        else f"{len(befunde)} Befund(e)"))
print("=" * 88)
sys.exit(1 if befunde else 0)
