#!/usr/bin/env python3
"""YAZIO nutrition sync v2 for JARVIS HealthManager.

- Fetches YAZIO consumed-items for one or more dates.
- Stores normalized item rows, all available nutrient keys, meal/day summaries.
- Scores items/meals/days against local SIGHi-inspired histamine rules.
- Maintains legacy ernaehrung/tagebuch rows for backward compatibility.

Output stays compact: no food lists are printed to chat by default.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import math
import re
import sqlite3
import subprocess
import sys
from collections import defaultdict
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Any

import requests

from dashboard_v5.nutrition_contract import (
    NUTRIENT_CONTRACTS,
    NUTRIENT_CONTRACT_VERSION,
    normalize_nutrient_status,
)

YAZIO_BASE_URL = "https://yzapi.yazio.com/v15"
# Existing local YAZIO credentials. Do not print these values.
YAZIO_SECRET_FILE = Path.home() / ".hermes" / "secrets" / "yazio_credentials.json"

def load_yazio_credentials() -> dict[str, str]:
    # Secret values live outside code; never print this file.
    data = json.loads(YAZIO_SECRET_FILE.read_text(encoding="utf-8"))
    return {
        "client_id": data["client_id"],
        "client_secret": data["client_secret"],
        "username": data["username"],
        "password": data["password"],
    }

BASE = Path("/home/agent/.hermes/assets/Gesundheit")
DB_PATH = BASE / "health_data.db"
PIPELINE = BASE / "scripts" / "health_pipeline.py"
CORRELATIONS = BASE / "scripts" / "multimodal_correlations.py"
MEALS = ["breakfast", "lunch", "dinner", "snack", "unknown"]

MACRO_KEYS = {
    "kcal": "energy.energy",
    "protein_g": "nutrient.protein",
    "carb_g": "nutrient.carb",
    "fat_g": "nutrient.fat",
}

NUTRIENT_UNITS = {key: contract.unit for key, contract in NUTRIENT_CONTRACTS.items()}


def get_token() -> str:
    resp = requests.post(
        f"{YAZIO_BASE_URL}/oauth/token",
        json={
            **load_yazio_credentials(),
            "grant_type": "password",
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()["access_token"]


def api_get(path: str, token: str, params: dict[str, Any] | None = None) -> Any:
    resp = requests.get(
        f"{YAZIO_BASE_URL}{path}",
        params=params,
        headers={"Authorization": f"Bearer {token}", "Accept-Language": "de-DE"},
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()


def fnum(x: Any) -> float:
    try:
        if x is None or x == "":
            return 0.0
        return float(x)
    except Exception:
        return 0.0


def optional_fnum(value: Any) -> float | None:
    if value is None or value == "" or isinstance(value, bool):
        return None
    try:
        number = float(value)
    except (TypeError, ValueError):
        return None
    return number if math.isfinite(number) else None


def display_number(value: Any, suffix: str = "") -> str:
    number = optional_fnum(value)
    return "unbekannt" if number is None else f"{number:g}{suffix}"


def normalize_text(s: str) -> str:
    s = (s or "").lower()
    repl = str.maketrans({"ä":"ae", "ö":"oe", "ü":"ue", "é":"e", "è":"e", "à":"a", "ß":"ss"})
    s = s.translate(repl)
    s = re.sub(r"[^a-z0-9]+", " ", s)
    return re.sub(r"\s+", " ", s).strip()


def nutrient_amount(raw_value: Any, amount_g: float | None, key: str) -> float | None:
    """Scale a documented YAZIO nutrient only when value and amount are present."""
    val = optional_fnum(raw_value)
    if val is None or amount_g is None:
        return None
    return round(val * amount_g, 3)


def item_hash(day: str, item: dict[str, Any]) -> str:
    payload = json.dumps(item, ensure_ascii=False, sort_keys=True, default=str)
    return hashlib.sha256((day + "|" + payload).encode()).hexdigest()


def fetch_items(day: str, token: str) -> list[dict[str, Any]]:
    diary = api_get("/user/consumed-items", token, {"date": day})
    product_cache: dict[str, dict[str, Any]] = {}
    out: list[dict[str, Any]] = []

    for raw in diary.get("products", []) or []:
        pid = str(raw.get("product_id") or "")
        prod: dict[str, Any] = {}
        if pid:
            if pid not in product_cache:
                try:
                    product_cache[pid] = api_get(f"/products/{pid}", token)
                except Exception:
                    product_cache[pid] = {}
            prod = product_cache[pid]
        amount = optional_fnum(raw.get("amount"))
        nutrients_raw = prod.get("nutrients") or {}
        nutrients_scaled: dict[str, float] = {}
        nutrient_provenance: dict[str, dict[str, Any]] = {}
        for key, value in nutrients_raw.items():
            contract = NUTRIENT_CONTRACTS.get(key)
            if contract is None or amount is None:
                continue
            normalized = normalize_nutrient_status(
                key, value, contract.source_unit, explicit_zero=False
            )
            nutrient_provenance[key] = {
                "raw_value": value,
                "raw_unit": f"{contract.source_unit}/g",
                "factor": float(normalized.get("factor") or contract.conversion_factor) * amount,
                "status": normalized["status"],
            }
            if normalized["status"] not in {"documented_value", "documented_zero"}:
                continue
            nutrients_scaled[key] = round(float(normalized["value"]) * amount, 3)
        out.append({
            "source_type": "product",
            "source_item_id": str(raw.get("id") or raw.get("diary_id") or ""),
            "source_product_id": pid,
            "datum": day,
            "meal": str(raw.get("daytime") or "unknown"),
            "name": str(prod.get("name") or raw.get("name") or f"Produkt {pid}"),
            "brand": prod.get("brand") or prod.get("manufacturer"),
            "amount": amount,
            "amount_unit": "g",
            "serving": raw.get("serving"),
            "serving_quantity": fnum(raw.get("serving_quantity")) if raw.get("serving_quantity") is not None else None,
            "nutrients": nutrients_scaled,
            "nutrient_provenance": nutrient_provenance,
            "raw_item_json": raw,
            "raw_product_json": prod,
        })

    for bucket, source_type in [("simple_products", "simple_product"), ("recipe_portions", "recipe")]:
        for raw in diary.get(bucket, []) or []:
            amount = optional_fnum(raw.get("amount") or raw.get("serving_quantity"))
            nutrients = raw.get("nutrients") or {}
            scaled: dict[str, float] = {}
            provenance: dict[str, dict[str, Any]] = {}
            if isinstance(nutrients, dict):
                for key, value in nutrients.items():
                    contract = NUTRIENT_CONTRACTS.get(key)
                    if contract is None:
                        continue
                    normalized = normalize_nutrient_status(
                        key, value, contract.source_unit, explicit_zero=False
                    )
                    provenance[key] = {
                        "raw_value": value,
                        "raw_unit": contract.source_unit,
                        "factor": normalized.get("factor") or contract.conversion_factor,
                        "status": normalized["status"],
                    }
                    if normalized["status"] in {"documented_value", "documented_zero"}:
                        scaled[key] = float(normalized["value"])
            # fallbacks seen in older/compact YAZIO shapes
            for k_src, k_dst in [("energy", "energy.energy"), ("kcal", "energy.energy"), ("protein", "nutrient.protein"), ("carb", "nutrient.carb"), ("fat", "nutrient.fat")]:
                if k_src in raw and k_dst not in scaled:
                    number = optional_fnum(raw.get(k_src))
                    if number is not None:
                        scaled[k_dst] = number
                        contract = NUTRIENT_CONTRACTS.get(k_dst)
                        if contract:
                            provenance[k_dst] = {
                                "raw_value": raw.get(k_src),
                                "raw_unit": contract.unit,
                                "factor": 1.0,
                                "status": "documented_zero" if number == 0 else "documented_value",
                            }
            out.append({
                "source_type": source_type,
                "source_item_id": str(raw.get("id") or ""),
                "source_product_id": str(raw.get("product_id") or raw.get("id") or ""),
                "datum": day,
                "meal": str(raw.get("daytime") or "unknown"),
                "name": str(raw.get("name") or raw.get("title") or raw.get("recipe_name") or source_type),
                "brand": raw.get("brand") or raw.get("manufacturer"),
                "amount": amount,
                "amount_unit": "g",
                "serving": raw.get("serving"),
                "serving_quantity": fnum(raw.get("serving_quantity")) if raw.get("serving_quantity") is not None else None,
                "nutrients": scaled,
                "nutrient_provenance": provenance,
                "raw_item_json": raw,
                "raw_product_json": {},
            })
    return out


def ensure_state_table(con: sqlite3.Connection) -> None:
    con.execute("""
        CREATE TABLE IF NOT EXISTS sync_state (
            key TEXT PRIMARY KEY,
            value TEXT,
            updated_at TEXT DEFAULT CURRENT_TIMESTAMP
        )
    """)


def classify_histamine(con: sqlite3.Connection, name: str) -> dict[str, Any]:
    n = normalize_text(name)
    aliases = list(con.execute("""
        SELECT a.alias, r.canonical_food, r.sighi_score, r.tags, r.confidence, r.notes
        FROM histamine_food_aliases a JOIN histamine_food_rules r ON r.canonical_food=a.canonical_food
        ORDER BY length(a.alias) DESC
    """))
    for a in aliases:
        alias_norm = normalize_text(a["alias"])
        if alias_norm and re.search(r"(^| )" + re.escape(alias_norm) + r"( |$)", n):
            score = int(a["sighi_score"])
            return {
                "canonical_food": a["canonical_food"],
                "sighi_score": score,
                "traffic_light": ["green", "yellow", "orange", "red"][score],
                "tags": a["tags"] or "",
                "confidence": a["confidence"] or "medium",
                "reason": f"Alias-Match: {a['alias']}",
            }
    return {"canonical_food": None, "sighi_score": None, "traffic_light": "unknown", "tags": "", "confidence": "low", "reason": "kein Mapping"}


def score_label(max_score: int, unknown: int, load: float) -> str:
    """Calibrated aggregate traffic light.

    Item-level SIGHi score 3 stays red at item level, but meal/day aggregates
    should only become red for genuinely high cumulative burden. A single risky
    item in an otherwise small meal is orange, not an apocalypse with cutlery.
    """
    if load >= 10:
        return "red"
    if max_score >= 3 or max_score == 2 or load >= 4:
        return "orange"
    if max_score == 1 or load >= 1:
        return "yellow"
    if unknown:
        return "unknown"
    return "green"


def sync_date(day: str, token: str, force: bool = False) -> tuple[bool, str]:
    items = fetch_items(day, token)
    payload = json.dumps(items, ensure_ascii=False, sort_keys=True, default=str)
    fp = hashlib.sha256(payload.encode()).hexdigest()
    con = sqlite3.connect(DB_PATH)
    con.row_factory = sqlite3.Row
    try:
        ensure_state_table(con)
        old = con.execute("SELECT value FROM sync_state WHERE key=?", (f"yazio:{day}:v2:fingerprint",)).fetchone()
        if old and old["value"] == fp and not force:
            return False, f"{day}: unverändert"

        # Replace this day's automated YAZIO rows only.
        ids = [r[0] for r in con.execute("SELECT id FROM nutrition_items WHERE datum=? AND source='yazio_api'", (day,))]
        if ids:
            q = ",".join("?" for _ in ids)
            con.execute(f"DELETE FROM nutrition_item_nutrients WHERE item_id IN ({q})", ids)
            con.execute(f"DELETE FROM nutrition_histamine_scores WHERE item_id IN ({q})", ids)
        con.execute("DELETE FROM nutrition_items WHERE datum=? AND source='yazio_api'", (day,))
        con.execute("DELETE FROM nutrition_meal_summary WHERE datum=?", (day,))
        con.execute("DELETE FROM nutrition_daily_summary_v2 WHERE datum=?", (day,))
        con.execute("DELETE FROM ernaehrung WHERE datum=? AND COALESCE(notizen,'') LIKE '%Quelle: YAZIO API%'", (day,))
        con.execute("DELETE FROM tagebuch WHERE datum=? AND COALESCE(verknuepft,'')='yazio_api'", (day,))

        if not items:
            con.execute("""
                INSERT INTO sync_state(key,value,updated_at) VALUES(?,?,CURRENT_TIMESTAMP)
                ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=CURRENT_TIMESTAMP
            """, (f"yazio:{day}:v2:fingerprint", fp))
            con.commit()
            return False, f"{day}: keine Einträge"

        inserted_ids: list[int] = []
        for it in items:
            nutrients = it["nutrients"] or {}
            kcal = optional_fnum(nutrients.get("energy.energy"))
            protein = optional_fnum(nutrients.get("nutrient.protein"))
            carb = optional_fnum(nutrients.get("nutrient.carb"))
            fat = optional_fnum(nutrients.get("nutrient.fat"))
            ihash = item_hash(day, it)
            cur = con.execute("""
                INSERT INTO nutrition_items(source, source_item_id, source_product_id, datum, meal, name, brand, amount, amount_unit, serving, serving_quantity,
                    kcal, protein_g, carb_g, fat_g, raw_item_json, raw_product_json, item_hash, updated_at)
                VALUES('yazio_api',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)
            """, (it["source_item_id"], it["source_product_id"], day, it["meal"], it["name"], it.get("brand"), it["amount"], it.get("amount_unit") or "g", it.get("serving"), it.get("serving_quantity"), kcal, protein, carb, fat, json.dumps(it["raw_item_json"], ensure_ascii=False, sort_keys=True, default=str), json.dumps(it["raw_product_json"], ensure_ascii=False, sort_keys=True, default=str), ihash))
            item_id = int(cur.lastrowid)
            inserted_ids.append(item_id)
            provenance = it.get("nutrient_provenance") or {}
            for key in sorted(set(nutrients) | set(provenance)):
                normalized = optional_fnum(nutrients.get(key))
                raw = provenance.get(key, {})
                con.execute(
                    """INSERT OR REPLACE INTO nutrition_item_nutrients(
                           item_id,nutrient_key,value,unit,raw_value,raw_unit,
                           canonical_unit,conversion_factor,value_status,
                           conversion_contract_version)
                       VALUES(?,?,?,?,?,?,?,?,?,?)""",
                    (
                        item_id,
                        key,
                        normalized,
                        NUTRIENT_UNITS.get(key),
                        str(raw.get("raw_value")) if raw.get("raw_value") is not None else None,
                        raw.get("raw_unit"),
                        NUTRIENT_UNITS.get(key),
                        raw.get("factor"),
                        raw.get("status", "unknown"),
                        NUTRIENT_CONTRACT_VERSION,
                    ),
                )
            hs = classify_histamine(con, it["name"])
            con.execute("""
                INSERT OR REPLACE INTO nutrition_histamine_scores(item_id,canonical_food,sighi_score,traffic_light,tags,confidence,reason,scored_at)
                VALUES(?,?,?,?,?,?,?,CURRENT_TIMESTAMP)
            """, (item_id, hs["canonical_food"], hs["sighi_score"], hs["traffic_light"], hs["tags"], hs["confidence"], hs["reason"]))
            desc = (
                f"{it['name']} ({display_number(it['amount'], ' g')}): "
                f"{display_number(kcal, ' kcal')}, {display_number(protein, ' g')} Protein, "
                f"{display_number(carb, ' g')} KH, {display_number(fat, ' g')} Fett"
            )
            con.execute("INSERT INTO ernaehrung(datum,mahlzeit,beschreibung,wirkung,notizen) VALUES(?,?,?,?,?)", (day, it["meal"], desc, "", f"Quelle: YAZIO API; Typ: {it['source_type']}; ID: {it['source_product_id']}"))

        recompute_summaries(con, day)
        make_legacy_diary(con, day)
        con.execute("""
            INSERT INTO sync_state(key,value,updated_at) VALUES(?,?,CURRENT_TIMESTAMP)
            ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=CURRENT_TIMESTAMP
        """, (f"yazio:{day}:v2:fingerprint", fp))
        con.commit()
        return True, f"{day}: {len(inserted_ids)} Einträge importiert/aktualisiert"
    finally:
        con.close()


def recompute_summaries(con: sqlite3.Connection, day: str) -> None:
    item_rows = list(con.execute("""
        SELECT i.id, i.meal, i.kcal, i.protein_g, i.carb_g, i.fat_g,
               h.sighi_score, h.traffic_light
        FROM nutrition_items i
        LEFT JOIN nutrition_histamine_scores h ON h.item_id=i.id
        WHERE i.datum=? AND i.source='yazio_api'
    """, (day,)))
    nutrient_rows = list(con.execute("""
        SELECT i.meal, n.nutrient_key, SUM(n.value) value
        FROM nutrition_items i JOIN nutrition_item_nutrients n ON n.item_id=i.id
        WHERE i.datum=? AND i.source='yazio_api'
        GROUP BY i.meal, n.nutrient_key
    """, (day,)))
    macro_columns = {"kcal": "kcal", "protein": "protein_g", "carb": "carb_g", "fat": "fat_g"}
    by_meal: dict[str, dict[str, Any]] = defaultdict(
        lambda: {
            "macros": {key: [] for key in macro_columns},
            "items": 0,
            "nutrients": defaultdict(float),
            "scores": [],
            "unknown": 0,
            "load": 0.0,
        }
    )
    for row in item_rows:
        meal = row["meal"] or "unknown"
        bucket = by_meal[meal]
        bucket["items"] += 1
        for key, column in macro_columns.items():
            value = optional_fnum(row[column])
            if value is not None:
                bucket["macros"][key].append(value)
        if row["sighi_score"] is None:
            bucket["unknown"] += 1
        else:
            score = int(row["sighi_score"])
            bucket["scores"].append(score)
            bucket["load"] += score
    for row in nutrient_rows:
        value = optional_fnum(row["value"])
        if value is not None:
            by_meal[row["meal"] or "unknown"]["nutrients"][row["nutrient_key"]] += value

    day_total = {
        "macros": {key: [] for key in macro_columns},
        "items": 0,
        "nutrients": defaultdict(float),
        "scores": [],
        "unknown": 0,
        "load": 0.0,
    }
    for meal, bucket in by_meal.items():
        max_score = max(bucket["scores"] or [0])
        label = score_label(max_score, bucket["unknown"], bucket["load"])
        macro_totals = {
            key: round(sum(values), 1) if values else None
            for key, values in bucket["macros"].items()
        }
        con.execute(
            """INSERT OR REPLACE INTO nutrition_meal_summary
               (datum,meal,kcal,protein_g,carb_g,fat_g,item_count,nutrient_json,
                histamine_score,histamine_label,updated_at)
               VALUES(?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)""",
            (
                day,
                meal,
                macro_totals["kcal"],
                macro_totals["protein"],
                macro_totals["carb"],
                macro_totals["fat"],
                int(bucket["items"]),
                json.dumps(dict(sorted(bucket["nutrients"].items())), ensure_ascii=False),
                round(bucket["load"], 1),
                label,
            ),
        )
        for key, values in bucket["macros"].items():
            day_total["macros"][key].extend(values)
        day_total["items"] += int(bucket["items"])
        day_total["unknown"] += bucket["unknown"]
        day_total["load"] += bucket["load"]
        day_total["scores"].extend(bucket["scores"])
        for key, value in bucket["nutrients"].items():
            day_total["nutrients"][key] += value
    max_score = max(day_total["scores"] or [0])
    label = score_label(max_score, day_total["unknown"], day_total["load"])
    daily_macros = {
        key: round(sum(values), 1) if values else None
        for key, values in day_total["macros"].items()
    }
    con.execute(
        """INSERT OR REPLACE INTO nutrition_daily_summary_v2
           (datum,kcal,protein_g,carb_g,fat_g,item_count,nutrient_json,histamine_score,
            histamine_max,histamine_unknown_count,histamine_label,updated_at)
           VALUES(?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)""",
        (
            day,
            daily_macros["kcal"],
            daily_macros["protein"],
            daily_macros["carb"],
            daily_macros["fat"],
            int(day_total["items"]),
            json.dumps(dict(sorted(day_total["nutrients"].items())), ensure_ascii=False),
            round(day_total["load"], 1),
            max_score,
            day_total["unknown"],
            label,
        ),
    )

def make_legacy_diary(con: sqlite3.Connection, day: str) -> None:
    rows = list(con.execute("""
        SELECT i.meal,i.name,i.amount,i.kcal,i.protein_g,i.carb_g,i.fat_g,h.traffic_light,h.canonical_food,h.sighi_score
        FROM nutrition_items i LEFT JOIN nutrition_histamine_scores h ON h.item_id=i.id
        WHERE i.datum=? AND i.source='yazio_api'
        ORDER BY CASE i.meal WHEN 'breakfast' THEN 1 WHEN 'lunch' THEN 2 WHEN 'dinner' THEN 3 WHEN 'snack' THEN 4 ELSE 9 END, i.id
    """, (day,)))
    totals = con.execute("SELECT * FROM nutrition_daily_summary_v2 WHERE datum=?", (day,)).fetchone()
    lines = []
    for r in rows:
        light = {"green":"🟢","yellow":"🟡","orange":"🟠","red":"🔴","unknown":"⚫"}.get(r["traffic_light"] or "unknown", "⚫")
        lines.append(
            f"- [{r['meal']}] {light} {r['name']} ({display_number(r['amount'], ' g')}): "
            f"{display_number(r['kcal'], ' kcal')}, {display_number(r['protein_g'], ' g')} Protein, "
            f"{display_number(r['carb_g'], ' g')} KH, {display_number(r['fat_g'], ' g')} Fett"
        )
    if totals:
        histamine = (
            display_number(totals["histamine_score"])
            if int(totals["histamine_unknown_count"] or 0) == 0
            else "unvollständig"
        )
        summary = (
            f"YAZIO-Ernährung {day}\n\n" + "\n".join(lines)
            + f"\n\nSumme: {display_number(totals['kcal'], ' kcal')} | "
            + f"{display_number(totals['protein_g'], ' g')} Protein | "
            + f"{display_number(totals['carb_g'], ' g')} Kohlenhydrate | "
            + f"{display_number(totals['fat_g'], ' g')} Fett | Histamin-Zuordnungsindex: {histamine}"
        )
    else:
        summary = f"YAZIO-Ernährung {day}\n\n" + "\n".join(lines)
    fp = hashlib.sha256(summary.encode()).hexdigest()
    con.execute("""
        INSERT INTO tagebuch(datum,kategori,titel,inhalt,wirkung,verknuepft,notizen)
        VALUES(?,?,?,?,?,?,?)
    """, (day, "ernaehrung", f"YAZIO-Ernährung: {day}", summary, "", "yazio_api", f"Automatisch via YAZIO API v2; fingerprint={fp}"))


def daterange(start: date, end: date) -> list[date]:
    if end < start:
        start, end = end, start
    days = []
    d = start
    while d <= end:
        days.append(d)
        d += timedelta(days=1)
    return days


def parse_day(s: str) -> date:
    return datetime.strptime(s, "%Y-%m-%d").date()


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--date", help="single YYYY-MM-DD date")
    ap.add_argument("--from", dest="from_date", help="backfill start YYYY-MM-DD")
    ap.add_argument("--to", dest="to_date", help="backfill end YYYY-MM-DD")
    ap.add_argument("--days", type=int, help="backfill last N days ending today")
    ap.add_argument("--force", action="store_true")
    ap.add_argument("--dashboard", action="store_true", help="refresh dashboard after changes")
    ap.add_argument("--verbose", action="store_true")
    args = ap.parse_args()

    if args.date:
        targets = [parse_day(args.date)]
    elif args.from_date or args.to_date:
        start = parse_day(args.from_date or args.to_date)
        end = parse_day(args.to_date or args.from_date)
        targets = daterange(start, end)
    elif args.days:
        end = date.today()
        start = end - timedelta(days=args.days - 1)
        targets = daterange(start, end)
    else:
        targets = [date.today(), date.today() - timedelta(days=1)]

    token = get_token()
    changed = []
    notes = []
    for d in targets:
        did, msg = sync_date(d.isoformat(), token, force=args.force)
        notes.append(msg)
        if did:
            changed.append(msg)
        if args.verbose:
            print(msg)

    if changed:
        correlations = subprocess.run([sys.executable, str(CORRELATIONS)], text=True, capture_output=True, timeout=180)
        if correlations.returncode != 0:
            print(correlations.stderr or correlations.stdout or f"Multimodal correlations failed with {correlations.returncode}", file=sys.stderr)
            return correlations.returncode

    if changed and args.dashboard:
        dash = subprocess.run([sys.executable, str(PIPELINE), "dashboard"], text=True, capture_output=True, timeout=180)
        if dash.returncode != 0:
            print(dash.stderr or dash.stdout or f"Dashboard refresh failed with {dash.returncode}", file=sys.stderr)
            return dash.returncode

    if changed:
        print("YAZIO-Nutrition-Sync aktualisiert: " + "; ".join(changed) + ("; Dashboard aktualisiert." if args.dashboard else ""))
    elif args.verbose:
        print("YAZIO-Nutrition-Sync: keine Änderungen (" + "; ".join(notes[:5]) + (" ..." if len(notes) > 5 else "") + ")")
    return 0


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