/**
 * Oracle History report — D3 ASH stacked bar chart + heatmaps.
 * Reads data from window.DemoOracleReport.history.
 */
(function () {
'use strict';

var PAGE = (window.DemoOracleReport || {}).history;
if (!PAGE) return;

/* ── Oracle standard ASH wait-class colours ─────────────────────────────── */
var ASH_COLORS = {
    'CPU':           '#00BB00',
    'Scheduler':     '#78E078',
    'User I/O':      '#0043D0',
    'Other':         '#F071B0',
    'Concurrency':   '#801800',
    'Commit':        '#E06800',
    'System I/O':    '#0090E0',
    'Network':       '#989070',
    'Configuration': '#606810',
    'Application':   '#FF0000'
};

var CAT_ORDER = [
    'CPU', 'Scheduler', 'User I/O', 'System I/O',
    'Network', 'Commit', 'Concurrency', 'Configuration', 'Application', 'Other'
];

function ashColor(cat) { return ASH_COLORS[cat] || '#888'; }

/* ── Data (parsed once) ─────────────────────────────────────────────────── */
var rawData = PAGE.ashData;
var days    = PAGE.days;
var yLabel  = PAGE.yLabel;

rawData.forEach(function (d) {
    d.t   = new Date(d.sample_bucket.replace(' ', 'T'));
    d.cnt = +d.cnt;
});

/* ── Chart ───────────────────────────────────────────────────────────────── */
function renderASHChart() {
    var container = document.getElementById('chart-ash');
    if (!container || !rawData.length) return;

    /* Categories present, in canonical order */
    var present = [];
    CAT_ORDER.forEach(function (c) {
        if (rawData.some(function (d) { return d.category === c; })) present.push(c);
    });
    rawData.forEach(function (d) {
        if (present.indexOf(d.category) < 0) present.push(d.category);
    });

    /* Unique sorted time buckets */
    var seen = {}, buckets = [];
    rawData.forEach(function (d) {
        var k = d.t.getTime();
        if (!seen[k]) { seen[k] = true; buckets.push(d.t); }
    });
    buckets.sort(function (a, b) { return a - b; });

    /* Pivot to wide format: [{t, CPU: n, 'User I/O': n, …}] */
    var pivoted = buckets.map(function (t) {
        var row = { t: t }, ts = t.getTime();
        present.forEach(function (c) { row[c] = 0; });
        rawData.forEach(function (d) {
            if (d.t.getTime() === ts) row[d.category] = d.cnt;
        });
        return row;
    });

    /* D3 stack */
    var stacked = d3.stack().keys(present)(pivoted);

    /* Dimensions — W limité à la largeur du conteneur ; barW (calculée plus
       bas à partir du ratio innerW × intMs / domainMs) s'adapte pour que
       toutes les barres tiennent dans la page sans scroll horizontal. */
    var margin   = { top: 16, right: 16, bottom: 60, left: 56 };
    var W        = container.clientWidth || 800;
    var H        = 280;
    var innerW   = W - margin.left - margin.right;
    var innerH   = H - margin.top  - margin.bottom;

    /* Scales — half-interval padding centres each bar on its timestamp */
    var intMs    = 3600 * 1000; // always hourly buckets
    var tMin     = new Date(buckets[0].getTime() - intMs / 2);
    var tMax     = new Date(buckets[buckets.length - 1].getTime() + intMs / 2);
    var domainMs = tMax.getTime() - tMin.getTime();

    var xScale   = d3.scaleTime().domain([tMin, tMax]).range([0, innerW]);
    var yTop     = d3.max(stacked, function (l) { return d3.max(l, function (d) { return d[1]; }); }) || 1;
    var yScale   = d3.scaleLinear().domain([0, yTop]).nice().range([innerH, 0]);

    /* Bar width derived from the scale ratio */
    var barW     = Math.max(2, Math.floor(innerW * intMs / domainMs) - 1);

    var tickFmt  = days <= 1
        ? d3.timeFormat('%H:%M')
        : (days <= 7 ? d3.timeFormat('%d/%m %Hh') : d3.timeFormat('%d/%m'));

    /* SVG */
    var svg = d3.create('svg')
        .attr('width', W).attr('height', H)
        .style('font-family', 'inherit').style('font-size', '11px');
    var g = svg.append('g')
        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

    /* Horizontal grid */
    g.append('g')
        .call(d3.axisLeft(yScale).tickSize(-innerW).tickFormat(''))
        .call(function (s) {
            s.select('.domain').remove();
            s.selectAll('line').attr('stroke', '#ccc').attr('stroke-dasharray', '3,3');
        });

    /* Stacked bars — one <g> per category layer */
    stacked.forEach(function (layer) {
        g.append('g').attr('fill', ashColor(layer.key))
            .selectAll('rect').data(layer).join('rect')
            .attr('x',      function (d) { return xScale(d.data.t) - barW / 2; })
            .attr('y',      function (d) { return yScale(d[1]); })
            .attr('height', function (d) { return Math.max(0, yScale(d[0]) - yScale(d[1])); })
            .attr('width',  barW)
            .append('title')
            .text(function (d) { return layer.key + ' : ' + d.data[layer.key] + ' sessions'; });
    });

    /* X axis */
    g.append('g')
        .attr('transform', 'translate(0,' + innerH + ')')
        .call(d3.axisBottom(xScale).tickFormat(tickFmt).ticks(Math.min(buckets.length, 20)))
        .call(function (s) {
            s.selectAll('text')
                .attr('transform', 'rotate(-45)')
                .style('text-anchor', 'end')
                .attr('dx', '-.5em').attr('dy', '.15em');
        });

    /* Y axis */
    g.append('g').call(d3.axisLeft(yScale).ticks(6));

    /* Y label */
    g.append('text')
        .attr('transform', 'rotate(-90)')
        .attr('x', -innerH / 2).attr('y', -margin.left + 14)
        .attr('text-anchor', 'middle').style('font-size', '11px').attr('fill', 'currentColor')
        .text(yLabel);

    /* Legend — rendered in #chart-ash-legend (même ligne que le titre h2) */
    var legendCt = document.getElementById('chart-ash-legend');
    var legendEl = document.createElement('div');
    legendEl.style.cssText = 'display:flex;flex-wrap:nowrap;align-items:center;gap:.15rem .5rem;font-size:.75rem;white-space:nowrap';
    present.forEach(function (cat) {
        var item = document.createElement('span');
        item.style.cssText = 'display:inline-flex;align-items:center;gap:.3rem';
        var sw = document.createElement('span');
        sw.style.cssText = 'display:inline-block;width:12px;height:12px;border-radius:2px;flex-shrink:0;background:' + ashColor(cat);
        item.appendChild(sw);
        item.appendChild(document.createTextNode(cat));
        legendEl.appendChild(item);
    });

    container.innerHTML = '';
    if (legendCt) { legendCt.innerHTML = ''; legendCt.appendChild(legendEl); }
    container.appendChild(svg.node());
}

renderASHChart();

/* ── Heatmap data from bridge ───────────────────────────────────────────── */
var heatChargeData = PAGE.heatChargeData;
var heatIoData     = PAGE.heatIoData;
var heatLogData    = PAGE.heatLogData;

/* ── URL du rapport détail ASH (pour les clics sur les cellules) ─────────── */
var detailBaseUrl = PAGE.detailBaseUrl;

/* ── Échelle de couleur des heatmaps ────────────────────────────────────────
   Même forme que le rapport Charge du module PostgreSQL. Les interpolateurs d3
   (Greens / Blues / YlOrRd) sont conservés : ils sont déjà continus, c'est le
   DOMAINE qui écrasait les nuances. Domaine [0, maxVal] veut dire qu'un seul
   pic — un batch mensuel, une réindexation — repousse toutes les autres heures
   dans le bas de la rampe, où les teintes ne se distinguent plus.

   Deux modes commutables par heatmap, sur r = cnt / haut de domaine :
     lin   t = r
     log   t = log1p(K·r) / log1p(K)

   Constantes reprises telles quelles du module PostgreSQL, où elles ont été
   calibrées sur sept profils de distribution simulés. */
var LOG_K = 24;          // courbure du log
var SKEW_LOG = 5;        // max/médiane au-delà duquel le log est le défaut
var SPREAD_MIN = 1.5;    // le p98 ne sert de haut de domaine que s'il vaut
                         // au moins SPREAD_MIN × la médiane

var SCALES = {
    lin: {
        t:   function (r) { return r; },
        inv: function (u) { return u; }
    },
    log: {
        t:   function (r) { return Math.log1p(LOG_K * r) / Math.log1p(LOG_K); },
        // Inverse exact : u = log1p(K·r)/log1p(K)  ⇒  r = ((1+K)^u − 1)/K.
        // Sert à replacer les stops de la légende, qui décrit ainsi la même
        // échelle que la grille au lieu d'en suggérer une linéaire.
        inv: function (u) { return (Math.pow(1 + LOG_K, u) - 1) / LOG_K; }
    }
};

function quantile(sorted, p) {
    var i = (sorted.length - 1) * p, lo = Math.floor(i), hi = Math.ceil(i);
    return sorted[lo] + (sorted[hi] - sorted[lo]) * (i - lo);
}

/**
 * Haut du domaine de couleur, maximum observé et dissymétrie.
 *
 * Le domaine est raboté au 98e centile des cases non nulles, mais SEULEMENT si
 * la distribution a de l'étendue en dessous. Sinon (plateau plat surmonté d'un
 * pic unique) le p98 vaut la médiane et c'est l'inverse qui se produit : toutes
 * les cases saturent au ton le plus foncé.
 *
 * @returns {{ref: number, max: number, skew: number}}
 */
function heatScale(heatData) {
    var nz = heatData.map(function (d) { return d.cnt; })
        .filter(function (v) { return v > 0; })
        .sort(function (a, b) { return a - b; });
    if (!nz.length) return { ref: 0, max: 0, skew: 1 };
    var max = nz[nz.length - 1];
    var med = quantile(nz, 0.5) || max;
    var p98 = quantile(nz, 0.98);
    return { ref: (p98 >= SPREAD_MIN * med ? p98 : max), max: max, skew: max / med };
}

/**
 * Mode par défaut : log quand beaucoup de valeurs faibles côtoient quelques
 * très grandes, linéaire sinon. Critère max/médiane des cases non nulles —
 * robuste (la médiane ignore les pics) et lisible : « le maximum vaut N fois
 * l'heure médiane ».
 */
function autoScale(s) { return s.skew >= SKEW_LOG ? 'log' : 'lin'; }

/** Position d'un comptage sur la rampe : t ∈ [0,1]. */
function scaleT(v, ref, mode) {
    if (!(v > 0) || !(ref > 0)) return 0;
    return SCALES[mode].t(Math.min(1, v / ref));
}

/* ── Heatmap renderer ────────────────────────────────────────────────────── */
function renderHeatmap(containerId, heatData, colorInterp, mode) {
    var container = document.getElementById(containerId);
    if (!container || !heatData.length) return;

    /* Unique sorted days */
    var daySet = {};
    heatData.forEach(function (d) { daySet[d.day] = true; });
    var hDays = Object.keys(daySet).sort();

    /* Lookup [day][hour] → cnt */
    var lookup = {};
    heatData.forEach(function (d) {
        if (!lookup[d.day]) lookup[d.day] = {};
        lookup[d.day][d.hour] = d.cnt;
    });

    /* Domaine raboté + mode lin/log, au lieu de d3.scaleSequential sur
       [0, maxVal] : c'est ce domaine brut qui rendait les grilles à pic
       uniformément pâles. */
    var sc = heatScale(heatData);
    var colorScale = function (cnt) { return colorInterp(scaleT(cnt, sc.ref, mode)); };
    var dayFmt    = d3.timeFormat('%d/%m');

    /* Dimensions — cellW est dynamique : on remplit la largeur du tiers
       de page (clientWidth du parent .ora-heatmap-host imposé par le grid)
       avec borne min/max pour rester lisible. cellH reste fixe (24h). */
    var margin = { top: 8, right: 12, bottom: 32, left: 38 };
    var host       = container.parentElement; // <div class="ora-heatmap-host">
    var availInner = Math.max(80, host.clientWidth - margin.left - margin.right);
    var MIN_CELL_W = 8;
    var MAX_CELL_W = 40;
    var cellW = Math.max(MIN_CELL_W,
                         Math.min(MAX_CELL_W, availInner / hDays.length));
    var cellH = 14;
    var innerW = hDays.length * cellW;
    var innerH = 24 * cellH;
    var W = innerW + margin.left + margin.right;
    var H = innerH + margin.top  + margin.bottom;

    var svg = d3.create('svg').attr('width', W).attr('height', H)
        .style('font-family', 'inherit').style('font-size', '10px');
    var g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

    /* Cells — flat data array for D3 binding + click navigation */
    var p2 = function (n) { return ('0' + n).slice(-2); };
    var cellData = [];
    hDays.forEach(function (day, di) {
        for (var h = 0; h < 24; h++) {
            cellData.push({
                day: day, di: di, h: h,
                cnt: (lookup[day] && lookup[day][h] != null) ? lookup[day][h] : 0
            });
        }
    });

    var cells = g.selectAll('.hm-cell')
        .data(cellData)
        .join('rect')
        .attr('class', 'hm-cell')
        .attr('x', function (d) { return d.di * cellW; })
        .attr('y', function (d) { return d.h  * cellH; })
        .attr('width',  cellW - 1).attr('height', cellH - 1).attr('rx', 1)
        .attr('fill',   function (d) { return colorScale(d.cnt); })
        .style('cursor', function (d) { return d.cnt > 0 ? 'pointer' : 'default'; })
        .on('click', function (event, d) {
            if (d.cnt === 0) return;
            var endH   = d.h + 1;
            var endDay = d.day;
            if (endH === 24) {
                endH = 0;
                var nd = new Date(d.day + 'T12:00:00');
                nd.setDate(nd.getDate() + 1);
                endDay = nd.getFullYear() + '-' + p2(nd.getMonth() + 1) + '-' + p2(nd.getDate());
            }
            var begin = d.day + ' ' + p2(d.h) + ':00';
            var end   = endDay + ' ' + p2(endH) + ':00';
            // L'URL du module de demonstration porte deja `?section=` : le
            // premier separateur est donc un `&`, pas un `?`.
            window.location.href = detailBaseUrl
                + '&dt_begin=' + encodeURIComponent(begin)
                + '&dt_end='   + encodeURIComponent(end);
        });

    cells.append('title')
        .text(function (d) { return d.day + ' ' + p2(d.h) + 'h : ' + d.cnt; });

    /* Y axis — every 3 hours */
    for (var h = 0; h < 24; h += 3) {
        g.append('text')
            .attr('x', -4).attr('y', h * cellH + cellH / 2)
            .attr('text-anchor', 'end').attr('dominant-baseline', 'middle')
            .attr('fill', 'currentColor')
            .text(String(h).padStart(2, '0') + 'h');
    }
    /* label for hour 23 */
    g.append('text')
        .attr('x', -4).attr('y', 23 * cellH + cellH / 2)
        .attr('text-anchor', 'end').attr('dominant-baseline', 'middle')
        .attr('fill', 'currentColor').text('23h');

    /* X axis — day labels (skip every other if many days) */
    var xStep = hDays.length > 20 ? 3 : (hDays.length > 10 ? 2 : 1);
    hDays.forEach(function (day, di) {
        if (di % xStep !== 0) return;
        var cx = di * cellW + cellW / 2;
        g.append('text')
            .attr('x', cx).attr('y', innerH + 4)
            .attr('text-anchor', 'end').attr('fill', 'currentColor')
            .attr('transform', 'rotate(-45,' + cx + ',' + (innerH + 4) + ')')
            .text(dayFmt(new Date(day + 'T12:00:00')));
    });

    container.innerHTML = '';
    container.appendChild(svg.node());
    return sc; // {ref, max, skew} — alimente la légende de CETTE heatmap
}

/* ── Heatmaps déclarées ───────────────────────────────────────────────────────
   `mode` est null jusqu'au premier rendu, où le défaut est déduit des données ;
   ensuite il porte le choix de l'utilisateur, y compris à travers un resize
   (qui redessine les SVG, cellW dépendant de la largeur disponible). */
var _hmDefs = [
    { key: 'charge', id: 'chart-heat-charge', data: heatChargeData, interp: d3.interpolateGreens, mode: null },
    { key: 'io',     id: 'chart-heat-io',     data: heatIoData,     interp: d3.interpolateBlues,  mode: null },
    { key: 'log',    id: 'chart-heat-log',    data: heatLogData,    interp: d3.interpolateYlOrRd, mode: null }
];

var I18N = PAGE.i18n || {};

function esc(s) {
    var d = document.createElement('div');
    d.textContent = s == null ? '' : s;
    return d.innerHTML;
}
function fmtCnt(n) { return Number(n).toLocaleString(); }

/**
 * Une légende par heatmap — les trois rampes diffèrent, la légende commune
 * qu'elles partageaient jusqu'ici ne pouvait décrire qu'une seule échelle.
 *
 * Les bornes chiffrées sont conservées. Attention à leur exactitude : la barre
 * s'arrête au HAUT DE DOMAINE (p98 le plus souvent), pas au maximum observé.
 * Quand les deux diffèrent, la borne droite est suffixée d'un « + » et le
 * maximum réel est rappelé à côté — sinon le nombre mentirait sur ce que la
 * couleur encode.
 *
 * Les stops sont replacés par l'INVERSE de la transformation active, donc la
 * barre change de forme à la bascule au lieu de suggérer une échelle linéaire
 * que la grille n'utiliserait pas.
 */
function renderHeatLegend(hm, sc) {
    var el = document.getElementById('heat-legend-' + hm.key);
    if (!el) return;
    if (!sc || !sc.max) { el.innerHTML = ''; return; }

    var inv = SCALES[hm.mode].inv;
    var stops = [0, 0.25, 0.5, 0.75, 1].map(function (u) {
        return hm.interp(u) + ' ' + (inv(u) * 100).toFixed(1) + '%';
    }).join(',');

    /* La comparaison porte sur les valeurs ARRONDIES, celles qui sont affichées :
       un p98 à 16,64 pour un maximum de 17 s'écrirait sinon « 17+ (max 17) ». */
    var top = Math.round(sc.ref);
    var clipped = top < sc.max;
    var topLabel = fmtCnt(top) + (clipped ? '+' : '');
    var barTip = clipped ? (I18N.heatClipped || '') : '';

    el.innerHTML = '<span style="opacity:.55">0</span>'
        + '<span title="' + esc(barTip) + '"'
        + ' style="display:inline-block;width:72px;height:8px;border-radius:2px;'
        + 'margin:0 .3rem;background:linear-gradient(to right,' + stops + ')"></span>'
        + '<span style="opacity:.55">' + esc(topLabel) + '</span>'
        + (clipped
            ? '<span style="opacity:.45;margin-left:.35rem">'
              + esc((I18N.heatMax || 'max') + ' ' + fmtCnt(sc.max)) + '</span>'
            : '')
        + scaleToggle(hm, sc);
}

/** Interrupteur lin/log d'une heatmap. Le défaut retenu est signalé en info-bulle. */
function scaleToggle(hm, sc) {
    var auto = autoScale(sc);
    var btns = ['lin', 'log'].map(function (m) {
        var on = hm.mode === m;
        var tip = (m === 'lin' ? I18N.heatScaleLin : I18N.heatScaleLog) || m;
        if (m === auto) tip += ' — ' + (I18N.heatScaleAuto || '');
        return '<button type="button" data-heat="' + hm.key + '" data-scale="' + m + '"'
            + ' aria-pressed="' + (on ? 'true' : 'false') + '"'
            + ' title="' + esc(tip) + '"'
            + ' style="font:inherit;font-size:.68rem;line-height:1.35;padding:.1rem .34rem;'
            + 'border:0;cursor:pointer;'
            + (on ? 'background:var(--color-primary,#2563eb);color:#fff'
                  : 'background:transparent;color:var(--color-text-muted)') + '">'
            + m + '</button>';
    }).join('');
    return '<span title="' + esc(I18N.heatScaleHint || '') + '" style="margin-left:.55rem;'
        + 'display:inline-flex;border:1px solid var(--color-border,#ddd);'
        + 'border-radius:4px;overflow:hidden">' + btns + '</span>';
}

function drawHeatmap(hm) {
    if (!hm.data.length) return;
    // Le mode par défaut se déduit de la distribution, une seule fois.
    if (hm.mode === null) hm.mode = autoScale(heatScale(hm.data));
    var sc = renderHeatmap(hm.id, hm.data, hm.interp, hm.mode);
    renderHeatLegend(hm, sc);
}

function renderAllHeatmaps() { _hmDefs.forEach(drawHeatmap); }

renderAllHeatmaps();

/* Bascule lin/log : délégation sur le document, la légende — donc ses boutons —
   étant réécrite à chaque redessin. Une heatmap à la fois : l'échelle est propre
   à chaque grille, les quantités n'étant pas comparables. */
document.addEventListener('click', function (e) {
    var b = e.target.closest ? e.target.closest('[data-scale]') : null;
    if (!b) return;
    var key = b.getAttribute('data-heat'), mode = b.getAttribute('data-scale');
    if (!SCALES[mode]) return;
    _hmDefs.forEach(function (hm) {
        if (hm.key === key && hm.mode !== mode) { hm.mode = mode; drawHeatmap(hm); }
    });
});

var _rt;
window.addEventListener('resize', function () { clearTimeout(_rt); _rt = setTimeout(function () {
    renderASHChart();
    renderAllHeatmaps();
}, 220); });

})();
