#!/usr/bin/env python3
"""Health intelligence pipeline for JARVIS.

Functions:
- Parse the user-maintained lab reference XLSX.
- Export 4-eyes correction workbooks.
- Generate the current lab report in the desired pivot format.
- Generate a simple HTML dashboard and daily/weekly markdown reports.
- Apply reviewed correction XLSX files to the master DB.
- Register local inbox files in the document table.

This script deliberately keeps OCR/Docling extraction separate from final DB writes:
newly extracted lab values should first go into laborwerte_staging and be reviewed.
"""
from __future__ import annotations

import argparse
import csv
import html
import json
import os
import re
import shutil
import sqlite3
import subprocess
import sys
import uuid
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Any, Iterable

import openpyxl
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter

BASE = Path(os.path.expanduser("~/.hermes/assets/Gesundheit"))
DB = BASE / "health_data.db"
INBOX = BASE / "inbox"
REPORTS = BASE / "reports"
CORRECTIONS = BASE / "corrections"
ATTACHMENTS = BASE / "attachments"
REFERENCE_XLSX = ATTACHMENTS / "Laborwerte_Uebersicht_2021-2025.xlsx"
REFERENCE_CANDIDATES = [
    INBOX / "260510_Laborwerte_Uebersicht.xlsx",
    ATTACHMENTS / "Laborwerte_Uebersicht_2021-2025.xlsx",
    BASE / "backup_original" / "attachments" / "Laborwerte_Uebersicht_2021-2025.xlsx",
]
ACCOUNT = "friday.uplink@gmail.com"
GOG_SECRET_ENV = Path.home() / ".hermes" / "secrets" / "gog_keyring.env"


def load_gog_keyring_password() -> str:
    value = os.environ.get("GOG_KEYRING_PASSWORD", "").strip()
    if value:
        return value
    if not GOG_SECRET_ENV.is_file():
        raise RuntimeError(f"Missing gog keyring secret environment file: {GOG_SECRET_ENV}")
    if GOG_SECRET_ENV.stat().st_mode & 0o077:
        raise RuntimeError(f"Unsafe permissions on gog keyring secret file: {GOG_SECRET_ENV}")
    for raw_line in GOG_SECRET_ENV.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#"):
            continue
        if line.startswith("export "):
            line = line[7:].strip()
        key, sep, raw_value = line.partition("=")
        if sep and key.strip() == "GOG_KEYRING_PASSWORD":
            secret = raw_value.strip().strip('"').strip("'")
            if secret:
                return secret
    raise RuntimeError(f"GOG_KEYRING_PASSWORD missing in {GOG_SECRET_ENV}")


def gog_env() -> dict[str, str]:
    return {
        **os.environ,
        "HOME": str(Path.home()),
        "XDG_CONFIG_HOME": str(Path.home() / ".config"),
        "GOG_KEYRING_PASSWORD": load_gog_keyring_password(),
    }

CATEGORY_NAMES = {
    "Blutabnahmen", "Entzündung", "Hämatologie", "Blutfette & Stoffwechsel",
    "Blutstatus Leuk", "Blutbild automatisch absolut", "Leber & Niere",
    "Elektrolyte & Vitamine", "Proteine", "Schilddrüse & Hormone",
    "Schilddrüse", "Hormone", "Spezielle Gerinnung", "Gerinnung",
    "Infektion / Autoimmun", "Virale Hepatitiden A,B,C,D,E",
    "Auto-Antikörper gegen", "Immunologie / Infektion", "Urin", "Sonstiges"
}

REVIEW_HEADERS = [
    "status", "kategorie", "parameter_name", "wert_extrahiert", "wert_korrigiert",
    "wert_final", "einheit", "referenzbereich", "abnahme_datum", "befund_datum",
    "flag", "confidence", "quelle", "dokument_id", "kommentar"
]


@dataclass
class LabValue:
    category: str
    parameter: str
    reference: str | None
    date: str
    value: str
    unit: str | None = None


def conn() -> sqlite3.Connection:
    c = sqlite3.connect(DB)
    c.row_factory = sqlite3.Row
    return c


def ensure_dirs() -> None:
    for p in [REPORTS, CORRECTIONS, INBOX, ATTACHMENTS]:
        p.mkdir(parents=True, exist_ok=True)


def normalize_date(value: Any) -> str | None:
    if value is None or value == "":
        return None
    if isinstance(value, datetime):
        return value.date().isoformat()
    if isinstance(value, date):
        return value.isoformat()
    text = str(value).strip()
    for fmt in ["%Y-%m-%d", "%d.%m.%Y", "%d.%m.%y", "%Y/%m/%d"]:
        try:
            return datetime.strptime(text, fmt).date().isoformat()
        except ValueError:
            pass
    m = re.search(r"(20\d{2})[-_.]?(\d{2})[-_.]?(\d{2})", text)
    if m:
        return f"{m.group(1)}-{m.group(2)}-{m.group(3)}"
    return text


def split_reference(ref: str | None) -> tuple[str | None, str | None, str | None]:
    if not ref:
        return None, None, None
    text = str(ref).strip()
    unit = None
    nums = re.findall(r"[-+]?\d+(?:[.,]\d+)?", text)
    # first alphabetical/non numeric tail as rough unit
    m_unit = re.search(r"(?:\d|[<>])\s*([A-Za-zµ%/][A-Za-z0-9µ/%^ .-]*)$", text)
    if m_unit:
        unit = m_unit.group(1).strip()
    if "-" in text and len(nums) >= 2:
        return nums[0].replace(",", "."), nums[1].replace(",", "."), unit
    if text.strip().startswith("<") and nums:
        return None, nums[0].replace(",", "."), unit
    if text.strip().startswith(">") and nums:
        return nums[0].replace(",", "."), None, unit
    return None, None, unit


def best_reference_xlsx() -> Path:
    existing = [p for p in REFERENCE_CANDIDATES if p.exists()]
    if not existing:
        return REFERENCE_XLSX
    def score(p: Path) -> tuple[int, float]:
        try:
            wb = load_workbook(p, read_only=False, data_only=True)
            ws = wb[wb.sheetnames[0]]
            return (int(ws.max_column or 0), p.stat().st_mtime)
        except Exception:
            return (0, p.stat().st_mtime)
    return max(existing, key=score)


def parse_reference_xlsx(path: Path | None = None) -> list[LabValue]:
    if path is None:
        path = best_reference_xlsx()
    if not path.exists():
        raise FileNotFoundError(path)
    wb = load_workbook(path, data_only=True)
    ws = wb[wb.sheetnames[0]]
    dates: dict[int, str] = {}
    for col in range(3, ws.max_column + 1):
        d = normalize_date(ws.cell(1, col).value)
        if d:
            dates[col] = d
    values: list[LabValue] = []
    category = "Sonstiges"
    for row in range(2, ws.max_row + 1):
        name = ws.cell(row, 1).value
        if name is None or str(name).strip() == "":
            continue
        name_s = str(name).strip()
        non_empty_date_values = [ws.cell(row, c).value for c in dates if ws.cell(row, c).value not in (None, "")]
        ref = ws.cell(row, 2).value
        # Category rows are plain labels without reference and date values.
        if name_s in CATEGORY_NAMES and not ref and not non_empty_date_values:
            category = name_s
            continue
        if not non_empty_date_values:
            continue
        _, _, unit = split_reference(str(ref) if ref else None)
        for col, d in dates.items():
            val = ws.cell(row, col).value
            if val is None or str(val).strip() == "":
                continue
            values.append(LabValue(category, name_s, str(ref).strip() if ref else None, d, str(val).strip(), unit))
    return values


def get_lab_matrix(values: list[LabValue]) -> tuple[list[str], list[str], dict[tuple[str, str], LabValue], dict[str, str], dict[str, str]]:
    dates = sorted({v.date for v in values})
    cats: dict[str, str] = {}
    refs: dict[str, str] = {}
    matrix: dict[tuple[str, str], LabValue] = {}
    for v in values:
        cats[v.parameter] = v.category
        if v.reference:
            refs[v.parameter] = v.reference
        matrix[(v.parameter, v.date)] = v
    order = {c: i for i, c in enumerate(CATEGORY_NAMES)}
    params = sorted(cats, key=lambda p: (order.get(cats.get(p, "Sonstiges"), 999), cats.get(p, ""), p.lower()))
    return dates, params, matrix, cats, refs


def style_sheet(ws: Any, freeze: str = "C2") -> None:
    ws.freeze_panes = freeze
    ws.auto_filter.ref = ws.dimensions
    for col in range(1, ws.max_column + 1):
        letter = get_column_letter(col)
        max_len = 10
        for row in range(1, min(ws.max_row, 80) + 1):
            val = ws.cell(row, col).value
            if val is not None:
                max_len = max(max_len, min(45, len(str(val)) + 2))
        ws.column_dimensions[letter].width = max_len


def generate_lab_report(output: Path = REPORTS / "aktuelle_blutwerte.xlsx", source: Path | None = None) -> Path:
    ensure_dirs()
    values = parse_reference_xlsx(source)
    dates, params, matrix, cats, refs = get_lab_matrix(values)
    wb = Workbook()
    ws = wb.active
    ws.title = "Aktuelle Blutwerte"
    header_fill = PatternFill("solid", fgColor="1F4E78")
    header_font = Font(color="FFFFFF", bold=True)
    cat_fill = PatternFill("solid", fgColor="D9EAF7")
    cat_font = Font(color="1F4E78", bold=True)
    border = Border(*(Side(style="thin", color="BFBFBF") for _ in range(4)))
    ws.append(["Parameter", "Referenzbereich"] + dates)
    for cell in ws[1]:
        cell.fill = header_fill; cell.font = header_font; cell.alignment = Alignment(horizontal="center", wrap_text=True); cell.border = border
    row = 2
    current_cat = None
    for p in params:
        cat = cats.get(p, "Sonstiges")
        if cat != current_cat:
            ws.append([cat] + [None] * (len(dates) + 1))
            ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=len(dates)+2)
            c = ws.cell(row, 1); c.fill = cat_fill; c.font = cat_font; c.border = border
            current_cat = cat; row += 1
        ws.append([p, refs.get(p, "")] + [matrix.get((p, d)).value if matrix.get((p, d)) else None for d in dates])
        for c in ws[row]:
            c.border = border; c.alignment = Alignment(horizontal="center" if c.column > 2 else "left", wrap_text=True)
        row += 1
    ws.column_dimensions["A"].width = 42
    ws.column_dimensions["B"].width = 22
    for col in range(3, len(dates)+3):
        ws.column_dimensions[get_column_letter(col)].width = 13
    style_sheet(ws)
    wb.save(output)
    return output


def export_correction_xlsx(batch_id: str | None = None, output: Path | None = None, source: Path | None = None) -> Path:
    """Export a review workbook. If batch_id exists in staging, export it; otherwise export reference XLSX as initial validated review."""
    ensure_dirs()
    if not batch_id:
        batch_id = "reference_" + datetime.now().strftime("%Y%m%d_%H%M%S")
    if output is None:
        output = CORRECTIONS / f"KORREKTUR_Laborwerte_{batch_id}.xlsx"
    wb = Workbook()
    ws = wb.active
    ws.title = "Laborwerte_Korrektur"
    ws.append(REVIEW_HEADERS)
    c = conn()
    rows = []
    try:
        rows = list(c.execute("SELECT * FROM laborwerte_staging WHERE batch_id=? ORDER BY kategorie, parameter_name, abnahme_datum", (batch_id,)))
    finally:
        c.close()
    if rows:
        for r in rows:
            final = r["wert_final"] or r["wert_korrigiert"] or r["wert_extrahiert"]
            ws.append([r["status"], r["kategorie"], r["parameter_name"], r["wert_extrahiert"], r["wert_korrigiert"], final, r["einheit"], r["referenzbereich"], r["abnahme_datum"], r["befund_datum"], r["flag"], r["confidence"], r["quelle"], r["dokument_id"], r["kommentar"]])
    else:
        source_path = source or best_reference_xlsx()
        for v in parse_reference_xlsx(source_path):
            ws.append(["validiert", v.category, v.parameter, v.value, None, v.value, v.unit, v.reference, v.date, None, None, 1.0, source_path.name, None, "Referenz-XLSX"])
    header_fill = PatternFill("solid", fgColor="7030A0")
    header_font = Font(color="FFFFFF", bold=True)
    for cell in ws[1]:
        cell.fill = header_fill; cell.font = header_font
    style_sheet(ws, freeze="A2")
    wb.save(output)
    return output


def apply_corrections(path: Path, batch_id: str | None = None) -> int:
    """Read reviewed correction workbook and upsert validated values into laborwerte."""
    wb = load_workbook(path, data_only=True)
    ws = wb[wb.sheetnames[0]]
    headers = [str(c.value).strip() if c.value is not None else "" for c in ws[1]]
    idx = {h: i + 1 for i, h in enumerate(headers)}
    required = {"parameter_name", "wert_final", "abnahme_datum"}
    if not required <= set(idx):
        raise ValueError(f"Missing required columns: {required - set(idx)}")
    c = conn()
    count = 0
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    try:
        for row in range(2, ws.max_row + 1):
            status = str(ws.cell(row, idx.get("status", 1)).value or "validiert").strip().lower()
            if status in {"abgelehnt", "reject", "gelöscht", "geloescht"}:
                continue
            param = ws.cell(row, idx["parameter_name"]).value
            value = ws.cell(row, idx["wert_final"]).value or ws.cell(row, idx.get("wert_korrigiert", idx["wert_final"])).value
            abnahme = normalize_date(ws.cell(row, idx["abnahme_datum"]).value)
            if not param or value in (None, "") or not abnahme:
                continue
            einheit = ws.cell(row, idx.get("einheit", 0)).value if idx.get("einheit") else None
            ref = ws.cell(row, idx.get("referenzbereich", 0)).value if idx.get("referenzbereich") else None
            rmin, rmax, _ = split_reference(str(ref) if ref else None)
            quelle = ws.cell(row, idx.get("quelle", 0)).value if idx.get("quelle") else path.name
            existing = c.execute("SELECT id FROM laborwerte WHERE parameter_name=? AND abnahme_datum=? AND COALESCE(einheit,'')=COALESCE(?, '')", (str(param), abnahme, einheit)).fetchone()
            if existing:
                c.execute("""UPDATE laborwerte SET wert=?, wert_original=COALESCE(wert_original, wert), einheit=?, reference_min=?, reference_max=?, bemerking=?, quelle=?, validierungsstatus='validiert', review_batch_id=?, befund_datum=COALESCE(befund_datum, ?) WHERE id=?""",
                          (str(value), einheit, rmin, rmax, str(ref) if ref else None, str(quelle), batch_id, now, existing["id"]))
            else:
                c.execute("""INSERT INTO laborwerte (parameter_name, wert, wert_original, einheit, reference_min, reference_max, einheiten, bemerking, ermittlung_datum, abnahme_datum, befund_datum, quelle, validierungsstatus, review_batch_id)
                             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'validiert', ?)""",
                          (str(param), str(value), str(value), einheit, rmin, rmax, einheit, str(ref) if ref else None, now, abnahme, now[:10], str(quelle), batch_id))
            count += 1
        c.commit()
    finally:
        c.close()
    return count


def latest_docs(limit: int = 8) -> list[sqlite3.Row]:
    c = conn()
    try:
        return list(c.execute("SELECT id,datei_name,kategorie,status,review_status,upload_datum, length(COALESCE(extrahierte_inhalte,'')) text_len FROM dokumente ORDER BY id DESC LIMIT ?", (limit,)))
    finally:
        c.close()


def db_counts() -> dict[str, Any]:
    c = conn()
    try:
        out: dict[str, Any] = {}
        for table in ["dokumente", "laborwerte", "laborwerte_staging", "ernaehrung", "tagebuch", "symptome", "health_events", "health_insights"]:
            out[table] = c.execute(f"SELECT count(*) FROM {table}").fetchone()[0]
        out["documents_with_text"] = c.execute("SELECT count(*) FROM dokumente WHERE length(trim(COALESCE(extrahierte_inhalte,'')))>0").fetchone()[0]
        out["open_reviews"] = c.execute("SELECT count(*) FROM laborwerte_staging WHERE status IN ('zur_pruefung','extrahiert')").fetchone()[0]
        return out
    finally:
        c.close()


def generate_dashboard(output: Path = REPORTS / "health_dashboard.html") -> Path:
    """Generate dashboard via robust v3 renderer.

    The previous inline Chart.js annotation-plugin implementation failed silently
    in some browsers/CDN states, leaving only titles/reference text visible.
    v3 uses plain Chart.js datasets for lines/reference bands/event markers.
    """
    import subprocess
    script = BASE / "scripts" / "health_dashboard_v3.py"
    subprocess.run([sys.executable, str(script)], check=True)
    return output


def generate_dashboard_v4(output: Path = REPORTS / "health_dashboard_v4.html") -> Path:
    """Generate the complete mobile and privacy-hardened dashboard v4."""
    import subprocess

    script = BASE / "scripts" / "health_dashboard_v4.py"
    subprocess.run([sys.executable, str(script), "--output", str(output)], check=True)
    return output


def generate_dashboard_v5(
    db: Path,
    output: Path = REPORTS / "health_dashboard_v5.html",
) -> Path:
    """Generate the parallel Dashboard v5 preview without replacing v4."""
    import subprocess

    script = BASE / "scripts" / "health_dashboard_v5.py"
    subprocess.run(
        [sys.executable, str(script), "--db", str(db), "--output", str(output)],
        check=True,
    )
    return output


def generate_multimodal_correlations(db: Path) -> None:
    """Refresh aggregate-only exploratory correlations for the explicitly selected database."""
    script = BASE / "scripts" / "multimodal_correlations.py"
    subprocess.run([sys.executable, str(script), "--db", str(db)], check=True)


def generate_daily_report(day: str | None = None, output: Path | None = None) -> Path:
    ensure_dirs()
    day = day or date.today().isoformat()
    output = output or REPORTS / f"daily_health_report_{day}.md"
    c = conn()
    try:
        ern = list(c.execute("SELECT mahlzeit,beschreibung,wirkung,notizen FROM ernaehrung WHERE datum=? ORDER BY id", (day,)))
        tb = list(c.execute("SELECT kategori,titel,inhalt,wirkung FROM tagebuch WHERE datum=? ORDER BY id", (day,)))
        sym = list(c.execute("SELECT symptom,schwergrad,wirkung,notizen FROM symptome WHERE datum=? ORDER BY id", (day,)))
    finally:
        c.close()
    lines = [f"# Tagesbericht Gesundheit — {day}", ""]
    if not any([ern, tb, sym]):
        lines += ["Keine neuen strukturierten Tagesdaten gefunden."]
    if ern:
        lines += ["## Ernährung", *[f"- {r['mahlzeit'] or 'Mahlzeit'}: {r['beschreibung']}" + (f" → {r['wirkung']}" if r['wirkung'] else "") for r in ern], ""]
    if sym:
        lines += ["## Symptome", *[f"- {r['symptom']} ({r['schwergrad'] or 'n/a'})" for r in sym], ""]
    if tb:
        lines += ["## Tagebuch / Gesundheitsdaten", *[f"- {r['kategori']}: {r['titel']} — {r['inhalt'] or ''}" for r in tb], ""]
    lines += ["## Nächste sinnvolle Schritte", "- Offene Korrektur-XLSX prüfen, falls vorhanden.", "- Bei neuen Labor-/Befunddokumenten Verarbeitung abwarten und Freigabe geben."]
    output.write_text("\n".join(lines), encoding="utf-8")
    c = conn(); c.execute("INSERT INTO report_runs(report_type, period_start, period_end, output_path, summary) VALUES(?,?,?,?,?)", ("daily", day, day, str(output), "Tagesbericht generiert")); c.commit(); c.close()
    return output


def generate_weekly_report(end_day: str | None = None, output: Path | None = None) -> Path:
    ensure_dirs()
    end = datetime.strptime(end_day, "%Y-%m-%d").date() if end_day else date.today()
    start = end - timedelta(days=6)
    output = output or REPORTS / f"weekly_health_report_{start.isoformat()}_{end.isoformat()}.md"
    c = conn()
    try:
        docs = c.execute("SELECT count(*) FROM dokumente WHERE date(upload_datum) BETWEEN ? AND ?", (start.isoformat(), end.isoformat())).fetchone()[0]
        ern = c.execute("SELECT count(*) FROM ernaehrung WHERE datum BETWEEN ? AND ?", (start.isoformat(), end.isoformat())).fetchone()[0]
        sym = c.execute("SELECT count(*) FROM symptome WHERE datum BETWEEN ? AND ?", (start.isoformat(), end.isoformat())).fetchone()[0]
        reviews = c.execute("SELECT count(*) FROM laborwerte_staging WHERE status IN ('zur_pruefung','extrahiert')").fetchone()[0]
    finally:
        c.close()
    lines = [
        f"# Wochenbericht Gesundheit — {start.isoformat()} bis {end.isoformat()}", "",
        "## Zusammenfassung", f"- Neue/registrierte Dokumente: {docs}", f"- Ernährungseinträge: {ern}", f"- Symptomeinträge: {sym}", f"- Offene Labor-Korrekturen: {reviews}", "",
        "## Einschätzung", "- Korrelationen werden belastbarer, sobald YAZIO-/Wearable-Daten täglich vollständig und Laborwerte validiert sind.", "- Auffälligkeiten sollten als Musterhinweise verstanden und bei medizinischer Relevanz mit Ärztinnen/Ärzten besprochen werden.", "",
        "## Offene Aktionen", "- Dokumente ohne Volltext weiter mit Docling nachbearbeiten.", "- Korrektur-XLSX prüfen und danach Freigabe zur Übernahme geben."
    ]
    output.write_text("\n".join(lines), encoding="utf-8")
    c = conn(); c.execute("INSERT INTO report_runs(report_type, period_start, period_end, output_path, summary) VALUES(?,?,?,?,?)", ("weekly", start.isoformat(), end.isoformat(), str(output), "Wochenbericht generiert")); c.commit(); c.close()
    return output


def register_inbox_files() -> int:
    ensure_dirs()
    c = conn()
    count = 0
    try:
        for p in INBOX.iterdir():
            if not p.is_file() or p.name.startswith("."):
                continue
            ext = p.suffix.lower()
            if ext not in {".pdf", ".png", ".jpg", ".jpeg", ".xlsx"}:
                continue
            data_type = "image" if ext in {".png", ".jpg", ".jpeg"} else "pdf"
            if ext == ".xlsx":
                data_type = "pdf"  # DB CHECK allows only pdf/image; legacy compromise.
            name_l = p.name.lower()
            category = "LABOR" if any(x in name_l for x in ["labor", "blut", "chemie", "haemat", "hämat"]) else "BEFUNDE" if any(x in name_l for x in ["befund", "bericht", "mri", "ct", "ultraschall", "röntgen", "roentgen"]) else "SONSTIGES"
            exists = c.execute("SELECT id FROM dokumente WHERE dateipfad=? OR datei_name=?", (str(p), p.name)).fetchone()
            if exists:
                continue
            c.execute("INSERT INTO dokumente(datei_name,dateipfad,daten_typ,groessekbytes,status,kategorie,quelle,review_status) VALUES(?,?,?,?,?,?,?,?)",
                      (p.name, str(p), data_type, int(p.stat().st_size/1024), "neu", category, "inbox", "nicht_geprueft"))
            count += 1
        c.commit()
    finally:
        c.close()
    return count


def gog_upload(path: Path, parent_id: str) -> str:
    cmd = ["gog", "-a", ACCOUNT, "drive", "upload", str(path), "--parent", parent_id, "--json", "--no-input"]
    out = subprocess.check_output(cmd, env=gog_env(), text=True)
    try:
        data = json.loads(out)
        return data.get("id") or data.get("file", {}).get("id") or out.strip()
    except Exception:
        return out.strip()


def main() -> None:
    ap = argparse.ArgumentParser()
    sub = ap.add_subparsers(dest="cmd", required=True)
    sub.add_parser("generate-lab-report")
    p_corr = sub.add_parser("export-correction"); p_corr.add_argument("--batch-id"); p_corr.add_argument("--output")
    p_apply = sub.add_parser("apply-corrections"); p_apply.add_argument("xlsx"); p_apply.add_argument("--batch-id")
    sub.add_parser("dashboard")
    sub.add_parser("dashboard-v4")
    p_dashboard_v5 = sub.add_parser("dashboard-v5")
    p_dashboard_v5.add_argument("--db", type=Path, required=True)
    p_correlations = sub.add_parser("correlations")
    p_correlations.add_argument("--db", type=Path, required=True)
    p_day = sub.add_parser("daily-report"); p_day.add_argument("--date")
    p_week = sub.add_parser("weekly-report"); p_week.add_argument("--end-date")
    sub.add_parser("register-inbox")
    p_upload = sub.add_parser("upload"); p_upload.add_argument("path"); p_upload.add_argument("--parent", required=True)
    args = ap.parse_args()

    if args.cmd == "generate-lab-report":
        print(generate_lab_report())
    elif args.cmd == "export-correction":
        print(export_correction_xlsx(args.batch_id, Path(args.output) if args.output else None))
    elif args.cmd == "apply-corrections":
        print(f"Übernommen: {apply_corrections(Path(args.xlsx), args.batch_id)}")
    elif args.cmd == "dashboard":
        print(generate_dashboard())
    elif args.cmd == "dashboard-v4":
        print(generate_dashboard_v4())
    elif args.cmd == "dashboard-v5":
        print(generate_dashboard_v5(args.db))
    elif args.cmd == "correlations":
        generate_multimodal_correlations(args.db)
    elif args.cmd == "daily-report":
        print(generate_daily_report(args.date))
    elif args.cmd == "weekly-report":
        print(generate_weekly_report(args.end_date))
    elif args.cmd == "register-inbox":
        print(f"Registriert: {register_inbox_files()}")
    elif args.cmd == "upload":
        print(gog_upload(Path(args.path), args.parent))


if __name__ == "__main__":
    main()


