<?php
// ROI.php – Umsatz/Peak/Histogramm pro Artikel inkl. Bundleanteil + Extern-Umsatz
// + "Slot-Umsatz" (auftragsbasiert, nicht tagesbasiert) inkl. Bundleanteil in Modal
// liegt in /dist

require('../config.php');
require('../EpiApi.php');
require_once __DIR__ . '/../vendor/autoload.php';
date_default_timezone_set('Europe/Berlin');

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;

$Epi = new Epirent();

/* =========================
   Filter (Jahres-Range)
   – ROI wird nur ausgeführt, wenn der User einen Zeitraum gewählt hat
     oder explizit "Alles" anfordert. Das spart bei großen Datenbeständen
     teure API-Calls.
   ========================= */
$filterMode = strtolower((string)($_GET['mode'] ?? ''));
$filterFrom = isset($_GET['from']) ? (int)$_GET['from'] : 0;
$filterTo   = isset($_GET['to'])   ? (int)$_GET['to']   : 0;

if ($filterFrom > 0 && $filterTo > 0 && $filterTo < $filterFrom) {
    [$filterFrom, $filterTo] = [$filterTo, $filterFrom];
}

$computeRoi = in_array($filterMode, ['range', 'all'], true);
if ($filterMode === 'range' && ($filterFrom <= 0 || $filterTo <= 0)) {
    $computeRoi = false;
    $filterMode = '';
}

/* =========================
   Helpers
   ========================= */

function formatEuro(float $amount): string {
    $formatted = number_format(abs($amount), 2, ',', '.') . ' €';
    return $amount < 0 ? '-' . $formatted : $formatted;
}

function yearFromDate(?string $ymd): ?int {
    if (!$ymd || $ymd === '0000-00-00') return null;
    return (int)substr($ymd, 0, 4);
}

function safeYmd(?string $ymd): ?string {
    if (!$ymd || $ymd === '0000-00-00') return null;
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $ymd)) return null;
    return $ymd;
}

function ymdToTs(string $ymd): int {
    return (new DateTime($ymd . ' 00:00:00', new DateTimeZone('Europe/Berlin')))->getTimestamp();
}

function tsToYmd(int $ts): string {
    return (new DateTime('@' . $ts))->setTimezone(new DateTimeZone('Europe/Berlin'))->format('Y-m-d');
}

function base64Json($data): string {
    return base64_encode(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}

function apiJsonDecode($raw) {
    if (is_object($raw)) {
        if (method_exists($raw, 'getContents')) {
            $raw = $raw->getContents();
        } else {
            $raw = (string)$raw;
        }
    }
    if (!is_string($raw)) $raw = (string)$raw;
    return json_decode($raw);
}

/* =========================
   API Caches
   ========================= */

$productCache = [];       // [pk => productObj]
$bundleLeafCache = [];    // [bundlePk => [leafPk => amount]]
$rentPriceCache = [];     // [pk => float]

function getProduct(Epirent $Epi, int $productPk) {
    global $productCache;
    if ($productPk <= 0) return null;
    if (isset($productCache[$productPk])) return $productCache[$productPk];

    $res = apiJsonDecode($Epi->requestEpiApi('/v1/product/' . $productPk . '?cl=' . Epirent_Mandant));
    $prod = ($res && ($res->success ?? false) && !empty($res->payload[0])) ? $res->payload[0] : null;
    $productCache[$productPk] = $prod;
    return $prod;
}

function getRentPrice(Epirent $Epi, int $productPk): float {
    global $rentPriceCache;
    if (isset($rentPriceCache[$productPk])) return $rentPriceCache[$productPk];

    $p = getProduct($Epi, $productPk);
    $price = 0.0;
    if ($p && isset($p->pricing) && isset($p->pricing->price_rent)) {
        $price = (float)$p->pricing->price_rent;
    }
    $rentPriceCache[$productPk] = $price;
    return $price;
}

/**
 * WICHTIG: Bundle-Auflösung NUR für virtuelle Bundles.
 * Ergebnis: [leafProductPk => amount]
 */
function resolveBundleLeafMap(Epirent $Epi, int $productPk, array &$stack = []): array {
    global $bundleLeafCache;

    if ($productPk <= 0) return [];
    if (isset($bundleLeafCache[$productPk])) return $bundleLeafCache[$productPk];

    if (isset($stack[$productPk])) {
        return [];
    }
    $stack[$productPk] = true;

    $p = getProduct($Epi, $productPk);
    if (!$p) {
        unset($stack[$productPk]);
        return [];
    }

    // NUR virtuelle Bundles zerlegen
    $isVirtual = (bool)($p->is_virtual ?? false);
    if (!$isVirtual) {
        unset($stack[$productPk]);
        $bundleLeafCache[$productPk] = [$productPk => 1.0];
        return $bundleLeafCache[$productPk];
    }

    $mats = [];
    if (!empty($p->materials_ext) && !empty($p->materials_ext->rent_fix) && is_array($p->materials_ext->rent_fix)) {
        $mats = $p->materials_ext->rent_fix;
    } elseif (!empty($p->materials) && is_array($p->materials)) {
        $mats = $p->materials;
    }

    // virtuell aber ohne Komponenten => leaf
    if (empty($mats)) {
        unset($stack[$productPk]);
        $bundleLeafCache[$productPk] = [$productPk => 1.0];
        return $bundleLeafCache[$productPk];
    }

    $leaf = [];
    foreach ($mats as $m) {
        $childPk = (int)($m->mat_product_pk ?? 0);
        $amt = (float)($m->amount ?? 1);
        if ($childPk <= 0 || $amt <= 0) continue;

        $childLeaf = resolveBundleLeafMap($Epi, $childPk, $stack);
        foreach ($childLeaf as $leafPk => $leafAmt) {
            if (!isset($leaf[$leafPk])) $leaf[$leafPk] = 0.0;
            $leaf[$leafPk] += $amt * (float)$leafAmt;
        }
    }

    unset($stack[$productPk]);

    if (empty($leaf)) $leaf = [$productPk => 1.0];

    $bundleLeafCache[$productPk] = $leaf;
    return $leaf;
}

/**
 * Bundle-Umsatz anteilig auf leaf-Produkte verteilen.
 * Gewichtung: rentPrice(leaf) * amount
 * Fallback: gleichverteilt nach amount
 */
function allocateBundleRevenue(Epirent $Epi, int $bundlePk, float $bundleRevenueNet): array {
    $leafMap = resolveBundleLeafMap($Epi, $bundlePk);
    if (empty($leafMap)) return [];

    // wenn NICHT virtuell (leafMap == self) => dann keine Allocation (weil kein Bundle)
    if (count($leafMap) === 1 && isset($leafMap[$bundlePk])) {
        return [];
    }

    $weights = [];
    $sumW = 0.0;

    foreach ($leafMap as $leafPk => $amt) {
        $rp = getRentPrice($Epi, (int)$leafPk);
        $w = $rp * (float)$amt;
        $weights[$leafPk] = $w;
        $sumW += $w;
    }

    if ($sumW <= 0.0) {
        $sumAmt = array_sum($leafMap);
        if ($sumAmt <= 0.0) $sumAmt = (float)count($leafMap);
        $alloc = [];
        foreach ($leafMap as $leafPk => $amt) {
            $alloc[$leafPk] = $bundleRevenueNet * ((float)$amt / $sumAmt);
        }
        return $alloc;
    }

    $alloc = [];
    foreach ($weights as $leafPk => $w) {
        $alloc[$leafPk] = $bundleRevenueNet * ($w / $sumW);
    }
    return $alloc;
}

/* =========================
   Journal Cache
   ========================= */

$journalCache = [];
function getJournalByChapter(Epirent $Epi, int $chapterId): array {
    global $journalCache;

    if ($chapterId <= 0) return [];
    if (isset($journalCache[$chapterId])) return $journalCache[$chapterId];

    $url = '/v1/journal/filter?chid=' . $chapterId . '&cl=' . Epirent_Mandant;
    $res = apiJsonDecode($Epi->requestEpiApi($url));
    $payload = ($res && ($res->success ?? false) && isset($res->payload) && is_array($res->payload)) ? $res->payload : [];

    $journalCache[$chapterId] = $payload;
    return $payload;
}

/* =========================
   Interval aggregation (Peak/Histogram) – tagesbasiert
   ========================= */

$eventsDirect = []; // eventsDirect[productPk][ymd] += deltaQty
$eventsIncl   = []; // direct + bundle

function addIntervalEvent(array &$events, int $productPk, string $startYmd, string $endYmd, float $qty): void {
    if ($productPk <= 0) return;
    $startYmd = safeYmd($startYmd);
    $endYmd = safeYmd($endYmd);
    if (!$startYmd || !$endYmd) return;
    if ($qty == 0.0) return;

    $startTs = ymdToTs($startYmd);
    $endTs   = ymdToTs($endYmd);
    if ($endTs < $startTs) return;

    $endPlusTs = $endTs + 86400;
    $endPlusYmd = tsToYmd($endPlusTs);

    if (!isset($events[$productPk])) $events[$productPk] = [];
    if (!isset($events[$productPk][$startYmd])) $events[$productPk][$startYmd] = 0.0;
    if (!isset($events[$productPk][$endPlusYmd])) $events[$productPk][$endPlusYmd] = 0.0;

    $events[$productPk][$startYmd] += $qty;
    $events[$productPk][$endPlusYmd] -= $qty;
}

function computePeakAndHistogram(array $eventMap): array {
    if (empty($eventMap)) return ['peak' => 0, 'hist' => []];

    ksort($eventMap);
    $dates = array_keys($eventMap);

    $level = 0.0;
    $peak = 0.0;
    $hist = [];

    for ($i = 0; $i < count($dates); $i++) {
        $d = $dates[$i];
        $level += (float)$eventMap[$d];

        // clamp, damit kein negativer Bestand "Tage" erzeugt
        if ($level < 0) $level = 0;

        $intLevel = (int)round($level);
        if ($intLevel > $peak) $peak = $intLevel;

        if ($i < count($dates) - 1) {
            $dTs = ymdToTs($d);
            $nTs = ymdToTs($dates[$i + 1]);
            $days = ($nTs - $dTs) / 86400.0;
            if ($days > 0 && $intLevel > 0) {
                if (!isset($hist[$intLevel])) $hist[$intLevel] = 0.0;
                $hist[$intLevel] += $days;
            }
        }
    }

    ksort($hist);
    foreach ($hist as $k => $v) $hist[$k] = (int)round($v);

    return ['peak' => (int)$peak, 'hist' => $hist];
}

/* =========================
   Data aggregation
   ========================= */

$rows = [];        // Debug rows (HTML only)
$pivot = [];       // direct revenue per year (net)
$pivotB = [];      // bundle revenue per year allocated to leaf (net)
$pivotExt = [];    // extern revenue per year (net) direct
$pivotExtB = [];   // extern revenue per year allocated (net) for virtual bundles

$meta = [];        // product meta [pk=>['product_no'=>..,'title'=>..]]
$allYears = [];

// Auftragsbasierte "Slots" (direct/incl) pro Produkt
$slotAgg = [];

function ensureMeta(Epirent $Epi, int $productPk, int $productNo, string $title): void {
    global $meta;
    if (!isset($meta[$productPk])) {
        $p = getProduct($Epi, $productPk);
        $meta[$productPk] = [
            'product_pk' => $productPk,
            'product_no' => $productNo ?: (int)($p->product_no ?? 0),
            'title'      => $title ?: (string)($p->name ?? ''),
        ];
    }
}

function addRevenue(array &$pivotRef, int $productPk, int $year, float $revenueNet): void {
    if ($productPk <= 0 || !$year) return;
    if (!isset($pivotRef[$productPk])) $pivotRef[$productPk] = [];
    if (!isset($pivotRef[$productPk][$year])) $pivotRef[$productPk][$year] = 0.0;
    $pivotRef[$productPk][$year] += $revenueNet;
}

/**
 * Extern-Logik:
 * - Amount External ist eine ANZAHL (nicht Umsatz).
 * - Wir berechnen extRevenue = sum_total_net * (amount_external / amount_total) (wenn amount_total>0)
 */
function calcExternalRevenueNet(object $li): float {
    $amountTotal = (float)($li->amount_total ?? 0);
    $amountExternal = (float)($li->amount_external ?? 0);
    $net = (float)($li->sum_total_net ?? 0);

    if ($net == 0.0) return 0.0;
    if ($amountTotal <= 0.0) return 0.0;
    if ($amountExternal <= 0.0) return 0.0;

    if ($amountExternal > $amountTotal) $amountExternal = $amountTotal;

    return $net * ($amountExternal / $amountTotal);
}

/* =========================
   Slot (auftragsbasiert) – Storno-sicher (SIGNED)
   ========================= */

/**
 * Intervall-Item für Slotting (ein "Auftragsteil" pro Produkt)
 * qty kann positiv oder negativ sein (negativ = Storno/Rückbuchung)
 */
function addSlotItem(
    array &$slotItems,
    int $productPk,
    int $invoicePk,
    int $orderPk,
    int $invoiceNo,
    string $startYmd,
    string $endYmd,
    float $qty,
    float $revenueNet,
    float $extRevenueNet
): void {
    $startYmd = safeYmd($startYmd);
    $endYmd   = safeYmd($endYmd);
    if ($productPk <= 0) return;
    if (!$startYmd || !$endYmd) return;
    if ($qty == 0.0) return;

    if (!isset($slotItems[$productPk])) $slotItems[$productPk] = [];

    $slotItems[$productPk][] = [
        'product_pk'  => $productPk,
        'invoice_pk'  => $invoicePk,
        'order_pk'    => $orderPk,
        'invoice_no'  => $invoiceNo,
        'start_ts'    => ymdToTs($startYmd),
        'end_ts'      => ymdToTs($endYmd),
        'qty'         => (float)$qty,           // signed erlaubt
        'rev'         => (float)$revenueNet,
        'rev_ext'     => (float)$extRevenueNet,
        'start_ymd'   => $startYmd,
        'end_ymd'     => $endYmd,
    ];
}

/**
 * Slotting (SIGNED, Storno gibt Slots wieder frei):
 * - Positive qty belegt Slots.
 * - Negative qty bucht Umsatz zurück UND gibt Slots frei.
 * - Priorität bei Storno: zuerst Slots mit gleichem Zeitraum (gleicher release_ts) abbauen.
 *
 * Ergebnis:
 *  - 'direct' => [slot => revenue]
 *  - 'direct_ext' => [slot => extRevenue]
 */
function computeOrderBasedSlots(array $itemsForProduct): array {
    if (empty($itemsForProduct)) {
        return ['direct' => [], 'direct_ext' => []];
    }

    // Sortierung: start ASC, end DESC, invoice_no ASC, invoice_pk ASC
    usort($itemsForProduct, function($a, $b){
        if (($a['start_ts'] ?? 0) !== ($b['start_ts'] ?? 0)) return ($a['start_ts'] ?? 0) <=> ($b['start_ts'] ?? 0);
        if (($a['end_ts'] ?? 0) !== ($b['end_ts'] ?? 0)) return ($b['end_ts'] ?? 0) <=> ($a['end_ts'] ?? 0);
        if (($a['invoice_no'] ?? 0) !== ($b['invoice_no'] ?? 0)) return ($a['invoice_no'] ?? 0) <=> ($b['invoice_no'] ?? 0);
        return ($a['invoice_pk'] ?? 0) <=> ($b['invoice_pk'] ?? 0);
    });

    // Active slots: slotIndex => release_ts
    $active = [];
    // release_ts => [slotIndex, ...]
    $activeByRelease = [];
    // free slot indices
    $freeSlots = [];

    // Revenues per slot
    $slotRevenue = [];
    $slotRevenueExt = [];

    $releaseTsFor = function(int $endTs): int {
        // Ende inklusiv -> frei am Folgetag 00:00
        return $endTs + 86400;
    };

    $freeExpired = function(int $currentStartTs) use (&$active, &$activeByRelease, &$freeSlots) {
        if (empty($active)) return;
        foreach ($active as $slot => $relTs) {
            if ($relTs <= $currentStartTs) {
                unset($active[$slot]);
                if (isset($activeByRelease[$relTs])) {
                    $activeByRelease[$relTs] = array_values(array_filter(
                        $activeByRelease[$relTs],
                        fn($s) => (int)$s !== (int)$slot
                    ));
                    if (empty($activeByRelease[$relTs])) unset($activeByRelease[$relTs]);
                }
                $freeSlots[] = (int)$slot;
            }
        }
    };

    $takeFreeSlot = function() use (&$freeSlots, &$active): int {
        if (!empty($freeSlots)) {
            sort($freeSlots);
            return (int)array_shift($freeSlots);
        }
        if (empty($active)) return 1;
        $keys = array_keys($active);
        return (int)(max($keys) + 1);
    };

    foreach ($itemsForProduct as $it) {
        $startTs = (int)($it['start_ts'] ?? 0);
        $endTs   = (int)($it['end_ts'] ?? 0);
        if ($startTs <= 0 || $endTs <= 0) continue;
        if ($endTs < $startTs) continue;

        // vor jeder Aktion: abgelaufene Slots freigeben
        $freeExpired($startTs);

        $qtySigned = (float)($it['qty'] ?? 0.0);
        if ($qtySigned == 0.0) continue;

        $qtyAbs = (int)round(abs($qtySigned));
        if ($qtyAbs <= 0) continue;

        $rev = (float)($it['rev'] ?? 0.0);
        $ext = (float)($it['rev_ext'] ?? 0.0);

        $revPer = $rev / $qtyAbs;
        $extPer = $ext / $qtyAbs;

        $relTs = $releaseTsFor($endTs);

        if ($qtySigned > 0) {
            // --- Belegen ---
            for ($k = 0; $k < $qtyAbs; $k++) {
                $slot = $takeFreeSlot();

                $active[$slot] = $relTs;
                if (!isset($activeByRelease[$relTs])) $activeByRelease[$relTs] = [];
                $activeByRelease[$relTs][] = $slot;

                if (!isset($slotRevenue[$slot])) $slotRevenue[$slot] = 0.0;
                if (!isset($slotRevenueExt[$slot])) $slotRevenueExt[$slot] = 0.0;

                $slotRevenue[$slot] += $revPer;
                $slotRevenueExt[$slot] += $extPer;
            }
        } else {
            // --- Storno / Freigeben ---
            // erst gleiche release_ts abbauen (damit "Rechnung+Storno im gleichen Zeitraum" sauber 0 Slots macht)
            $candidates = $activeByRelease[$relTs] ?? [];

            for ($k = 0; $k < $qtyAbs; $k++) {
                $slot = null;

                if (!empty($candidates)) {
                    rsort($candidates); // höchste Slotnummer zuerst abbauen
                    $slot = (int)array_shift($candidates);
                } else {
                    if (!empty($active)) {
                        $keys = array_keys($active);
                        rsort($keys);
                        $slot = (int)$keys[0];
                    }
                }

                if ($slot === null) break;

                // Umsatz auf Slot zurückbuchen
                if (!isset($slotRevenue[$slot])) $slotRevenue[$slot] = 0.0;
                if (!isset($slotRevenueExt[$slot])) $slotRevenueExt[$slot] = 0.0;
                $slotRevenue[$slot] += $revPer;      // revPer ist i.d.R. negativ
                $slotRevenueExt[$slot] += $extPer;  // extPer ist i.d.R. negativ

                // Slot freigeben
                $oldRel = $active[$slot] ?? null;
                unset($active[$slot]);

                if ($oldRel !== null && isset($activeByRelease[$oldRel])) {
                    $activeByRelease[$oldRel] = array_values(array_filter(
                        $activeByRelease[$oldRel],
                        fn($s) => (int)$s !== (int)$slot
                    ));
                    if (empty($activeByRelease[$oldRel])) unset($activeByRelease[$oldRel]);
                }

                $freeSlots[] = $slot;
            }
        }
    }

    // --- Cleanup: Slots ohne Umsatz entfernen + neu durchnummerieren ---
    $eps = 0.00001;
    $allSlots = array_unique(array_merge(array_keys($slotRevenue), array_keys($slotRevenueExt)));
    sort($allSlots);

    $kept = [];
    foreach ($allSlots as $s) {
        $rev = (float)($slotRevenue[$s] ?? 0.0);
        $ext = (float)($slotRevenueExt[$s] ?? 0.0);
        if (abs($rev) < $eps && abs($ext) < $eps) continue;
        $kept[] = $s;
    }

    $newRev = [];
    $newExt = [];
    $idx = 1;
    foreach ($kept as $old) {
        $newRev[$idx] = (float)($slotRevenue[$old] ?? 0.0);
        $newExt[$idx] = (float)($slotRevenueExt[$old] ?? 0.0);
        $idx++;
    }

    return ['direct' => $newRev, 'direct_ext' => $newExt];
}

/* =========================
   Prozessierung einer Artikelzeile
   ========================= */

function processLineItem(
    Epirent $Epi,
    array &$rows,
    array &$slotItemsDirect,
    array &$slotItemsIncl,
    int $orderPk,
    int $invoicePk,
    int $invoiceNo,
    string $invoiceDate,
    int $invoiceYear,
    ?int $chapterId,
    object $li,
    bool $isFromJournal,
    bool $invoiceIsCredit // <-- wichtig: Storno-Rechnung (z.B. sum_net < 0)
): void {
    global $pivot, $pivotB, $pivotExt, $pivotExtB, $allYears, $eventsDirect, $eventsIncl;

    $type = (int)($li->type ?? -1);
    $productPk = (int)($li->product_pk ?? 0);
    if ($type !== 0 || $productPk <= 0) return; // nur echte Artikelpositionen

    $title = (string)($li->title ?? '');
    $productNo = (int)($li->product_no ?? 0);

    $qtyRaw = (float)($li->amount_total ?? 0);
    if ($qtyRaw == 0.0) $qtyRaw = 0.0;

    // Umsatz bleibt wie geliefert (kann negativ sein)
    $revenueNet = (float)($li->sum_total_net ?? 0);
    $extRevenueNet = calcExternalRevenueNet($li);

    // Für Auslastung/Slots: wenn Storno-Rechnung, Menge "negativ signieren"
    // (weil Epi bei Storno typischerweise Menge positiv lässt, aber Preise negativ macht)
    $qtySigned = $qtyRaw;
    if ($invoiceIsCredit && $qtySigned != 0.0) {
        $qtySigned = -abs($qtySigned);
    }

    $dateStart = safeYmd((string)($li->date_start ?? '')) ?: null;
    $dateEnd   = safeYmd((string)($li->date_end ?? '')) ?: null;

    ensureMeta($Epi, $productPk, $productNo, $title);

    // Jahr: Rechnungsdatum
    $year = $invoiceYear;
    if ($year) $allYears[$year] = true;

    // Direct Umsatz
    if ($revenueNet != 0.0 && $year) addRevenue($pivot, $productPk, $year, $revenueNet);

    // Direct Extern-Umsatz
    if ($extRevenueNet != 0.0 && $year) addRevenue($pivotExt, $productPk, $year, $extRevenueNet);

    // Peak/Histogram direct + incl (tagesbasiert)
    // -> qtySigned sorgt dafür, dass Storno zeitgleich aufhebt
    if ($dateStart && $dateEnd && $qtySigned != 0.0) {
        addIntervalEvent($eventsDirect, $productPk, $dateStart, $dateEnd, $qtySigned);
        addIntervalEvent($eventsIncl,   $productPk, $dateStart, $dateEnd, $qtySigned);
    }

    // Slotting: direct (signed qty)
    if ($dateStart && $dateEnd && $qtySigned != 0.0) {
        addSlotItem($slotItemsDirect, $productPk, $invoicePk, $orderPk, $invoiceNo, $dateStart, $dateEnd, $qtySigned, $revenueNet, $extRevenueNet);
        addSlotItem($slotItemsIncl,   $productPk, $invoicePk, $orderPk, $invoiceNo, $dateStart, $dateEnd, $qtySigned, $revenueNet, $extRevenueNet);
    }

    // Debug Row
    $rows[] = [
        'year'        => $year,
        'invoice_pk'  => $invoicePk,
        'invoice_no'  => $invoiceNo,
        'invoice_date'=> $invoiceDate,
        'chapter_id'  => $chapterId ?? 0,
        'source'      => $isFromJournal ? 'journal' : 'direct',
        'product_pk'  => $productPk,
        'product_no'  => $productNo,
        'title'       => $title,
        'date_start'  => $dateStart ?? '',
        'date_end'    => $dateEnd ?? '',
        'qty'         => $qtyRaw,
        'qty_signed'  => $qtySigned,
        'revenue_net' => $revenueNet,
        'ext_rev_net' => $extRevenueNet,
        'amount_ext'  => (float)($li->amount_external ?? 0),
        'amount_total'=> (float)($li->amount_total ?? 0),
        'is_credit'   => $invoiceIsCredit ? 1 : 0,
    ];

    /* ===== Bundle-Auflösung (nur virtuelle Bundles!) ===== */
    $leafMap = resolveBundleLeafMap($Epi, $productPk);
    $isVirtualBundle = !(count($leafMap) === 1 && isset($leafMap[$productPk]) && (float)$leafMap[$productPk] === 1.0);

    if ($isVirtualBundle) {
        // Umsatz allokieren (inkl Bundle-Anteil)
        if ($revenueNet != 0.0 && $year) {
            $alloc = allocateBundleRevenue($Epi, $productPk, $revenueNet);
            foreach ($alloc as $leafPk => $leafRevenue) {
                $leafPk = (int)$leafPk;
                ensureMeta($Epi, $leafPk, 0, '');
                addRevenue($pivotB, $leafPk, $year, (float)$leafRevenue);
            }
        }

        // Extern-Umsatz allokieren (inkl Bundle-Anteil)
        if ($extRevenueNet != 0.0 && $year) {
            $allocExt = allocateBundleRevenue($Epi, $productPk, $extRevenueNet);
            foreach ($allocExt as $leafPk => $leafRevenue) {
                $leafPk = (int)$leafPk;
                ensureMeta($Epi, $leafPk, 0, '');
                addRevenue($pivotExtB, $leafPk, $year, (float)$leafRevenue);
            }
        }

        // Auslastung allokieren (Peak/Histogramm inkl Bundle) – signed qty!
        if ($dateStart && $dateEnd && $qtySigned != 0.0) {
            foreach ($leafMap as $leafPk => $amt) {
                $leafPk = (int)$leafPk;
                $amt = (float)$amt;
                if ($leafPk <= 0 || $amt <= 0) continue;
                ensureMeta($Epi, $leafPk, 0, '');
                addIntervalEvent($eventsIncl, $leafPk, $dateStart, $dateEnd, $qtySigned * $amt);
            }
        }

        // Slotting allokieren (inkl Bundle) – signed qty!
        if ($dateStart && $dateEnd && $qtySigned != 0.0) {
            $alloc = ($revenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $revenueNet) : [];
            $allocExt = ($extRevenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $extRevenueNet) : [];

            foreach ($leafMap as $leafPk => $amt) {
                $leafPk = (int)$leafPk;
                $amt = (float)$amt;
                if ($leafPk <= 0 || $amt <= 0) continue;

                $leafRev = (float)($alloc[$leafPk] ?? 0.0);
                $leafExt = (float)($allocExt[$leafPk] ?? 0.0);

                $qtyLeafSigned = $qtySigned * $amt;

                ensureMeta($Epi, $leafPk, 0, '');
                addSlotItem($slotItemsIncl, $leafPk, $invoicePk, $orderPk, $invoiceNo, $dateStart, $dateEnd, $qtyLeafSigned, $leafRev, $leafExt);
            }
        }
    }
}

/* =========================
   1) Invoices holen + Daten sammeln
   ========================= */

// Defaults für die HTML-Ausgabe, wenn keine Berechnung läuft
$years            = [];
$pivotRows        = [];
$excelFileName    = '';
$excelDownloadUrl = '';

if ($computeRoi) {

$slotItemsDirect = []; // productPk => list of intervals
$slotItemsIncl   = []; // productPk => list of intervals

$invoiceAll = apiJsonDecode($Epi->requestEpiApi('/v1/invoice/all?ir=true&ib=true&cl=' . Epirent_Mandant));
$invoiceList = ($invoiceAll && ($invoiceAll->success ?? false) && is_array($invoiceAll->payload ?? null)) ? $invoiceAll->payload : [];

foreach ($invoiceList as $inv) {
    $invoicePk = (int)($inv->primary_key ?? 0);
    if ($invoicePk <= 0) continue;

    // Vorab-Filter: wenn das Listen-Item bereits invoice_date hat, sparen wir den teuren Detail-Call
    if ($filterMode === 'range') {
        $listYear = yearFromDate((string)($inv->invoice_date ?? '')) ?? 0;
        if ($listYear > 0) {
            if ($filterFrom > 0 && $listYear < $filterFrom) continue;
            if ($filterTo   > 0 && $listYear > $filterTo)   continue;
        }
    }

    $invRes = apiJsonDecode($Epi->requestEpiApi('/v1/invoice/' . $invoicePk . '?cl=' . Epirent_Mandant));
    $invObj = ($invRes && ($invRes->success ?? false) && !empty($invRes->payload[0])) ? $invRes->payload[0] : null;
    if (!$invObj) continue;

    $invoiceDate = (string)($invObj->invoice_date ?? '');
    $invoiceYear = yearFromDate($invoiceDate) ?? 0;
    $invoiceNo   = (int)($invObj->invoice_no ?? 0);
    $orderPk     = (int)($invObj->order_pk ?? 0);

    // Endgültiger Year-Filter (falls Listen-Antwort kein Datum hatte)
    if ($filterMode === 'range') {
        if ($invoiceYear <= 0) continue;
        if ($filterFrom > 0 && $invoiceYear < $filterFrom) continue;
        if ($filterTo   > 0 && $invoiceYear > $filterTo)   continue;
    }

    // Storno-Erkennung (Credit Note): sum_net < 0
    $invoiceIsCredit = ((float)($invObj->sum_net ?? 0.0)) < 0.0;

    $orderItems = $invObj->order_items ?? [];
    if (!is_array($orderItems)) $orderItems = [];

    foreach ($orderItems as $oi) {
        $oiType = (int)($oi->type ?? -1);
        $oiPk   = (int)($oi->primary_key ?? 0);

        // Kapitel (type=5): chid ist primary_key der Kapitelzeile
        if ($oiType === 5 && $oiPk > 0) {
            $chapterId = $oiPk;
            $journalItems = getJournalByChapter($Epi, $chapterId);

            foreach ($journalItems as $ji) {
                processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $orderPk, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, $chapterId, $ji, true, $invoiceIsCredit);
            }
            continue;
        }

        // Direkte Artikelposition ohne Kapitel (type=0)
        if ($oiType === 0 && (int)($oi->product_pk ?? 0) > 0) {
            processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $orderPk, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, null, $oi, false, $invoiceIsCredit);
            continue;
        }
    }
}

/* =========================
   2) Slot Aggregation (auftragsbasiert)
   ========================= */

foreach ($meta as $productPk => $m) {
    $productPk = (int)$productPk;

    $resD = computeOrderBasedSlots($slotItemsDirect[$productPk] ?? []);
    $resI = computeOrderBasedSlots($slotItemsIncl[$productPk] ?? []);

    $slotAgg[$productPk] = [
        'direct'     => $resD['direct'] ?? [],
        'incl'       => $resI['direct'] ?? [],
        'direct_ext' => $resD['direct_ext'] ?? [],
        'incl_ext'   => $resI['direct_ext'] ?? [],
    ];
}

/* =========================
   3) Years + Pivot Rows bauen
   ========================= */

$years = array_keys($allYears);
sort($years);

$pivotRows = [];

foreach ($meta as $productPk => $m) {
    $productPk = (int)$productPk;

    $row = [
        'product_pk' => $productPk,
        'product_no' => (int)($m['product_no'] ?? 0),
        'title'      => (string)($m['title'] ?? ''),
        'years'      => [],
        'years_ext'  => [],
        'total_direct'=> 0.0,
        'total_incl'  => 0.0,
        'total_ext_direct'=> 0.0,
        'total_ext_incl'  => 0.0,
        'peak_direct' => 0,
        'peak_incl'   => 0,
        'hist_direct' => [],
        'hist_incl'   => [],
        'slots_direct' => $slotAgg[$productPk]['direct'] ?? [],
        'slots_incl'   => $slotAgg[$productPk]['incl'] ?? [],
        'slots_direct_ext' => $slotAgg[$productPk]['direct_ext'] ?? [],
        'slots_incl_ext'   => $slotAgg[$productPk]['incl_ext'] ?? [],
    ];

    $sumDirect = 0.0;
    $sumIncl = 0.0;
    $sumExtDirect = 0.0;
    $sumExtIncl = 0.0;

    foreach ($years as $y) {
        $d  = (float)($pivot[$productPk][$y]  ?? 0.0);
        $b  = (float)($pivotB[$productPk][$y] ?? 0.0);
        $incl = $d + $b;

        $de = (float)($pivotExt[$productPk][$y]  ?? 0.0);
        $be = (float)($pivotExtB[$productPk][$y] ?? 0.0);
        $inclE = $de + $be;

        $row['years'][$y] = ['direct'=>$d, 'incl'=>$incl];
        $row['years_ext'][$y] = ['direct'=>$de, 'incl'=>$inclE];

        $sumDirect += $d;
        $sumIncl   += $incl;
        $sumExtDirect += $de;
        $sumExtIncl   += $inclE;
    }

    $row['total_direct'] = $sumDirect;
    $row['total_incl']   = $sumIncl;
    $row['total_ext_direct'] = $sumExtDirect;
    $row['total_ext_incl']   = $sumExtIncl;

    $pd = computePeakAndHistogram($GLOBALS['eventsDirect'][$productPk] ?? []);
    $pi = computePeakAndHistogram($GLOBALS['eventsIncl'][$productPk] ?? []);
    $row['peak_direct'] = (int)$pd['peak'];
    $row['peak_incl']   = (int)$pi['peak'];
    $row['hist_direct'] = $pd['hist'];
    $row['hist_incl']   = $pi['hist'];

    $hasAny = (
        $row['total_direct'] != 0.0 ||
        $row['total_incl'] != 0.0 ||
        $row['total_ext_direct'] != 0.0 ||
        $row['total_ext_incl'] != 0.0 ||
        $row['peak_direct'] != 0 ||
        $row['peak_incl'] != 0
    );
    if (!$hasAny) continue;

    $pivotRows[] = $row;
}

usort($pivotRows, fn($a,$b) => ($b['total_incl'] <=> $a['total_incl']));

/* =========================
   4) Excel Export (nur Pivot)
   ========================= */

$exportDir = __DIR__ . '/exports';
if (!is_dir($exportDir)) {
    @mkdir($exportDir, 0775, true);
}
$timestamp = date('Ymd_His');
$excelFileName = "ROI_Artikel_Jahre_{$timestamp}.xlsx";
$excelFilePath = $exportDir . '/' . $excelFileName;
$excelDownloadUrl = 'exports/' . $excelFileName;

$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Pivot');

$cols = [
    'Produkt-PK','Artikel-Nr','Artikel',
    'Peak','Peak (inkl. Bundle)',
    'Summe','Summe (inkl. Bundle)',
    'davon extern','davon extern (inkl. Bundle)'
];

foreach ($years as $y) {
    $cols[] = (string)$y;
    $cols[] = (string)$y . ' (inkl. Bundle)';
    $cols[] = (string)$y . ' ext';
    $cols[] = (string)$y . ' ext (inkl. Bundle)';
}

$colCount = count($cols);
$lastColLetter = Coordinate::stringFromColumnIndex($colCount);

// Header
$sheet->setCellValue('A1', 'EpiWebview – ROI / Umsatz pro Artikel & Jahr');
$sheet->mergeCells('A1:' . $lastColLetter . '1');
$sheet->getStyle('A1:' . $lastColLetter . '1')->getFont()->setBold(true)->setSize(14);
$sheet->getStyle('A1:' . $lastColLetter . '1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);

$sheet->setCellValue('A2', 'Exportdatum: ' . date('d.m.Y H:i') . ' | Mandant: ' . (defined('Epirent_Mandant') ? Epirent_Mandant : ''));
$sheet->mergeCells('A2:' . $lastColLetter . '2');
$sheet->getStyle('A2:' . $lastColLetter . '2')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);

$headerRow = 4;
for ($i=0; $i<$colCount; $i++) {
    $addr = Coordinate::stringFromColumnIndex($i+1) . $headerRow;
    $sheet->setCellValue($addr, $cols[$i]);
    $sheet->getStyle($addr)->getFont()->setBold(true);
    $sheet->getStyle($addr)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
    $sheet->getColumnDimension(Coordinate::stringFromColumnIndex($i+1))->setAutoSize(true);
}

$rowIdx = $headerRow + 1;
foreach ($pivotRows as $r) {
    $c = 1;
    $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['product_pk'], DataType::TYPE_NUMERIC);
    $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['product_no'], DataType::TYPE_NUMERIC);
    $sheet->setCellValue(Coordinate::stringFromColumnIndex($c++).$rowIdx, (string)$r['title']);

    $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['peak_direct'], DataType::TYPE_NUMERIC);
    $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['peak_incl'], DataType::TYPE_NUMERIC);

    $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, (float)$r['total_direct'], DataType::TYPE_NUMERIC);
    $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
    $c++;

    $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, (float)$r['total_incl'], DataType::TYPE_NUMERIC);
    $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
    $c++;

    $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, (float)$r['total_ext_direct'], DataType::TYPE_NUMERIC);
    $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
    $c++;

    $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, (float)$r['total_ext_incl'], DataType::TYPE_NUMERIC);
    $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
    $c++;

    foreach ($years as $y) {
        $d = (float)$r['years'][$y]['direct'];
        $iVal = (float)$r['years'][$y]['incl'];

        $de = (float)$r['years_ext'][$y]['direct'];
        $ie = (float)$r['years_ext'][$y]['incl'];

        $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, $d, DataType::TYPE_NUMERIC);
        $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
        $c++;

        $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, $iVal, DataType::TYPE_NUMERIC);
        $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
        $c++;

        $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, $de, DataType::TYPE_NUMERIC);
        $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
        $c++;

        $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, $ie, DataType::TYPE_NUMERIC);
        $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
        $c++;
    }

    $rowIdx++;
}

$sheet->setAutoFilter("A{$headerRow}:{$lastColLetter}{$headerRow}");
$sheet->freezePane("A" . ($headerRow + 1));

$writer = new Xlsx($spreadsheet);
$writer->save($excelFilePath);

} // end if ($computeRoi)

/* =========================
   5) HTML Ausgabe
   ========================= */

function slotsToDisplay(array $slotMap): array {
    ksort($slotMap);
    $out = [];
    foreach ($slotMap as $slot => $rev) {
        $slot = (int)$slot;
        if ($slot <= 0) continue;
        $v = (float)$rev;
        if (abs($v) < 0.0000001) $v = 0.0;
        $out[] = ['slot'=>$slot, 'rev'=>$v];
    }
    return $out;
}

?>
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>ROI – Umsatz pro Artikel / Jahr</title>

    <link href="css/styles.css" rel="stylesheet" />
    <link href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css" rel="stylesheet" crossorigin="anonymous" />

    <script src="js/jquery-3.5.1.min.js"></script>
    <script src="https://kit.fontawesome.com/93d71de8bc.js" crossorigin="anonymous"></script>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>

    <script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js" crossorigin="anonymous"></script>
    <script src="https://cdn.datatables.net/1.10.20/js/dataTables.bootstrap4.min.js" crossorigin="anonymous"></script>

    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" crossorigin="anonymous"></script>

    <style>
        .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
        .btn-xs { padding:.15rem .4rem; font-size:.78rem; }
        .nowrap { white-space: nowrap; }
    </style>

    <script>
        $(function () {
            $('#layoutSidenav_nav').load('../sources/getSidenav.php');
            $('#footerholder').load('../sources/getFooter.php');
        });
    </script>
</head>

<body class="sb-nav-fixed">
<nav class="sb-topnav navbar navbar-expand navbar-dark bg-dark">
    <a class="navbar-brand" href="index.php">Epi Webview</a>
    <button class="btn btn-link btn-sm order-1 order-lg-0" id="sidebarToggle"><i class="fas fa-bars"></i></button>
</nav>

<div id="layoutSidenav">
    <div id="layoutSidenav_nav"></div>
    <div id="layoutSidenav_content">
        <main>
            <div class="container-fluid">
                <h1 class="mt-4">ROI</h1>
                <ol class="breadcrumb mb-4">
                    <li class="breadcrumb-item active">Umsatz / Peak / Histogramm pro Artikel</li>
                </ol>

                <!-- Zeitraum-Filter -->
                <div class="card mb-4">
                    <div class="card-header"><i class="fas fa-filter mr-1"></i> Zeitraum wählen</div>
                    <div class="card-body">
                        <form method="get" class="form-row align-items-end">
                            <div class="form-group col-md-3 mb-2">
                                <label for="from">Startjahr</label>
                                <input type="number" name="from" id="from" class="form-control" min="2000" max="2100"
                                       value="<?php echo $filterFrom > 0 ? (int)$filterFrom : ((int)date('Y') - 2); ?>">
                            </div>
                            <div class="form-group col-md-3 mb-2">
                                <label for="to">Endjahr</label>
                                <input type="number" name="to" id="to" class="form-control" min="2000" max="2100"
                                       value="<?php echo $filterTo > 0 ? (int)$filterTo : (int)date('Y'); ?>">
                            </div>
                            <div class="form-group col-md-3 mb-2">
                                <button type="submit" name="mode" value="range" class="btn btn-primary btn-block">
                                    <i class="fas fa-play"></i> Zeitraum berechnen
                                </button>
                            </div>
                            <div class="form-group col-md-3 mb-2">
                                <button type="submit" name="mode" value="all" class="btn btn-secondary btn-block">
                                    <i class="fas fa-infinity"></i> Alles berechnen
                                </button>
                            </div>
                        </form>
                        <small class="text-muted">
                            <?php if ($computeRoi && $filterMode === 'range'): ?>
                                <br><b>Aktuell:</b> <?php echo (int)$filterFrom; ?>&nbsp;–&nbsp;<?php echo (int)$filterTo; ?>
                            <?php elseif ($computeRoi && $filterMode === 'all'): ?>
                                <br><b>Aktuell:</b> alle Jahre
                            <?php endif; ?>
                        </small>
                    </div>
                </div>

<?php if (!$computeRoi): ?>
                <div class="alert alert-info">
                    Bitte oben einen Zeitraum auswählen und auf <b>Zeitraum berechnen</b> klicken,
                    oder <b>Alles berechnen</b> für die vollständige Auswertung.
                </div>
<?php else: ?>

                <div class="card mb-4">
                    <div class="card-header d-flex justify-content-between align-items-center">
                        <div><i class="fas fa-table mr-1"></i> Pivot: Artikel × Rechnungsjahr</div>
                        <a class="btn btn-sm btn-success" href="<?php echo htmlspecialchars($excelDownloadUrl); ?>">
                            <i class="fas fa-file-excel"></i> Export nach Excel
                        </a>
                    </div>
                    <div class="card-body">
                        <div class="table-responsive">
                            <table class="table table-bordered" id="pivotTable" width="100%" cellspacing="0">
                                <thead>
                                <tr>
                                    <th class="nowrap">Produkt-PK</th>
                                    <th class="nowrap">Artikel-Nr</th>
                                    <th>Artikel</th>
                                    <th class="nowrap">Peak</th>
                                    <th class="nowrap">Summe</th>
                                    <th class="nowrap">davon extern</th>
                                    <?php foreach ($years as $y): ?>
                                        <th class="nowrap"><?php echo (int)$y; ?></th>
                                    <?php endforeach; ?>
                                    <th class="nowrap">Histogramm</th>
                                    <th class="nowrap">Slots</th>
                                </tr>
                                </thead>
                                <tbody>
                                <?php foreach ($pivotRows as $r): ?>
                                    <?php
                                        $peakDirect = (int)$r['peak_direct'];
                                        $peakIncl   = (int)$r['peak_incl'];

                                        $histDirectB64 = base64Json($r['hist_direct']);
                                        $histInclB64   = base64Json($r['hist_incl']);

                                        $sumDirect = (float)$r['total_direct'];
                                        $sumIncl   = (float)$r['total_incl'];

                                        $extDirect = (float)$r['total_ext_direct'];
                                        $extIncl   = (float)$r['total_ext_incl'];

                                        $slotsDirect = slotsToDisplay($r['slots_direct'] ?? []);
                                        $slotsIncl   = slotsToDisplay($r['slots_incl'] ?? []);
                                        $slotsDirectExt = slotsToDisplay($r['slots_direct_ext'] ?? []);
                                        $slotsInclExt   = slotsToDisplay($r['slots_incl_ext'] ?? []);

                                        $slotsPayload = [
                                            'direct' => $slotsDirect,
                                            'incl' => $slotsIncl,
                                            'direct_ext' => $slotsDirectExt,
                                            'incl_ext' => $slotsInclExt,
                                        ];
                                        $slotsB64 = base64Json($slotsPayload);
                                    ?>
                                    <tr>
                                        <td class="mono"><?php echo (int)$r['product_pk']; ?></td>
                                        <td class="mono"><?php echo (int)$r['product_no']; ?></td>
                                        <td><?php echo htmlspecialchars((string)$r['title']); ?></td>

                                        <td class="nowrap">
                                            <?php echo $peakDirect; ?>
                                            <span class="text-muted">(<?php echo $peakIncl; ?>)</span>
                                        </td>

                                        <td class="nowrap">
                                            <?php echo formatEuro($sumDirect); ?>
                                            <span class="text-muted">(<?php echo formatEuro($sumIncl); ?>)</span>
                                        </td>

                                        <td class="nowrap">
                                            <?php echo formatEuro($extDirect); ?>
                                            <span class="text-muted">(<?php echo formatEuro($extIncl); ?>)</span>
                                        </td>

                                        <?php foreach ($years as $y): ?>
                                            <?php
                                                $d = (float)$r['years'][$y]['direct'];
                                                $iVal = (float)$r['years'][$y]['incl'];
                                            ?>
                                            <td class="nowrap">
                                                <?php echo formatEuro($d); ?>
                                                <span class="text-muted">(<?php echo formatEuro($iVal); ?>)</span>
                                            </td>
                                        <?php endforeach; ?>

                                        <td class="nowrap">
                                            <button
                                                class="btn btn-outline-primary btn-xs js-hist"
                                                data-title="<?php echo htmlspecialchars((string)$r['title']); ?>"
                                                data-hist="<?php echo htmlspecialchars($histDirectB64); ?>"
                                                data-peak="<?php echo (int)$peakDirect; ?>"
                                                data-mode="direct">
                                                Histogramm
                                            </button>

                                            <button
                                                class="btn btn-outline-secondary btn-xs js-hist"
                                                data-title="<?php echo htmlspecialchars((string)$r['title']); ?>"
                                                data-hist="<?php echo htmlspecialchars($histInclB64); ?>"
                                                data-peak="<?php echo (int)$peakIncl; ?>"
                                                data-mode="incl">
                                                inkl. Bundle
                                            </button>
                                        </td>

                                        <td class="nowrap">
                                            <button
                                                class="btn btn-outline-dark btn-xs js-slots"
                                                data-title="<?php echo htmlspecialchars((string)$r['title']); ?>"
                                                data-slots="<?php echo htmlspecialchars($slotsB64); ?>">
                                                Slots
                                            </button>
                                        </td>
                                    </tr>
                                <?php endforeach; ?>
                                </tbody>
                            </table>

                            <small class="text-muted">
                                Jahr-Zuordnung Umsatz: <span class="mono">invoice_date</span>.
                                Peak/Histogramm: Zeitraum <span class="mono">date_start..date_end</span> (tagesbasiert).
                                Werte in Klammern = inkl. Bundle-Anteil (nur <b>virtuelle</b> Bundles werden zerlegt).
                                Extern-Umsatz: <span class="mono">sum_total_net * (amount_external/amount_total)</span>.
                                Slots: auftragsbasiert, SIGNED (Storno gibt Slots frei und bucht auf die gleichen Zeiträume zurück).
                                Storno-Erkennung: Rechnung mit <span class="mono">sum_net &lt; 0</span>.
                            </small>
                        </div>
                    </div>
                </div>

                <div class="card mb-4">
                    <div class="card-header">
                        <i class="fas fa-list mr-1"></i> Debug: verarbeitete Mietpositionen
                    </div>
                    <div class="card-body">
                        <div class="table-responsive">
                            <table class="table table-bordered" id="detailTable" width="100%" cellspacing="0">
                                <thead>
                                <tr>
                                    <th>Jahr</th>
                                    <th>Invoice</th>
                                    <th>Rechnungsnr</th>
                                    <th>Rechnungsdatum</th>
                                    <th>Quelle</th>
                                    <th>Chapter</th>
                                    <th>Produkt-PK</th>
                                    <th>Artikel-Nr</th>
                                    <th>Artikel</th>
                                    <th>Start</th>
                                    <th>Ende</th>
                                    <th>Menge</th>
                                    <th>Menge (signed)</th>
                                    <th>Ext-Menge</th>
                                    <th>Umsatz netto</th>
                                    <th>Umsatz extern</th>
                                    <th>Credit</th>
                                </tr>
                                </thead>
                                <tbody>
                                <?php foreach ($rows as $r): ?>
                                    <tr>
                                        <td><?php echo (int)$r['year']; ?></td>
                                        <td class="mono"><?php echo (int)$r['invoice_pk']; ?></td>
                                        <td class="mono"><?php echo (int)$r['invoice_no']; ?></td>
                                        <td class="mono"><?php echo htmlspecialchars((string)$r['invoice_date']); ?></td>
                                        <td><?php echo htmlspecialchars((string)$r['source']); ?></td>
                                        <td class="mono"><?php echo (int)$r['chapter_id']; ?></td>
                                        <td class="mono"><?php echo (int)$r['product_pk']; ?></td>
                                        <td class="mono"><?php echo (int)$r['product_no']; ?></td>
                                        <td><?php echo htmlspecialchars((string)$r['title']); ?></td>
                                        <td class="mono"><?php echo htmlspecialchars((string)$r['date_start']); ?></td>
                                        <td class="mono"><?php echo htmlspecialchars((string)$r['date_end']); ?></td>
                                        <td class="mono"><?php echo htmlspecialchars((string)$r['qty']); ?></td>
                                        <td class="mono"><?php echo htmlspecialchars((string)($r['qty_signed'] ?? $r['qty'])); ?></td>
                                        <td class="mono"><?php echo htmlspecialchars((string)($r['amount_ext'] ?? '0')); ?></td>
                                        <td class="nowrap"><?php echo formatEuro((float)$r['revenue_net']); ?></td>
                                        <td class="nowrap"><?php echo formatEuro((float)$r['ext_rev_net']); ?></td>
                                        <td class="mono"><?php echo (int)($r['is_credit'] ?? 0); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                                </tbody>
                            </table>
                            <small class="text-muted">
                                Ext-Menge ist Anzahl; Umsatz extern ist anteilig berechnet.
                                Menge(signed) ist für Peak/Slots relevant (Storno-Rechnung -> negativ).
                            </small>
                        </div>
                    </div>
                </div>

<?php endif; // $computeRoi ?>

            </div>
        </main>
        <div id="footerholder"></div>
    </div>
</div>

<!-- Histogram Modal -->
<div class="modal fade" id="histModal" tabindex="-1" role="dialog" aria-hidden="true">
    <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Histogramm</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <canvas id="histChart" height="140"></canvas>
                <div class="mt-2 text-muted">
                    X-Achse: gleichzeitig vermietete Stückzahl (1..Peak) | Y-Achse: Tage
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Slots Modal -->
<div class="modal fade" id="slotsModal" tabindex="-1" role="dialog" aria-hidden="true">
    <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Slots</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <div class="mb-2 text-muted">
                    Slot-Umsatz ist <b>auftragsbasiert</b>: Umsatz einer Position wird gleichmäßig auf die belegten Slots verteilt.
                    Werte in Klammern = inkl. Bundle-Anteil (nur virtuelle Bundles).
                    Storno bucht Umsatz auf die gleichen Slots zurück und gibt die Slots frei (SIGNED).
                </div>

                <div class="table-responsive">
                    <table class="table table-sm table-bordered" id="slotsTable">
                        <thead>
                        <tr>
                            <th class="nowrap">Slot</th>
                            <th class="nowrap">Umsatz</th>
                            <th class="nowrap">Umsatz inkl. Bundle</th>
                            <th class="nowrap">davon extern</th>
                            <th class="nowrap">davon extern inkl. Bundle</th>
                        </tr>
                        </thead>
                        <tbody></tbody>
                    </table>
                </div>

                <div class="mt-2">
                    <canvas id="slotsChart" height="130"></canvas>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
let histChart = null;
let slotsChart = null;

function decodeB64Json(b64) {
    try {
        const json = atob(b64);
        return JSON.parse(json);
    } catch (e) {
        return {};
    }
}

function openHistogram(title, mode, peak, histObj) {
    const labels = [];
    const data = [];
    const p = parseInt(peak || 0, 10);

    for (let i = 1; i <= p; i++) {
        labels.push(String(i));
        data.push(histObj[String(i)] ? parseInt(histObj[String(i)], 10) : 0);
    }

    $('#histModal .modal-title').text(`${title} – ${mode === 'incl' ? 'inkl. Bundle' : 'ohne Bundle'}`);

    const ctx = document.getElementById('histChart');
    if (histChart) { histChart.destroy(); histChart = null; }

    histChart = new Chart(ctx, {
        type: 'bar',
        data: { labels, datasets: [{ label: 'Tage', data }] },
        options: {
            responsive: true,
            animation: false,
            scales: { y: { beginAtZero: true, ticks: { precision: 0 } } }
        }
    });

    $('#histModal').modal('show');
}

function fmtEuro(n) {
    const sign = n < 0 ? '-' : '';
    n = Math.abs(n);
    return sign + n.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €';
}

function openSlots(title, payload) {
    $('#slotsModal .modal-title').text(`${title} – Slot-Umsatz (auftragsbasiert)`);

    const direct = payload.direct || [];
    const incl   = payload.incl || [];
    const directExt = payload.direct_ext || [];
    const inclExt   = payload.incl_ext || [];

    const mapD = {};
    const mapI = {};
    const mapDE = {};
    const mapIE = {};

    direct.forEach(x => mapD[String(x.slot)] = Number(x.rev || 0));
    incl.forEach(x => mapI[String(x.slot)] = Number(x.rev || 0));
    directExt.forEach(x => mapDE[String(x.slot)] = Number(x.rev || 0));
    inclExt.forEach(x => mapIE[String(x.slot)] = Number(x.rev || 0));

    const slots = new Set([
        ...Object.keys(mapD), ...Object.keys(mapI),
        ...Object.keys(mapDE), ...Object.keys(mapIE)
    ]);

    const slotNums = Array.from(slots).map(s => parseInt(s,10)).filter(n => n>0).sort((a,b)=>a-b);

    const $tb = $('#slotsTable tbody');
    $tb.empty();

    const labels = [];
    const dataD = [];
    const dataI = [];

    slotNums.forEach(s => {
        const sd = mapD[String(s)] || 0;
        const si = mapI[String(s)] || 0;
        const sde = mapDE[String(s)] || 0;
        const sie = mapIE[String(s)] || 0;

        $tb.append(`
            <tr>
                <td class="mono">${s}</td>
                <td class="nowrap">${fmtEuro(sd)}</td>
                <td class="nowrap text-muted">(${fmtEuro(si)})</td>
                <td class="nowrap">${fmtEuro(sde)}</td>
                <td class="nowrap text-muted">(${fmtEuro(sie)})</td>
            </tr>
        `);

        labels.push(String(s));
        dataD.push(sd);
        dataI.push(si);
    });

    const ctx = document.getElementById('slotsChart');
    if (slotsChart) { slotsChart.destroy(); slotsChart = null; }

    slotsChart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels,
            datasets: [
                { label: 'Umsatz', data: dataD },
                { label: 'Umsatz inkl. Bundle', data: dataI }
            ]
        },
        options: {
            responsive: true,
            animation: false,
            scales: { y: { beginAtZero: true } }
        }
    });

    $('#slotsModal').modal('show');
}

$(function() {
    $('#pivotTable').DataTable({ pageLength: 50, order: [[4,'desc']] });
    $('#detailTable').DataTable({ pageLength: 50, order: [[1,'desc']] });

    $(document).on('click', '.js-hist', function() {
        const title = $(this).data('title') || '';
        const mode  = $(this).data('mode') || 'direct';
        const peak  = $(this).data('peak') || 0;
        const b64   = $(this).data('hist') || '';
        const histObj = decodeB64Json(b64);
        openHistogram(title, mode, peak, histObj);
    });

    $(document).on('click', '.js-slots', function() {
        const title = $(this).data('title') || '';
        const b64   = $(this).data('slots') || '';
        const payload = decodeB64Json(b64);
        openSlots(title, payload);
    });
});
</script>

</body>
</html>