import importlib_resources


class Html():
    def __init__(self, title=None, handler=None):
        self._handler=handler
        self._header=''
        self._body=''
        self._variables={}
        self.onInit()

        if title:
            self.header(f'<title>{title}</title>')

    def onInit(self):
        pass

    def header(self, data):
        if data:
            self._header+=data
            self._header+='\n'

    def style(self, data):
        if data:
            self.header(f'<style>{data}</style>')

    def variable(self, name, value):
        if name:
            name='{%s}' % name.lower()
            if value is None:
                value=''
            value=str(value)
            self._variables[name]=value

    def body(self, data):
        if data:
            if self._variables:
                for v in self._variables.keys():
                    data=data.replace(v, self._variables[v])
            self._body+=data
            self._body+='\n'

    def readFile(self, fname):
        try:
            my_resources = importlib_resources.files("digimat.mbio")
            data = my_resources.joinpath(fname).read_text()
            self.write(data)
            return True
        except:
            pass
        return False

    def write(self, data):
        self.body(data)

    def data(self):
        if self._header:
            data='<html>'
            data+='<head>'
            data+=self._header
            data+='</head>'
            data+='<body>'
            data+=self._body
            data+='</body>'
            data+='</html>'
        else:
            data=self._body
        return data

    def bytes(self):
        return self.data().encode('utf-8')

    def flush(self):
        handler=self._handler
        if handler:
            handler.send_response(200)
            handler.send_header("Content-Type", "text/html; charset=utf-8")
            handler.end_headers()
            handler.wfile.write(self.bytes())

    def button(self, bid, name):
        self.write(f'<button id="{bid}" class="ui-button red-button ui-widget ui-corner-all">{name}</button>')


class HtmlPage(Html):
    def onInit(self):
        super().onInit()

        self.header('<meta charset="utf-8" />')
        self.header('<meta name="viewport" content="width=device-width, initial-scale=1" />')

        # self.header('<link rel="stylesheet" href="/www/jquery-ui/jquery-ui.min.css">')
        # self.header('<script src="/www/jquery-ui/external/jquery/jquery.js"></script>')
        # self.header('<script src="/www/jquery-ui/jquery-ui.min.js"></script>')

        # https://jquery.com/download/
        self.body('<script src="/www/jquery/jquery.min.js"></script>')
        self.body('<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>')

        # https://getbootstrap.com/docs/5.0/getting-started/download/
        self.header('<link href="/www/Bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">')
        self.body('<script src="/www/Bootstrap/dist/js/bootstrap.bundle.min.js"></script>')

        # https://datatables.net/download/
        self.header('<link href="/www/DataTables/datatables.min.css" rel="stylesheet">')
        self.body('<script src="/www/DataTables/datatables.min.js"></script>')

        self.style('.red-button {background-color: red; color: white;}')

        data="""
        .form-switch {
        position: relative;
        display: inline-block;
        width: 42px;
        height: 22px;
        }

        .form-switch input {
        opacity: 0;
        width: 0;
        height: 0;
        }

        .form-switch .slider {
        position: absolute;
        cursor: pointer;
        top: 0; left: 0; right: 0; bottom: 0;
        background-color: #ccc;
        border-radius: 22px;
        transition: .3s;
        }

        .form-switch .slider:before {
        position: absolute;
        content: "";
        height: 18px; width: 18px;
        left: 2px; bottom: 2px;
        background-color: white;
        border-radius: 50%;
        transition: .3s;
        }

        .form-switch input:checked + .slider {
        background-color: #4caf50;
        }

        .form-switch input:checked + .slider:before {
        transform: translateX(20px);
        }

        @keyframes flashRow { 0%{background:#fff3cd;} 100%{background:white;} }
        .flash-update { animation: flashRow 1.2s ease-out 1; }
        """
        self.style(data)

        self.body('<div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index:9999"></div>')
        self.body(r"""<script>
$.fn.dataTable.ext.errMode = 'none';

function showToast(message, type) {
    var bg = type === 'success' ? 'bg-success' : type === 'danger' ? 'bg-danger' : 'bg-primary';
    var id = 'toast-' + Date.now();
    var html = '<div id="' + id + '" class="toast align-items-center text-white ' + bg + ' border-0" role="alert" aria-atomic="true">'
             + '<div class="d-flex"><div class="toast-body">' + message + '</div>'
             + '<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>'
             + '</div></div>';
    $('#toast-container').append(html);
    var el = document.getElementById(id);
    var toast = new bootstrap.Toast(el, {delay: 3500});
    toast.show();
    el.addEventListener('hidden.bs.toast', function() { el.remove(); });
}

// Generic DataTable copy-to-clipboard (TSV).
// Options:
//   selector      : DataTables row selector (default: {search:'applied'} = visible rows)
//   extraColumns  : [{header: str, getValue: fn(rowData)}] prepended before table columns
//   skipClasses   : th CSS classes to skip (default: ['dt-no-copy','dt-select'])
// Buttons and inputs are stripped from rendered HTML before extracting text.
function copyDataTableToClipboard(dt, options) {
    options = options || {};
    var skipClasses = options.skipClasses || ['dt-no-copy', 'dt-select'];
    var extraColumns = options.extraColumns || [];

    function cellText(rendered) {
        if (rendered === null || rendered === undefined) return '';
        var s = String(rendered);
        if (s.indexOf('<') === -1) return s.trim();
        try {
            var doc = new DOMParser().parseFromString(s, 'text/html');
            doc.querySelectorAll('button, input').forEach(function(el) { el.remove(); });
            return doc.body.textContent.trim();
        } catch(e) {
            return s.replace(/<[^>]+>/g, '').trim();
        }
    }

    var colHeaders = [], colIndices = [];
    dt.columns().every(function(i) {
        var th = $(this.header());
        var skip = skipClasses.some(function(cls) { return th.hasClass(cls); });
        if (!skip) {
            colHeaders.push(th.text().trim());
            colIndices.push(i);
        }
    });

    var headers = extraColumns.map(function(c) { return c.header; }).concat(colHeaders);
    var selector = options.selector !== undefined ? options.selector : {search: 'applied'};
    var rowsApi = dt.rows(selector);
    var rowNodes = rowsApi.nodes().toArray();
    var rowData = rowsApi.data().toArray();
    if (rowData.length === 0) return;

    var lines = [headers.join('\t')];
    rowData.forEach(function(data, i) {
        var cells = [];
        extraColumns.forEach(function(c) {
            var v = c.getValue(data);
            cells.push(v !== null && v !== undefined ? String(v) : '');
        });
        var node = rowNodes[i];
        colIndices.forEach(function(colIdx) {
            var text = '';
            if (node) {
                text = cellText(dt.cell(node, colIdx).render('display'));
            } else {
                var field = dt.column(colIdx).dataSrc();
                var raw = typeof field === 'string' ? data[field] : '';
                text = raw !== null && raw !== undefined ? String(raw) : '';
            }
            cells.push(text);
        });
        lines.push(cells.join('\t'));
    });

    var text = lines.join('\n');
    var count = rowData.length;

    function showFeedback() {
        showToast('Copied ' + count + ' row' + (count !== 1 ? 's' : '') + ' to clipboard', 'success');
    }
    function fallback() {
        var ta = document.createElement('textarea');
        ta.value = text;
        ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
        document.body.appendChild(ta);
        ta.focus(); ta.select();
        try { document.execCommand('copy'); showFeedback(); } catch(e) {}
        document.body.removeChild(ta);
    }
    if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(text).then(showFeedback).catch(fallback);
    } else {
        fallback();
    }
}

// [/] or [Ctrl+F] → focus the main search input on the page.
// Looks for [data-search-focus] first; falls back to DataTables #dt-search-0.
(function() {
    $(document).on('keydown', function(e) {
        if ($(e.target).is('input,textarea,select')) return;
        var slash = !e.ctrlKey && !e.metaKey && !e.altKey && e.key === '/';
        var ctrlf = (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key === 'f';
        if (!slash && !ctrlf) return;
        var $s = $('[data-search-focus]').first();
        if (!$s.length) $s = $('#dt-search-0.dt-input').first();
        if ($s.length) { e.preventDefault(); $s.focus().select(); }
    });
})();

// Session watchdog: redirect to /login on session expiry; show offline overlay when server is unreachable.
// Uses redirect:'manual' — an opaque redirect means the server sent us to /login.
// No-op when authentication is disabled (server returns 200 normally).
(function() {
    function showOfflineOverlay() {
        if (document.getElementById('_mbio-offline')) return;
        var ov = document.createElement('div');
        ov.id = '_mbio-offline';
        ov.style.cssText = 'position:fixed;inset:0;background:rgba(15,23,42,.97);'
            + 'color:#f1f5f9;z-index:99999;display:flex;align-items:center;justify-content:center;'
            + 'font-family:system-ui,sans-serif;';
        ov.innerHTML =
            '<div style="text-align:center;padding:2.5rem;max-width:380px">'
            + '<svg style="width:52px;height:52px;stroke:#f87171;fill:none;stroke-width:1.8;'
            + 'margin-bottom:1.2rem;display:block;margin-left:auto;margin-right:auto" viewBox="0 0 24 24">'
            + '<circle cx="12" cy="12" r="10"/>'
            + '<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/>'
            + '</svg>'
            + '<div style="font-size:1.35rem;font-weight:600;margin-bottom:.6rem">Service offline</div>'
            + '<div style="font-size:.9rem;color:#94a3b8;margin-bottom:2rem">'
            + 'The MBIO server is unreachable.</div>'
            + '<div style="font-size:.85rem;color:#60a5fa">'
            + 'Reconnecting in <b id="_mbio-offline-n">5</b>s...</div>'
            + '</div>';
        document.body.appendChild(ov);
        startRetryCountdown();
    }

    function startRetryCountdown() {
        var n = 5;
        var el = document.getElementById('_mbio-offline-n');
        if (el) el.textContent = n;
        var t = setInterval(function() {
            n--;
            var el2 = document.getElementById('_mbio-offline-n');
            if (el2) el2.textContent = Math.max(n, 0);
            if (n <= 0) { clearInterval(t); tryReconnect(); }
        }, 1000);
    }

    function tryReconnect() {
        fetch('/api/v1/getsessions', {redirect: 'manual'})
            .then(function() { window.location.reload(); })
            .catch(function() { startRetryCountdown(); });
    }

    var _sw = setInterval(function() {
        fetch('/api/v1/getsessions', {redirect: 'manual'})
            .then(function(r) {
                if (r.type === 'opaqueredirect' || r.status === 401 || r.status === 403) {
                    clearInterval(_sw);
                    window.location.href = '/login';
                }
            })
            .catch(function() {
                clearInterval(_sw);
                showOfflineOverlay();
            });
    }, 15000);
})();
</script>""")

    def header(self, data):
        if data:
            self._header+=data
            self._header+='\n'

    def style(self, data):
        if data:
            self.header(f'<style>{data}</style>')

    def variable(self, name, value):
        if name:
            name='{%s}' % name.lower()
            if value is None:
                value=''
            value=str(value)
            self._variables[name]=value

    def body(self, data):
        if data:
            if self._variables:
                for v in self._variables.keys():
                    data=data.replace(v, self._variables[v])
            self._body+=data
            self._body+='\n'

    def write(self, data):
        self.body(data)

    def data(self):
        data='<html>'
        data+='<head>'
        data+=self._header
        data+='</head>'
        data+='<body>'
        data+=self._body
        data+='</body>'
        data+='</html>'
        return data

    def bytes(self):
        return self.data().encode('utf-8')

    def flush(self):
        handler=self._handler
        if handler:
            handler.send_response(200)
            handler.send_header("Content-Type", "text/html; charset=utf-8")
            handler.end_headers()
            handler.wfile.write(self.bytes())

    def button(self, bid, name):
        self.write(f'<button id="{bid}" class="ui-button red-button ui-widget ui-corner-all">{name}</button>')

