"""
MBIOWsMonitorModbusServerMixin -- Web monitor mixin for the Modbus TCP server.

Provides:
    /modbusserver           -- Overview page (status + register table)
    /api/v1/getmodbusserverinfo     -- JSON status
    /api/v1/getmodbusserverregisters -- JSON register list
    /api/v1/writemodbusregister     -- Debug write (POST)
"""

import json

from .wsmonitor_html import HtmlPage


class MBIOWsMonitorModbusServerMixin:
    """Monitor mixin for MBIOTaskModbusServer.

    Follows the same pattern as MBIOWsMonitorBACnetMixin: all methods run
    on a MBIOWsMonitor instance so self.getMBIO(), self.cb_write_json(), etc.
    are always available.
    """

    def _getMbsTasks(self):
        """Return all MBIOTaskModbusServer instances."""
        try:
            from .modbusserver import MBIOTaskModbusServer
            mbio = self.getMBIO()
            return [t for t in mbio.tasks.all() if isinstance(t, MBIOTaskModbusServer)]
        except Exception:
            return []

    def _hasMbsTasks(self):
        """Return True if at least one MBIOTaskModbusServer exists."""
        return len(self._getMbsTasks()) > 0

    def _modbusNavHtml(self):
        """Return Modbus nav link, or empty string if no Modbus server tasks."""
        if self._hasMbsTasks():
            return ' / <a href="/modbusserver">Modbus</a>'
        return ''

    # --- API callbacks ---

    def cb_getmodbusserverinfo(self, handler, params):
        """Return status information for all Modbus server tasks.
        GET /api/v1/getmodbusserverinfo[?task=&lt;taskkey&gt;]
          task (optional): filter to a specific task key
        Returns {"data": [{"task", "running", "host", "port", "unitid",
                           "n_hr", "n_ir", "n_co", "n_di", "n_expose"}, ...]}
        """
        task_filter = params.get('task')
        items = []
        for task in self._getMbsTasks():
            if task_filter and task.key != task_filter:
                continue
            status = task.getModbusStatus()
            items.append({
                'task': task.key,
                'running': status.get('running', False),
                'host': status.get('host', ''),
                'port': status.get('port', 0),
                'unitid': status.get('unitid', 1),
                'n_hr': status.get('n_hr', 0),
                'n_ir': status.get('n_ir', 0),
                'n_co': status.get('n_co', 0),
                'n_di': status.get('n_di', 0),
                'n_expose': status.get('n_expose', 0),
                'state': task.statestr(),
                'error': task.isError(),
                'stats': status.get('stats', {}),
            })
        self.cb_write_json(handler, {'data': items})
        return True

    def cb_getmodbusserverregisters(self, handler, params):
        """Return the register map for a Modbus server task.
        GET /api/v1/getmodbusserverregisters?task=&lt;taskkey&gt;
          task: task key
        Returns {"data": [{"name", "space", "addr", "n_regs", "dtype",
                           "unit", "scale", "offset", "writable", "is_expose"}, ...]}
        """
        task_key = params.get('task')
        if not task_key:
            return self.cb_write_failure(handler)
        mbio = self.getMBIO()
        task = mbio.task(task_key)
        if task is None:
            return self.cb_write_failure(handler)
        status = task.getModbusStatus()
        self.cb_write_json(handler, {'data': status.get('registers', [])})
        return True

    def cb_writemodbusregister(self, handler, params, data=None):
        """Write a value to a named Modbus register (debug/test endpoint).
        POST /api/v1/writemodbusregister
          Body JSON: {"task": "mbs", "name": "myvalue", "value": 42.0}
        Returns {"success": true} or HTTP 400 on failure.
        Only works for writable registers (HR / CO).
        """
        try:
            body = data or {}
            if not isinstance(body, dict):
                body = json.loads(body) if isinstance(body, (str, bytes)) else {}
            task_key = body.get('task') or params.get('task')
            name = body.get('name') or params.get('name')
            value = body.get('value')
            if task_key is None or name is None or value is None:
                return self.cb_write_failure(handler)
            mbio = self.getMBIO()
            task = mbio.task(task_key)
            if task is None:
                return self.cb_write_failure(handler)
            server = getattr(task, '_server', None)
            if server is None:
                return self.cb_write_failure(handler)
            # Update the value in the AV/BV dict
            if name in task._av:
                task._av[name]['value'].updateValue(float(value))
                task._save_writable_values()
                self.cb_write_success(handler)
                return True
            elif name in task._bv:
                task._bv[name]['value'].updateValue(bool(value))
                task._save_writable_values()
                self.cb_write_success(handler)
                return True
        except Exception as e:
            self.logger.warning('[ModbusServer] writemodbusregister error: %s' % e)
        return self.cb_write_failure(handler)

    # --- HTML page ---

    def cb_modbusserver(self, handler, params):
        """Modbus server overview page.
        GET /modbusserver[?task=&lt;taskkey&gt;]
        Displays server status and register map for all (or filtered) Modbus server tasks.
        """
        mbio = self.getMBIO()
        h = HtmlPage('Digimat MBIO Modbus Server', handler)
        h.variable('mbio.version', mbio.version)
        h.variable('main.nav', self._navHtml('/modbusserver'))
        h.variable('bacnet.nav', self._bacnetNavHtml() + self._logoutHtml())

        tasks = self._getMbsTasks()
        task_param = params.get('task', tasks[0].key if tasks else '')
        h.variable('task.key', task_param)

        # Build task selector options
        task_options = ''
        for t in tasks:
            sel = ' selected' if t.key == task_param else ''
            task_options += '<option value="%s"%s>%s</option>' % (t.key, sel, t.key)
        h.variable('task.options', task_options)

        data = """
            <div class="page container-fluid">
                <div class="card p-4 p-lg-5">
                    <div class="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-3">
                    <div>
                        <h1 class="h3 mb-1">Digimat MBIO Processor Monitor</h1>
                        <p class="text-secondary mb-0"><b>Modbus Server</b></p>
                    </div>
                    <div class="text-muted small">{mbio.version} /
                    {main.nav}{bacnet.nav}
                    </div>
                    </div>
                    <ul class="nav nav-tabs mt-3 mb-0 d-flex align-items-center" role="tablist">
                    <li class="nav-item" role="presentation">
                        <button class="nav-link active" id="tab-status-btn"
                            data-bs-toggle="tab" data-bs-target="#tab-status" type="button">Status</button>
                    </li>
                    <li class="nav-item" role="presentation">
                        <button class="nav-link" id="tab-map-btn"
                            data-bs-toggle="tab" data-bs-target="#tab-map" type="button">Map</button>
                    </li>
                    <li class="nav-item" role="presentation">
                        <button class="nav-link" id="tab-registers-btn"
                            data-bs-toggle="tab" data-bs-target="#tab-registers" type="button">Variables</button>
                    </li>
                    <li class="nav-item ms-auto pb-1 d-flex gap-2 align-items-center">
                        <select id="task-select" class="form-select form-select-sm" style="width:auto">
                            {task.options}
                        </select>
                        <button id="btn-refresh" class="btn btn-sm btn-outline-primary">Refresh</button>
                    </li>
                    </ul>
                    <div class="tab-content border border-top-0 rounded-bottom p-3 mb-2">

                    <div class="tab-pane fade show active" id="tab-status" role="tabpanel">
                        <div id="status-info" class="d-flex flex-wrap gap-3 mb-3 align-items-center small"></div>
                        <table id="status-table" class="display nowrap" style="width:100%">
                        <thead><tr>
                            <th>Space</th>
                            <th>Type</th>
                            <th>Variables</th>
                            <th>Reads</th>
                            <th>Writes</th>
                        </tr></thead>
                        </table>
                    </div>

                    <div class="tab-pane fade" id="tab-registers" role="tabpanel">
                        <table id="registers-table" class="display nowrap" style="width:100%">
                        <thead>
                            <tr>
                                <th>Name</th>
                                <th>Space</th>
                                <th>Addr</th>
                                <th>Regs</th>
                                <th>Type</th>
                                <th>Value</th>
                                <th>Writable</th>
                                <th>Scale</th>
                                <th>Offset</th>
                                <th>Raw</th>
                                <th>Reads</th>
                                <th>Writes</th>
                            </tr>
                        </thead>
                        </table>
                    </div>

                    <div class="tab-pane fade" id="tab-map" role="tabpanel">
                        <div id="map-container" class="row g-3"></div>
                    </div>

                    <!-- Edit modal (writable registers) -->
                    <div class="modal fade" id="mbsEditModal" tabindex="-1">
                    <div class="modal-dialog modal-sm modal-dialog-centered">
                    <div class="modal-content">
                    <div class="modal-header py-2">
                        <h6 class="modal-title" id="mbsEditModalTitle">Edit value</h6>
                        <button type="button" class="btn-close btn-close-sm" data-bs-dismiss="modal"></button>
                    </div>
                    <div class="modal-body py-3" id="mbsEditModalBody"></div>
                    <div class="modal-footer py-1">
                        <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
                        <button type="button" class="btn btn-sm btn-primary" id="mbsEditModalOk">OK</button>
                    </div>
                    </div></div></div>

                    </div>
                </div>
            </div>
        """
        h.write(data)

        data = """
        <script>
        var currentTask = '{task.key}';
        var regTable = null;
        var statusTable = null;

        function spaceLabel(space) {
            return {hr: 'HR', ir: 'IR', co: 'CO', di: 'DI'}[space] || space.toUpperCase();
        }

        function loadStatus() {
            $.get('/api/v1/getmodbusserverinfo?task=' + encodeURIComponent(currentTask), function(resp) {
                if (!resp.data || resp.data.length === 0) {
                    $('#status-info').html('<span class="text-muted">No Modbus server tasks found.</span>');
                    return;
                }
                // Use first matching task (selector filters to one task)
                var s = resp.data[0];
                var st = s.stats || {};

                // --- Header info strip ---
                var runBadge = s.running
                    ? '<span class="badge bg-success">Running</span>'
                    : '<span class="badge bg-danger">Stopped</span>';
                $('#status-info').html(
                    runBadge +
                    '<span class="text-muted">|</span>' +
                    '<span><span class="text-muted">Host:Port</span> <strong>' + s.host + ':' + s.port + '</strong></span>' +
                    '<span><span class="text-muted">Unit ID</span> <strong>' + s.unitid + '</strong></span>' +
                    '<span><span class="text-muted">State</span> <strong>' + s.state + '</strong></span>' +
                    '<span><span class="text-muted">Exposed</span> <strong>' + s.n_expose + '</strong></span>'
                );

                // --- DataTable rows ---
                var rows = [
                    { space: 'HR', type: 'R/W Analog',   vars: s.n_hr, reads: st.hr_reads || 0, writes: st.hr_writes || 0, writable: true  },
                    { space: 'IR', type: 'RO Analog',    vars: s.n_ir, reads: st.ir_reads || 0, writes: null,               writable: false },
                    { space: 'CO', type: 'R/W Digital',  vars: s.n_co, reads: st.co_reads || 0, writes: st.co_writes || 0, writable: true  },
                    { space: 'DI', type: 'RO Digital',   vars: s.n_di, reads: st.di_reads || 0, writes: null,               writable: false },
                ];

                if (!statusTable) {
                    statusTable = new DataTable('#status-table', {
                        paging: false, searching: false, ordering: false, info: false,
                        data: rows,
                        columns: [
                            { data: 'space', render: function(d, type, row) {
                                if (type !== 'display') return d;
                                var cls = (row.writable) ? 'bg-warning text-dark' : 'bg-secondary text-white';
                                return '<span class="badge ' + cls + '">' + d + '</span>';
                            }},
                            { data: 'type' },
                            { data: 'vars', className: 'text-end font-monospace' },
                            { data: 'reads', className: 'text-end font-monospace' },
                            { data: 'writes', className: 'text-end font-monospace', render: function(d) {
                                return d === null ? '<span class="text-muted">—</span>' : d;
                            }},
                        ],
                    });
                } else {
                    statusTable.clear().rows.add(rows).draw(false);
                }
            });
        }

        function loadRegisters() {
            if (regTable) {
                regTable.ajax.reload();
                return;
            }
            regTable = new DataTable('#registers-table', {
                responsive: true,
                paging: false,
                searching: true,
                ordering: true,
                info: false,
                search: { smart: true, regex: false, caseInsensitive: true, boundary: false },
                ajax: {
                    url: '/api/v1/getmodbusserverregisters?task=' + encodeURIComponent(currentTask),
                    dataSrc: 'data'
                },
                columns: [
                    { data: 'name', render: function(d, type, row) {
                        if (type !== 'display') return d;
                        var txt = row.writable ? '<strong>' + d + '</strong>' : d;
                        if (row.overlap) {
                            txt += ' <span class="badge bg-danger ms-1" style="font-size:.7em" title="Address overlap with another register">ERR</span>';
                        }
                        if (row.is_expose) {
                            txt += ' <span class="badge bg-info text-dark ms-1" style="font-size:.7em">exp</span>';
                        }
                        return txt;
                    }},
                    { data: 'space', render: function(d) { return spaceLabel(d); } },
                    { data: 'addr' },
                    { data: 'n_regs' },
                    { data: 'dtype', render: function(d) { return d || '—'; } },
                    { data: 'value', render: function(d, type, row) {
                        if (type !== 'display') return d == null ? '' : d;
                        var txt;
                        if (row.space === 'co' || row.space === 'di') {
                            txt = (d === true || d === 1) ? '<span class="badge bg-success">True</span>' : '<span class="badge bg-secondary">False</span>';
                        } else {
                            txt = (d == null) ? '<span class="text-muted">—</span>' : String(d);
                        }
                        if (row.writable) {
                            txt += ' <button class="btn btn-link btn-sm p-0 ms-1 js-mbs-edit" data-name="' + row.name + '" data-space="' + row.space + '" data-value="' + d + '" title="Edit" style="opacity:.65;font-size:.9em">&#x270E;</button>';
                        }
                        return txt;
                    }},
                    { data: 'writable', render: function(d) { return d ? '<span class="badge bg-warning text-dark">R/W</span>' : '<span class="badge bg-secondary">RO</span>'; } },
                    { data: 'scale' },
                    { data: 'offset' },
                    { data: 'raw', render: function(d) { return d == null ? '<span class="text-muted">—</span>' : d; } },
                    { data: 'reads', className: 'text-end font-monospace',
                      render: function(d) { return d == null ? '<span class="text-muted">—</span>' : d; } },
                    { data: 'writes', className: 'text-end font-monospace',
                      render: function(d) { return d == null ? '<span class="text-muted">—</span>' : d; } },
                ],
                rowCallback: function(row, data) {
                    if (data.overlap) {
                        $(row).addClass('table-danger');
                    } else if (data.is_expose) {
                        $('td', row).css('color', '#555');
                    }
                }
            });
        }

        // --- Memory Map ---

        var _mapData = null;

        var _spaceConfig = [
            { space: 'hr', label: 'HR — Holding Registers (4x)',  badge: 'bg-warning text-dark', badgeRO: null,           desc: 'R/W analog' },
            { space: 'ir', label: 'IR — Input Registers (3x)',    badge: 'bg-primary text-white', badgeRO: null,          desc: 'RO analog'  },
            { space: 'co', label: 'CO — Coils (0x)',              badge: 'bg-warning text-dark', badgeRO: null,           desc: 'R/W digital'},
            { space: 'di', label: 'DI — Discrete Inputs (1x)',   badge: 'bg-secondary text-white', badgeRO: null,        desc: 'RO digital' },
        ];

        function renderValueCell(row) {
            var d = row.value;
            var txt;
            if (row.space === 'co' || row.space === 'di') {
                txt = (d === true || d === 1) ? '<span class="badge bg-success">True</span>' : '<span class="badge bg-secondary">False</span>';
            } else {
                txt = (d == null) ? '<span class="text-muted">—</span>' : String(d);
            }
            if (row.writable) {
                txt += ' <button class="btn btn-link btn-sm p-0 ms-1 js-mbs-edit" data-name="' + row.name + '" data-space="' + row.space + '" data-value="' + d + '" title="Edit" style="opacity:.65;font-size:.9em">&#x270E;</button>';
            }
            return txt;
        }

        function renderMapSpace(cfg, entries) {
            // Sort by address
            var rows = entries.filter(function(e) { return e.space === cfg.space; });
            rows.sort(function(a, b) { return a.addr - b.addr; });

            var tableRows = '';
            var prevEnd = -1;

            rows.forEach(function(row) {
                // Gap row
                if (row.addr > prevEnd + 1) {
                    var gapStart = prevEnd + 1;
                    var gapCount = row.addr - gapStart;
                    var gapLabel = gapCount === 1
                        ? 'addr ' + gapStart
                        : 'addr ' + gapStart + ' – ' + (row.addr - 1);
                    tableRows += '<tr class="text-muted" style="font-size:.8em;background:#f8f9fa">' +
                        '<td colspan="5" class="py-0 ps-3 fst-italic">' +
                        '&#xB7;&#xB7;&#xB7; ' + gapCount + ' free reg' + (gapCount > 1 ? 's' : '') +
                        ' <span class="text-muted" style="font-size:.85em">(' + gapLabel + ')</span>' +
                        '</td></tr>';
                }
                prevEnd = row.addr + row.n_regs - 1;

                var addrStr = row.n_regs > 1
                    ? row.addr + '–' + (row.addr + row.n_regs - 1)
                    : String(row.addr);

                var nameBadges = '';
                if (row.is_expose) {
                    nameBadges += '<span class="badge bg-info text-dark me-1" style="font-size:.7em">exp</span>';
                }
                if (row.overlap) {
                    nameBadges += '<span class="badge bg-danger me-1" style="font-size:.7em" title="Address overlap">ERR</span>';
                }

                var rawStr = row.raw == null ? '<span class="text-muted">—</span>' : row.raw;
                var typeStr = row.dtype || (row.space === 'co' || row.space === 'di' ? 'bool' : '—');
                var rowCls = row.overlap ? ' class="table-danger"' : '';

                tableRows += '<tr' + rowCls + '>' +
                    '<td class="font-monospace small py-1">' + addrStr + '</td>' +
                    '<td class="py-1">' + nameBadges + (row.writable ? '<strong>' + row.name + '</strong>' : row.name) + '</td>' +
                    '<td class="py-1 text-muted small">' + typeStr + '</td>' +
                    '<td class="py-1">' + renderValueCell(row) + '</td>' +
                    '<td class="py-1 text-muted small font-monospace">' + rawStr + '</td>' +
                    '</tr>';
            });

            if (!tableRows) {
                tableRows = '<tr><td colspan="5" class="text-muted fst-italic small py-2">— empty —</td></tr>';
            }

            var rwBadge = (cfg.space === 'hr' || cfg.space === 'co')
                ? '<span class="badge bg-warning text-dark ms-2" style="font-size:.7em">R/W</span>'
                : '<span class="badge bg-secondary ms-2" style="font-size:.7em">RO</span>';

            return '<div class="col-12 col-xl-6">' +
                '<div class="card h-100 shadow-sm">' +
                '<div class="card-header py-2 d-flex align-items-center justify-content-between">' +
                '<span class="fw-bold small">' + cfg.label + rwBadge + '</span>' +
                '<span class="text-muted small">' + rows.length + ' var' + (rows.length !== 1 ? 's' : '') + '</span>' +
                '</div>' +
                '<div class="card-body p-0">' +
                '<table class="table table-sm table-hover mb-0">' +
                '<thead class="table-light"><tr>' +
                '<th class="py-1 ps-3" style="width:6em">Addr</th>' +
                '<th class="py-1">Name</th>' +
                '<th class="py-1" style="width:7em">Type</th>' +
                '<th class="py-1">Value</th>' +
                '<th class="py-1" style="width:7em">Raw</th>' +
                '</tr></thead>' +
                '<tbody>' + tableRows + '</tbody>' +
                '</table></div></div></div>';
        }

        function loadMap(force) {
            $.get('/api/v1/getmodbusserverregisters?task=' + encodeURIComponent(currentTask), function(resp) {
                _mapData = resp.data || [];
                renderMap();
            });
        }

        function renderMap() {
            if (!_mapData) return;
            var container = $('#map-container');
            container.empty();
            _spaceConfig.forEach(function(cfg) {
                container.append(renderMapSpace(cfg, _mapData));
            });
        }

        var _editModal = null;
        var _editEntry = null;

        function openEditModal(name, space, currentVal) {
            _editEntry = {name: name, space: space};
            $('#mbsEditModalTitle').text('Edit — ' + name);
            var body = '';
            if (space === 'co') {
                var checked = (currentVal === true || currentVal === 1 || currentVal === 'true') ? ' checked' : '';
                body = '<div class="form-check form-switch">' +
                    '<input class="form-check-input" type="checkbox" id="mbsEditBool" role="switch"' + checked + '>' +
                    '<label class="form-check-label" for="mbsEditBool">Value</label></div>';
            } else {
                var numVal = (currentVal == null || currentVal === 'null') ? '' : currentVal;
                body = '<label class="form-label small mb-1">Value</label>' +
                    '<input type="number" step="any" id="mbsEditNum" class="form-control form-control-sm" value="' + numVal + '">';
            }
            $('#mbsEditModalBody').html(body);
            if (!_editModal) {
                _editModal = new bootstrap.Modal('#mbsEditModal');
                document.getElementById('mbsEditModal').addEventListener('shown.bs.modal', function() {
                    var el = document.getElementById('mbsEditNum') || document.getElementById('mbsEditBool');
                    if (el) { el.focus(); if (el.select) el.select(); }
                });
            }
            _editModal.show();
        }

        $('#mbsEditModalOk').on('click', function() {
            if (!_editEntry) return;
            var val;
            if (_editEntry.space === 'co') {
                val = $('#mbsEditBool').is(':checked') ? 1 : 0;
            } else {
                val = parseFloat($('#mbsEditNum').val());
                if (isNaN(val)) { alert('Invalid value'); return; }
            }
            $.ajax({
                url: '/api/v1/writemodbusregister',
                method: 'POST',
                contentType: 'application/json',
                data: JSON.stringify({task: currentTask, name: _editEntry.name, value: val}),
                success: function() {
                    if (_editModal) _editModal.hide();
                    if (regTable) { regTable.ajax.reload(null, false); }
                    if ($('#tab-map').hasClass('show')) { loadMap(); }
                },
                error: function() { alert('Write failed'); }
            });
        });

        $('#mbsEditModal').on('keydown', function(e) {
            if (e.key === 'Enter') { e.preventDefault(); $('#mbsEditModalOk').click(); }
        });

        $(document).on('click', '.js-mbs-edit', function(e) {
            e.preventDefault();
            e.stopPropagation();
            var btn = $(this);
            openEditModal(btn.data('name'), btn.data('space'), btn.data('value'));
        });

        function autoRefresh() {
            loadStatus();
            if ($('#tab-registers').hasClass('show') && regTable) {
                regTable.ajax.reload(null, false);
            }
            if ($('#tab-map').hasClass('show')) {
                loadMap();
            }
        }

        $(function() {
            loadStatus();
            setInterval(autoRefresh, 10000);

            $('#task-select').on('change', function() {
                currentTask = $(this).val();
                _mapData = null;
                loadStatus();
                if (regTable) {
                    regTable.destroy();
                    regTable = null;
                    $('#registers-table').empty().append('<thead><tr>' +
                        '<th>Name</th><th>Space</th><th>Addr</th><th>Regs</th>' +
                        '<th>Type</th><th>Value</th><th>Writable</th><th>Scale</th><th>Offset</th>' +
                        '<th>Raw</th><th>Reads</th><th>Writes</th></tr></thead>');
                }
                if ($('#tab-registers').hasClass('show')) { loadRegisters(); }
                if ($('#tab-map').hasClass('show')) { loadMap(); }
            });

            $('#btn-refresh').on('click', function() {
                loadStatus();
                if (regTable) { regTable.ajax.reload(null, false); }
                if ($('#tab-map').hasClass('show')) { loadMap(); }
            });

            $('button[data-bs-target="#tab-registers"]').on('shown.bs.tab', function() {
                loadRegisters();
            });

            $('button[data-bs-target="#tab-map"]').on('shown.bs.tab', function() {
                loadMap();
            });
        });
        </script>
        """
        h.write(data)
        h.flush()
        return True
