/*!
 * subnet-bargraph.js — barre de remplissage d'un subnet (inspiré Avolites Titan).
 * 100 % client, 3 couches pures : métier → layout → rendu.
 * ES5 / IIFE, aligné sur le style de subnet.php. Aucune dépendance.
 */
window.SubnetBargraph = (function () {
    'use strict';

    // ── Helpers IP (entier non signé ↔ pointé) ──
    function intToIp(n) {
        n = n >>> 0;
        return [(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255].join('.');
    }

    // Nombre de bits d'hôte pour couvrir `count` adresses (arrondi sup. puissance de 2).
    function hostBitsFor(count) {
        var bits = 0, size = 1;
        while (size < count) { size *= 2; bits++; }
        return bits;
    }
    // Masque (pointé) du plus petit bloc CIDR contenant [startInt, endInt].
    function maskForCount(startInt, endInt) {
        var bits = hostBitsFor(endInt - startInt + 1);
        var mask = bits >= 32 ? 0 : (0xFFFFFFFF << bits) >>> 0;
        return intToIp(mask);
    }
    // Plus grand CIDR aligné entièrement contenu dans [startInt, endInt].
    // On teste les tailles décroissantes ; pour chaque taille, la 1re base alignée ≥ start
    // dont la fin ≤ end convient. Retourne "base/prefix".
    function largestAlignedCidr(startInt, endInt) {
        var span = endInt - startInt + 1;
        var bits = hostBitsFor(span); // borne haute de recherche
        for (; bits >= 0; bits--) {
            var size = Math.pow(2, bits);
            var mask = bits >= 32 ? 0 : (0xFFFFFFFF << bits) >>> 0;
            // 1re base alignée ≥ startInt
            var base = (startInt & mask) >>> 0;
            if (base < startInt) base = (base + size) >>> 0;
            if (base >= startInt && (base + size - 1) <= endInt) {
                return intToIp(base) + '/' + (32 - bits);
            }
        }
        // fallback (ne devrait pas arriver) : /32 sur start
        return intToIp(startInt) + '/32';
    }

    // Fusionne les réservations contiguës de MÊME type (toutes "reservation" ici),
    // puis injecte les pools comme blocs propres. Retourne la liste triée par startInt.
    function computeBlocks(rows, subnet) {
        var resv = [];
        for (var i = 0; i < rows.length; i++) {
            var r = rows[i];
            var ipInt = r.ip_int >>> 0;
            if (!ipInt) continue; // sans IP fixe → vit dans le pool
            if (ipInt < subnet.firstUsable || ipInt > subnet.lastUsable) {
                if (window.console) console.warn('bargraph: réservation hors CIDR ignorée', r);
                continue;
            }
            resv.push(r);
        }
        resv.sort(function (a, b) { return (a.ip_int >>> 0) - (b.ip_int >>> 0); });

        var blocks = [];
        var cur = null;
        for (var j = 0; j < resv.length; j++) {
            var row = resv[j];
            var ip = row.ip_int >>> 0;
            if (cur && ip === cur.endInt + 1) {
                cur.endInt = ip;
                cur.count++;
                cur.hostIds.push(row.host_id);
                accGroup(cur.groups, row.group_name);
                if (row.short_lease) cur.shortLeaseCount++;
            } else {
                cur = {
                    type: 'reservation',
                    startInt: ip, endInt: ip, count: 1,
                    groups: {}, shortLeaseCount: row.short_lease ? 1 : 0,
                    hostIds: [row.host_id]
                };
                accGroup(cur.groups, row.group_name);
                blocks.push(cur);
            }
        }

        // Pools
        if (subnet.dynStartInt != null && subnet.dynEndInt != null) {
            blocks.push(poolBlock('dynamic', subnet.dynStartInt, subnet.dynEndInt));
        }
        if (subnet.shortStartInt != null && subnet.shortEndInt != null) {
            blocks.push(poolBlock('short-pool', subnet.shortStartInt, subnet.shortEndInt));
        }

        // Blocs réservés hors DHCP
        parseReserved(subnet.reservedIps, subnet.networkInt >>> 0).forEach(function (r) {
            blocks.push(reservedBlock(r.startInt, r.endInt, r.desc));
        });

        blocks.sort(function (a, b) { return a.startInt - b.startInt; });
        return blocks;
    }
    function accGroup(groups, name) {
        var k = name || '';
        groups[k] = (groups[k] || 0) + 1;
    }
    function poolBlock(type, startInt, endInt) {
        return {
            type: type, startInt: startInt >>> 0, endInt: endInt >>> 0,
            count: (endInt >>> 0) - (startInt >>> 0) + 1,
            groups: {}, shortLeaseCount: 0, hostIds: []
        };
    }
    // Parse le texte "reservedIps" (même règle raccourcie) → blocs {startInt,endInt,desc} fusionnés.
    function parseReserved(raw, networkInt) {
        if (!raw) return [];
        function toInt(part) {
            if (!/^\d{1,3}(?:\.\d{1,3}){0,3}$/.test(part)) return null;
            var oct = part.split('.').map(Number);
            if (oct.length < 4) {
                var b = networkInt >>> 0;
                var base = [(b >>> 24) & 255, (b >>> 16) & 255, (b >>> 8) & 255, b & 255];
                oct = base.slice(0, 4 - oct.length).concat(oct);
            }
            if (oct.length !== 4) return null;
            for (var i = 0; i < 4; i++) { if (isNaN(oct[i]) || oct[i] < 0 || oct[i] > 255) return null; }
            return (oct[0] * 16777216 + oct[1] * 65536 + oct[2] * 256 + oct[3]) >>> 0;
        }
        var entries = [];
        raw.split(/\r\n|\r|\n/).forEach(function (line) {
            var hash = line.indexOf('#');
            var desc = hash >= 0 ? line.slice(hash + 1).trim() : '';
            var part = (hash >= 0 ? line.slice(0, hash) : line).trim();
            if (part === '') return;
            var a, e, dash = part.indexOf('-');
            if (dash >= 0) { a = toInt(part.slice(0, dash).trim()); e = toInt(part.slice(dash + 1).trim()); }
            else { a = toInt(part); e = a; }
            if (a == null || e == null) return; // invalide → ignoré côté rendu (le serveur bloque déjà)
            if (a > e) { var t = a; a = e; e = t; }
            entries.push({ startInt: a, endInt: e, desc: desc });
        });
        entries.sort(function (x, y) { return x.startInt - y.startInt; });
        var merged = [];
        entries.forEach(function (en) {
            var n = merged.length;
            if (n > 0 && en.startInt <= merged[n - 1].endInt + 1) {
                if (en.endInt > merged[n - 1].endInt) merged[n - 1].endInt = en.endInt;
                if (en.desc && merged[n - 1].desc.indexOf(en.desc) === -1)
                    merged[n - 1].desc = merged[n - 1].desc ? merged[n - 1].desc + ' · ' + en.desc : en.desc;
            } else {
                merged.push({ startInt: en.startInt, endInt: en.endInt, desc: en.desc });
            }
        });
        return merged;
    }
    function reservedBlock(startInt, endInt, desc) {
        return { type: 'reserved', startInt: startInt >>> 0, endInt: endInt >>> 0,
                 count: (endInt >>> 0) - (startInt >>> 0) + 1,
                 groups: {}, shortLeaseCount: 0, hostIds: [], desc: desc };
    }

    // Trous = intervalles [firstUsable, lastUsable] non couverts par les blocs (déjà triés).
    function computeHoles(blocks, subnet) {
        var holes = [];
        var cursor = subnet.firstUsable;
        for (var i = 0; i < blocks.length; i++) {
            var b = blocks[i];
            if (b.startInt > cursor) {
                holes.push(makeHole(cursor, b.startInt - 1));
            }
            if (b.endInt + 1 > cursor) cursor = b.endInt + 1;
        }
        if (cursor <= subnet.lastUsable) {
            holes.push(makeHole(cursor, subnet.lastUsable));
        }
        return holes;
    }
    function makeHole(startInt, endInt) {
        return {
            startInt: startInt, endInt: endInt,
            count: endInt - startInt + 1,
            largestCidr: largestAlignedCidr(startInt, endInt)
        };
    }

    // Projette blocs + trous en segments {kind, ref, leftPct, widthPct}.
    // Plancher minPx sur les BLOCS occupés uniquement, puis renormalisation globale (Σ=100).
    // Cas saturé (planchers > 100 %) : mise à l'échelle uniforme finale, planchers sacrifiés.
    function layoutSegments(blocks, holes, subnet, opts) {
        opts = opts || {};
        var barWidthPx = opts.barWidthPx || 0;
        var minPx = opts.minPx != null ? opts.minPx : 5;
        var minPct = barWidthPx > 0 ? (minPx / barWidthPx * 100) : 0;

        // 1) Séquence ordonnée blocs+trous
        var items = [];
        blocks.forEach(function (b) { items.push({ kind: 'block', ref: b, count: b.count }); });
        holes.forEach(function (h) { items.push({ kind: 'hole', ref: h, count: h.count }); });
        items.sort(function (a, b) { return a.ref.startInt - b.ref.startInt; });

        var total = subnet.total || 1;

        // 2) Largeur idéale
        items.forEach(function (it) { it.widthPct = it.count / total * 100; });

        // 3) Plancher sur les blocs
        if (minPct > 0) {
            items.forEach(function (it) {
                if (it.kind === 'block' && it.widthPct < minPct) {
                    it.floored = true;
                    it.widthPct = minPct;
                }
            });
            // 4) Renormalisation globale : retirer le surplus aux segments non planchés,
            //    proportionnellement, en itérant si un segment repasserait sous son plancher.
            renormalize(items, minPct);

            // 4b) Cas saturé : si les planchers seuls dépassent 100 % (barre trop étroite
            //     pour tous les micro-blocs), renormalize s'arrête au-dessus de 100. On met
            //     alors TOUT à l'échelle uniformément (planchers sacrifiés) pour garantir Σ=100.
            var saturated = 0;
            items.forEach(function (it) { saturated += it.widthPct; });
            if (saturated > 100 + 1e-6) {
                var k = 100 / saturated;
                items.forEach(function (it) { it.widthPct *= k; });
            }
        }

        // 5) Positions cumulées
        var left = 0, out = [];
        items.forEach(function (it) {
            out.push({ kind: it.kind, ref: it.ref, leftPct: left, widthPct: it.widthPct });
            left += it.widthPct;
        });
        return out;
    }
    // Ramène Σ widthPct à 100 en réduisant proportionnellement les segments "réductibles"
    // (au-dessus de leur plancher). Un bloc ne descend jamais sous minPct.
    function renormalize(items, minPct) {
        for (var pass = 0; pass < 20; pass++) {
            var sum = 0; items.forEach(function (it){ sum += it.widthPct; });
            var excess = sum - 100;
            if (Math.abs(excess) < 1e-6) return;
            var reducible = 0;
            items.forEach(function (it) {
                var floor = (it.kind === 'block') ? minPct : 0;
                if (it.widthPct > floor) reducible += (it.widthPct - floor);
            });
            if (reducible <= 1e-9) return; // rien à réduire (barre saturée de planchers)
            var ratio = excess / reducible;
            items.forEach(function (it) {
                var floor = (it.kind === 'block') ? minPct : 0;
                if (it.widthPct > floor) {
                    var room = it.widthPct - floor;
                    it.widthPct -= room * ratio;
                    if (it.widthPct < floor) it.widthPct = floor;
                }
            });
        }
    }

    var TYPE_LABEL = {
        'reservation': 'Réservation',
        'dynamic': 'Pool dynamique',
        'short-pool': 'Pool short-lease',
        'reserved': 'Réservé (hors DHCP)'
    };

    // Taux d'occupation (pools comptés comme occupés).
    // Compte les adresses DISTINCTES via l'union des intervalles (les blocs peuvent se
    // chevaucher : une réservation à IP fixe peut tomber dans le pool short-lease).
    function occupancy(blocks, subnet) {
        var iv = blocks.map(function (b) { return [b.startInt, b.endInt]; });
        iv.sort(function (a, b) { return a[0] - b[0]; });
        var occ = 0, curStart = null, curEnd = null;
        for (var i = 0; i < iv.length; i++) {
            var s = iv[i][0], e = iv[i][1];
            if (curEnd === null) { curStart = s; curEnd = e; }
            else if (s <= curEnd + 1) { if (e > curEnd) curEnd = e; } // chevauche ou adjacent
            else { occ += curEnd - curStart + 1; curStart = s; curEnd = e; }
        }
        if (curEnd !== null) occ += curEnd - curStart + 1;
        var pct = subnet.total ? Math.round(occ / subnet.total * 100) : 0;
        return { occ: occ, total: subnet.total, pct: pct };
    }

    function el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }

    // Construit le HTML d'infobulle d'un segment.
    function tipHtml(s, subnet) {
        var ref = s.ref;
        var startIp = intToIp(ref.startInt), endIp = intToIp(ref.endInt);
        var range = (ref.startInt === ref.endInt) ? startIp : (startIp + ' – ' + endIp);
        var rows = '';
        function row(k, v) { return '<div class="sbg-tip-row"><span>' + k + '</span><b>' + v + '</b></div>'; }

        if (s.kind === 'hole') {
            rows += row('Plage', range);
            rows += row('Adresses', ref.count);
            rows += row('Plus grand bloc', ref.largestCidr);
            return '<div class="sbg-tip-title">Libre</div>' + rows;
        }
        rows += row('Type', TYPE_LABEL[ref.type] || ref.type);
        rows += row('Plage', range);
        rows += row('Adresses', ref.count + ' (' + maskForCount(ref.startInt, ref.endInt) + ')');
        var title = TYPE_LABEL[ref.type] || 'Bloc';
        if (ref.type === 'reservation') {
            var chips = '';
            for (var g in ref.groups) {
                if (!ref.groups.hasOwnProperty(g)) continue;
                var label = g === '' ? '(sans groupe)' : g;
                chips += '<span class="sbg-grp">' + escapeHtml(label) + ' · ' + ref.groups[g] + '</span>';
            }
            if (chips) rows += '<div style="margin-top:4px">' + chips + '</div>';
            if (ref.shortLeaseCount > 0) rows += row('Dont short-lease', ref.shortLeaseCount);
        }
        if (ref.type === 'reserved' && ref.desc) {
            rows += row('Description', escapeHtml(ref.desc));
        }
        return '<div class="sbg-tip-title">' + title + '</div>' + rows;
    }
    function escapeHtml(str) {
        return String(str).replace(/[&<>"']/g, function (c) {
            return { '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c];
        });
    }

    // Branche survol (infobulle) + clic (filtre / ajout) sur un segment.
    function bindSegment(seg, s, tip, subnet, onFilter, canEdit) {
        seg.addEventListener('mouseenter', function () {
            tip.innerHTML = tipHtml(s, subnet);
            tip.classList.add('show');
        });
        seg.addEventListener('mousemove', function (ev) {
            var pad = 14;
            var x = ev.clientX + pad, y = ev.clientY + pad;
            var w = tip.offsetWidth, h = tip.offsetHeight;
            if (x + w > window.innerWidth)  x = ev.clientX - w - pad;
            if (y + h > window.innerHeight) y = ev.clientY - h - pad;
            tip.style.left = x + 'px';
            tip.style.top = y + 'px';
        });
        seg.addEventListener('mouseleave', function () {
            tip.classList.remove('show');
        });

        if (s.kind === 'block' && s.ref.type === 'reservation') {
            seg.addEventListener('click', function () {
                var q = intToIp(s.ref.startInt) + '-' + intToIp(s.ref.endInt);
                onFilter(q);
            });
        } else if (s.kind === 'hole' && canEdit) {
            seg.addEventListener('click', function () {
                var firstFreeIp = intToIp(s.ref.startInt);
                window.location.href = subnet.addUrl + '&ip=' + encodeURIComponent(firstFreeIp);
            });
        }
        // blocs pool : pas de clic (infobulle seule)
    }

    function init(cfg) {
        var mount = cfg.mount;
        if (!mount) return;
        var rows = cfg.rows || [];
        var subnet = cfg.subnet;
        var onFilter = cfg.onFilter || function () {};
        var canEdit = !!cfg.canEdit;
        var rateMount = cfg.rateMount || null; // si fourni, le taux y est écrit (hors barre)

        var blocks = computeBlocks(rows, subnet);
        var holes = computeHoles(blocks, subnet);

        var o = occupancy(blocks, subnet);
        var rateText = o.occ + ' / ' + o.total + ' occupées · ' + o.pct + ' %';

        mount.innerHTML = '';
        var wrap = el('div', 'sbg-wrap');
        if (rateMount) {
            rateMount.textContent = rateText;
        } else {
            var rate = el('div', 'sbg-rate');
            rate.textContent = rateText;
            wrap.appendChild(rate);
        }
        var bar = el('div', 'sbg-bar');
        wrap.appendChild(bar);
        mount.appendChild(wrap);

        // Infobulle unique réutilisée
        var tip = el('div', 'sbg-tip');
        document.body.appendChild(tip);

        function draw() {
            var barWidthPx = bar.clientWidth;
            if (barWidthPx <= 0) { requestAnimationFrame(draw); return; }
            bar.innerHTML = '';
            var segs = layoutSegments(blocks, holes, subnet, { barWidthPx: barWidthPx, minPx: 5 });
            segs.forEach(function (s) {
                var seg = el('div', 'sbg-seg sbg-' + (s.kind === 'hole' ? 'hole' : s.ref.type));
                seg.style.left = s.leftPct + '%';
                seg.style.width = s.widthPct + '%';
                bindSegment(seg, s, tip, subnet, onFilter, canEdit);
                bar.appendChild(seg);
            });
        }
        draw();

        var t;
        window.addEventListener('resize', function () {
            clearTimeout(t); t = setTimeout(draw, 120);
        });
    }

    return {
        intToIp: intToIp,
        maskForCount: maskForCount,
        largestAlignedCidr: largestAlignedCidr,
        computeBlocks: computeBlocks,
        computeHoles: computeHoles,
        layoutSegments: layoutSegments,
        init: init
    };
}());
