// Restock price/stock timeline - a hand-rolled smoothed SVG line graph.
// Line segments are green where the item was in stock and red where it was out of stock.
//
// Two consumers:
//   * the restock diff page (#restock-graph), which fetches window.restock_data_url and also
//     builds the data table below it; and
//   * the watchlist inline roll-down, which calls window.renderRestockGraph(el, series, currency, i18n).
//
// Series shape (oldest -> newest): [{timestamp: <epoch sec>, price: <float|null>, in_stock: <bool|null>}]

(function ($) {
    'use strict';

    const SVG_NS = 'http://www.w3.org/2000/svg';
    const COLOR_IN = '#1fa463';   // in stock = green
    const COLOR_OUT = '#e74c3c';  // out of stock = red
    const COLOR_UNKNOWN = '#999'; // stock state not known
    const HEIGHT = 320;          // default (diff page); the inline watchlist graph passes a shorter one
    const PAD = { top: 20, right: 16, bottom: 30, left: 52 };
    const MAX_X_LABELS = 6;       // keep the date axis uncluttered

    const DEFAULT_I18N = { in_stock: 'In stock', out_of_stock: 'Out of stock',
        no_data: 'No price data available to graph yet.', load_error: 'Could not load price history.',
        changes: 'Changes', avg_price: 'Average price',
        price_low: 'Currently low', price_typical: 'Currently typical', price_high: 'Currently high',
        cheaper_than: 'cheaper than %s% of tracked prices',
        pricier_than: 'more expensive than %s% of tracked prices',
        typical_note: 'around the usual price', avg_label: 'avg' };
    const OVERLAY_MIN_POINTS = 5; // need enough history for low/typical/high to be meaningful

    function el(name, attrs) {
        const e = document.createElementNS(SVG_NS, name);
        for (const k in attrs) e.setAttribute(k, attrs[k]);
        return e;
    }

    function stockColor(inStock) {
        if (inStock === true) return COLOR_IN;
        if (inStock === false) return COLOR_OUT;
        return COLOR_UNKNOWN;
    }

    // X-axis label: day + short month + short year, e.g. "21 Jun '25".
    function fmtAxisDate(epoch) {
        const d = new Date(epoch * 1000);
        const dm = d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
        return `${dm} '${String(d.getFullYear()).slice(-2)}`;
    }

    function fmtDateTime(epoch) {
        // undefined locale => the browser's locale; explicit options for a clean, complete format.
        return new Date(epoch * 1000).toLocaleString(undefined, {
            year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
        });
    }

    function fmtPrice(v, currency) {
        const n = Math.round(v * 100) / 100;
        return (currency || '') + (Number.isInteger(n) ? n : n.toFixed(2));
    }

    // Thin a large price series down to ~target points for drawing (you can't perceive more
    // points than pixels). Always keeps the first, last and the true min/max points so the line
    // still touches its extremes. The axis range + summary stay computed over the FULL data.
    function downsample(arr, target) {
        if (arr.length <= target) return arr;
        const keep = new Set([0, arr.length - 1]);
        let minI = 0, maxI = 0;
        for (let i = 1; i < arr.length; i++) {
            if (arr[i].price < arr[minI].price) minI = i;
            if (arr[i].price > arr[maxI].price) maxI = i;
        }
        keep.add(minI); keep.add(maxI);
        const step = arr.length / target;
        for (let i = 0; i < target; i++) keep.add(Math.floor(i * step));
        return Array.from(keep).sort((a, b) => a - b).map(i => arr[i]);
    }

    // Monotone cubic spline (Fritsch–Carlson). A gentle, smooth curve that — unlike Catmull-Rom —
    // is guaranteed NOT to overshoot beyond the actual data points: tangents are clamped so the
    // line never dips below / rises above neighbouring values. So the curve stays faithful to the
    // real prices (no phantom dips/spikes) while still looking soft. Returns an SVG path string.
    function monotonePath(pts) {
        const n = pts.length;
        if (n < 2) return '';
        const xs = pts.map(p => p.x), ys = pts.map(p => p.y);
        if (n === 2) return `M ${xs[0].toFixed(1)} ${ys[0].toFixed(1)} L ${xs[1].toFixed(1)} ${ys[1].toFixed(1)}`;

        const dx = [], slope = [];
        for (let i = 0; i < n - 1; i++) {
            const h = xs[i + 1] - xs[i];
            dx.push(h);
            slope.push(h === 0 ? 0 : (ys[i + 1] - ys[i]) / h);
        }

        const m = new Array(n);
        m[0] = slope[0];
        m[n - 1] = slope[n - 2];
        for (let i = 1; i < n - 1; i++) {
            // Local extremum (slope changes sign) → flat tangent so the curve doesn't overshoot.
            m[i] = (slope[i - 1] * slope[i] <= 0) ? 0 : (slope[i - 1] + slope[i]) / 2;
        }
        // Fritsch–Carlson: clamp tangents so each segment stays monotone (no overshoot).
        for (let i = 0; i < n - 1; i++) {
            if (slope[i] === 0) { m[i] = 0; m[i + 1] = 0; continue; }
            const a = m[i] / slope[i], b = m[i + 1] / slope[i], s = a * a + b * b;
            if (s > 9) {
                const t = 3 / Math.sqrt(s);
                m[i] = t * a * slope[i];
                m[i + 1] = t * b * slope[i];
            }
        }

        let d = `M ${xs[0].toFixed(1)} ${ys[0].toFixed(1)}`;
        for (let i = 0; i < n - 1; i++) {
            const h = dx[i];
            const c1x = xs[i] + h / 3, c1y = ys[i] + m[i] * h / 3;
            const c2x = xs[i + 1] - h / 3, c2y = ys[i + 1] - m[i + 1] * h / 3;
            d += ` C ${c1x.toFixed(1)} ${c1y.toFixed(1)} ${c2x.toFixed(1)} ${c2y.toFixed(1)} ${xs[i + 1].toFixed(1)} ${ys[i + 1].toFixed(1)}`;
        }
        return d;
    }

    function draw($container, series, currency, i18n, summary, height) {
        height = height || HEIGHT;
        // Only points that actually have a price can sit on the line.
        const data = (series || []).filter(p => p.price !== null && p.price !== undefined);
        if (data.length < 2) {
            $container.html($('<p class="pure-form-message-inline"></p>').text(i18n.no_data));
            return;
        }

        const width = Math.max($container.width() || 0, 320);
        const innerH = height - PAD.top - PAD.bottom;

        const prices = data.map(p => p.price);
        let dataMin = Math.min.apply(null, prices), dataMax = Math.max.apply(null, prices);
        if (dataMin === dataMax) { dataMin -= 1; dataMax += 1; }
        // Pad the value domain ~10% top & bottom so the (smoothed, possibly overshooting) line
        // and the min/max labels sit inside the plot area instead of being clipped at the edges.
        const vpad = (dataMax - dataMin) * 0.1;
        const domMin = dataMin - vpad, domMax = dataMax + vpad, range = domMax - domMin;

        const ticks = [dataMax, (dataMin + dataMax) / 2, dataMin];

        // Widen the left gutter to fit the price labels (e.g. "CZK12.34") so a long currency
        // prefix isn't clipped at the SVG's left edge. ~7px/char + padding.
        const maxLabelChars = Math.max.apply(null, ticks.map(v => fmtPrice(v, currency).length));
        const padLeft = Math.max(PAD.left, maxLabelChars * 7 + 14);

        const innerW = width - padLeft - PAD.right;
        // Draw at most ~one point per few pixels; keeps the SVG light on long histories / mobile.
        const drawData = downsample(data, Math.max(40, Math.min(250, Math.floor(innerW / 4))));
        const x = i => padLeft + (i * innerW) / (drawData.length - 1);
        const y = price => PAD.top + (1 - (price - domMin) / range) * innerH;
        const pts = drawData.map((p, i) => ({ x: x(i), y: y(p.price), d: p }));

        // Size the SVG in real pixels at the measured width (no viewBox => no CSS rescaling).
        const svg = el("svg", { width: width, height: height, role: "img" });

        // Y axis: max / mid / min gridlines + price labels.
        ticks.forEach(price => {
            const yy = y(price);
            svg.appendChild(el('line', { class: 'rg-axis', x1: padLeft, y1: yy, x2: width - PAD.right, y2: yy, opacity: price === dataMin ? 0.6 : 0.15 }));
            const t = el('text', { class: 'rg-label', x: padLeft - 8, y: yy + 4, 'text-anchor': 'end' });
            t.textContent = fmtPrice(price, currency);
            svg.appendChild(t);
        });

        // Analyser overlays (Google-Flights style): a "typical range" band (p25-p75) behind the
        // line + a dashed average line. Drawn before the data line so they sit behind it.
        const showOverlays = summary && summary.count >= OVERLAY_MIN_POINTS;
        if (showOverlays) {
            const yTop = y(summary.p75), yBot = y(summary.p25);
            svg.appendChild(el('rect', {
                class: 'rg-band', x: padLeft, y: Math.min(yTop, yBot),
                width: Math.max(0, width - PAD.right - padLeft), height: Math.abs(yBot - yTop)
            }));
            const yAvg = y(summary.avg);
            svg.appendChild(el('line', {
                class: 'rg-avg-line', x1: padLeft, y1: yAvg, x2: width - PAD.right, y2: yAvg,
                'stroke-dasharray': '4 4'
            }));
            const at = el('text', { class: 'rg-label rg-avg-text', x: width - PAD.right, y: yAvg - 4, 'text-anchor': 'end' });
            at.textContent = i18n.avg_label || 'avg';
            svg.appendChild(at);
        }

        // Single neutral, gently-smoothed line (monotone cubic — soft but never overshoots the
        // real values). Stock status is shown by the DOT colours instead, not the line.
        svg.appendChild(el('path', {
            class: 'rg-line', d: monotonePath(pts), fill: 'none',
            'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round'
        }));

        // Dots, coloured by each point's own stock state (green = in stock, red = out of stock).
        // Sized to match the legend swatch (~9px) with a thin surface-coloured ring so they stand
        // out from the line instead of blending into it.
        pts.forEach(p => {
            svg.appendChild(el('circle', {
                class: 'rg-dot', cx: p.x.toFixed(1), cy: p.y.toFixed(1), r: 4.5, fill: stockColor(p.d.in_stock)
            }));
        });

        // Sparse x-axis date labels: evenly spaced, always including both endpoints. The number
        // adapts to the available width (each label ~64px) so they don't jam together on narrow
        // / mobile widths, capped at MAX_X_LABELS on desktop. Min 2 (the two endpoints).
        const fitLabels = Math.max(2, Math.min(MAX_X_LABELS, Math.floor(innerW / 64)));
        const count = Math.min(fitLabels, pts.length);
        const labelIdx = Array.from(new Set(
            Array.from({ length: count }, (_, i) => Math.round((i * (pts.length - 1)) / (count - 1)))
        ));
        labelIdx.forEach(i => {
            const anchor = i === 0 ? 'start' : (i === pts.length - 1 ? 'end' : 'middle');
            const t = el('text', { class: 'rg-label', x: pts[i].x.toFixed(1), y: height - 10, 'text-anchor': anchor });
            t.textContent = fmtAxisDate(pts[i].d.timestamp);
            svg.appendChild(t);
        });

        $container.empty().append(svg);

        // --- Hover tooltip: a generous transparent hit-circle per point shows price + date ---
        const $tip = $('<div class="rg-tooltip"></div>').appendTo($container);
        pts.forEach(p => {
            const state = p.d.in_stock === null ? '' :
                (p.d.in_stock ? ' &middot; ' + i18n.in_stock : ' &middot; ' + i18n.out_of_stock);
            const hit = el('circle', { cx: p.x.toFixed(1), cy: p.y.toFixed(1), r: 14, fill: 'transparent', style: 'cursor: pointer;' });
            svg.appendChild(hit);
            $(hit).on('mouseenter mousemove', function (e) {
                const r = $container[0].getBoundingClientRect();
                const cx = e.clientX - r.left, cy = e.clientY - r.top;
                $tip.html('<strong>' + fmtPrice(p.d.price, currency) + '</strong>' + state + '<br>' + fmtDateTime(p.d.timestamp)).show();
                const tipW = $tip.outerWidth(), GAP = 14;
                // Default to the right of the cursor; flip left if it would overflow the right edge.
                let left = cx + GAP;
                if (left + tipW > $container.width()) left = cx - GAP - tipW;
                if (left < 0) left = 0;
                $tip.css({ left: left + 'px', top: cy + 'px' });
            }).on('mouseleave', function () {
                $tip.hide();
            });
        });

        // Legend explaining the dot colours (the line is neutral; only dots indicate stock).
        const $legend = $('<div class="rg-legend"></div>');
        $legend.append($('<span class="rg-legend-item"></span>')
            .append('<span class="rg-legend-dot in"></span>').append(document.createTextNode(' ' + (i18n.in_stock || 'In stock'))));
        $legend.append($('<span class="rg-legend-item"></span>')
            .append('<span class="rg-legend-dot out"></span>').append(document.createTextNode(' ' + (i18n.out_of_stock || 'Out of stock'))));
        $container.append($legend);

        // Header row above the graph: status pill (LOW / TYPICAL / HIGH, left) and the summary
        // stats (right), on one line. The pill sub-text is phrased per status so it never reads
        // awkwardly (e.g. "cheaper than 0%" when high).
        const $header = $('<div class="rg-header"></div>');

        let $pill = $('<span></span>');  // placeholder keeps the stats pushed to the right
        if (showOverlays) {
            $pill = $('<div class="rg-status rg-status-' + summary.status + '"></div>');
            $pill.append($('<span class="rg-status-label"></span>').text(i18n['price_' + summary.status] || ''));
            let sub;
            if (summary.status === 'low') {
                sub = (i18n.cheaper_than || '').replace('%s', summary.cheaper_than_pct);
            } else if (summary.status === 'high') {
                sub = (i18n.pricier_than || '').replace('%s', summary.pricier_than_pct);
            } else {
                sub = i18n.typical_note || '';
            }
            if (sub) $pill.append($('<span class="rg-status-sub"></span>').text(sub));
        }

        const avgPrice = prices.reduce((a, b) => a + b, 0) / prices.length;
        const $stats = $('<div class="rg-stats"></div>')
            .text((i18n.avg_price || 'Average price') + ' ' + fmtPrice(avgPrice, currency) +
                  ', ' + (series ? series.length : data.length) + ' ' + (i18n.changes || 'Changes'));

        $header.append($pill).append($stats);
        $container.prepend($header);
    }

    function buildTable($table, series, currency, i18n) {
        const $tbody = $table.find('tbody');
        if (!$tbody.length) return;
        $tbody.empty();
        series.slice().reverse().forEach(p => {
            let stock = '-';
            if (p.in_stock === true) stock = $('<span class="restock-badge in-stock"></span>').text(i18n.in_stock);
            else if (p.in_stock === false) stock = $('<span class="restock-badge out-of-stock"></span>').text(i18n.out_of_stock);
            const price = (p.price !== null && p.price !== undefined) ? fmtPrice(p.price, currency) : '-';
            $('<tr></tr>')
                .append($('<td></td>').text(fmtDateTime(p.timestamp)))
                .append($('<td></td>').append(stock))
                .append($('<td></td>').text(price))
                .appendTo($tbody);
        });
    }

    // Redraw a graph only if its container width actually changed (skips redundant redraws and
    // prevents ResizeObserver feedback loops).
    function redraw($c) {
        const ctx = $c.data('rg');
        if (!ctx) return;
        const w = Math.round($c.width());
        if (w === ctx.lastWidth) return;
        ctx.lastWidth = w;
        draw($c, ctx.series, ctx.currency, ctx.i18n, ctx.summary, ctx.height);
    }

    // Watch the container's own size, not just the window: the content area also changes width
    // when the action sidebar expands on hover (no window 'resize' event fires for that).
    const hasRO = typeof ResizeObserver !== 'undefined';
    let ro = null;
    if (hasRO) {
        let roTimer;
        const pending = new Set();
        ro = new ResizeObserver(function (entries) {
            entries.forEach(e => pending.add(e.target));
            clearTimeout(roTimer);
            roTimer = setTimeout(function () {
                pending.forEach(t => redraw($(t)));
                pending.clear();
            }, 120);
        });
    } else {
        // Fallback for browsers without ResizeObserver: window resize only.
        let resizeTimer;
        $(window).on('resize.restockgraph', function () {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(function () {
                $('.js-restock-graph').each(function () { redraw($(this)); });
            }, 150);
        });
    }

    // Public reusable renderer. Stores its context on the element and observes the element so
    // it redraws on any size change; usable from anywhere (e.g. the watchlist inline roll-down).
    window.renderRestockGraph = function (container, series, currency, i18n, summary, height) {
        const $c = $(container);
        if (!$c.length) return;
        const merged = $.extend({}, DEFAULT_I18N, i18n || {});
        // Seed lastWidth to the current width so the immediate ResizeObserver callback (which
        // fires once on observe()) doesn't trigger a redundant redraw.
        $c.addClass('js-restock-graph').data('rg', {
            series: series || [], currency: currency || '', i18n: merged, summary: summary || null,
            height: height || HEIGHT, lastWidth: Math.round($c.width())
        });
        draw($c, series || [], currency || '', merged, summary || null, height || HEIGHT);
        if (ro) {
            try { ro.unobserve($c[0]); } catch (e) {}
            ro.observe($c[0]);
        }
    };

    // Diff page bootstrap: fetch the timeline, render the graph + the data table.
    $(function () {
        const $container = $('#restock-graph');
        if (!$container.length || !window.restock_data_url) return;
        const i18n = $.extend({}, DEFAULT_I18N, window.restock_i18n || {});
        $.getJSON(window.restock_data_url).done(function (data) {
            const series = (data && data.series) || [];
            window.renderRestockGraph($container, series, (data && data.currency) || '', i18n, (data && data.summary) || null);
            buildTable($('#restock-history-table'), series, (data && data.currency) || '', i18n);
        }).fail(function () {
            $container.html($('<p class="pure-form-message-inline"></p>').text(i18n.load_error));
        });
    });

})(jQuery);
