<?php
/*
** Copyright (C) 2021-2026 initMAX s.r.o.
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/


namespace Modules\uxMAX\Services;

use APP;

/**
 * uxMAX's additions to Zabbix's own "Latest data" page - written ONCE for both
 * module trees.
 *
 * The strategy is deliberately conservative: uxMAX renders CORE's view
 * unchanged (by including the stock file) and only appends a payload plus a
 * little CSS/JS on top. That is what lets one implementation serve 6.0 through
 * 7.4 - the page markup is whatever that frontend's own version produces, and
 * uxMAX never has to keep a fork of it in step.
 */
final class LatestViewPage {

    /**
     * Include one of core's own view files with the same bindings CView gives
     * it: `$this` is the view, `$data` is the controller's data.
     *
     * Both matter. `include` inside a closure sees that closure's locals, so
     * `$data` has to be imported explicitly - core's Latest data view reads it
     * on its very first lines and dies on null, taking the whole page with it.
     */
    private static function includeCore($view, array $data, string $relative,
            bool $strip_header = false): bool {
        $path = APP::getRootDir().'/app/views/'.$relative;

        if (!is_readable($path)) {
            error(_s('Cannot read original view: "%1$s".', $path));

            return false;
        }

        if (!$strip_header) {
            $render = function () use ($path, $data) {
                include $path;
            };
            $render->call($view);

            return true;
        }

        // Included by eval() rather than include(): core's file opens with its
        // own `declare(strict_types)` header, which may not appear after the
        // first statement of the including file. Everything after the first
        // PHP close tag is plain template and evaluates cleanly.
        // (A literal close tag must not appear even in a comment here - PHP
        // leaves PHP mode inside a // comment too, and the file then ends
        // mid-class with "Unclosed '{'".)
        $source = (string) file_get_contents($path);
        $close = strpos($source, '?'.'>');
        $template = ($close !== false) ? substr($source, $close + 2) : $source;

        $render = function () use ($template, $data) {
            eval('?'.'>'.$template);
        };
        $render->call($view);

        return true;
    }

    /**
     * The Latest data page itself: core's view verbatim, plus the ids of the
     * disabled items in the current result set for the client to grey out.
     */
    public static function showView($view, array $data): void {
        if (!self::includeCore($view, $data, 'monitoring.latest.view.php')) {
            return;
        }

        echo '<script>window.uxmaxDisabledItemIds = '
            .json_encode(self::disabledItemIds($data['items'] ?? []))
            .";</script>\n";
    }

    /**
     * The asynchronous table refresh: core's JSON response, with the disabled
     * ids added so the styling stays right after an auto-refresh.
     */
    public static function showRefresh($view, array $data): void {
        $path = APP::getRootDir().'/app/views/monitoring.latest.view.refresh.php';

        if (!is_readable($path)) {
            error(_s('Cannot read original view: "%1$s".', $path));

            return;
        }

        ob_start();
        $render = function () use ($path, $data) {
            include $path;
        };
        $render->call($view);
        $json = ob_get_clean();

        $output = json_decode($json, true);

        if (!is_array($output)) {
            echo $json;

            return;
        }

        $output['uxmax_disabled_itemids'] = self::disabledItemIds($data['results']['items'] ?? []);
        echo json_encode($output);
    }

    /**
     * Core's JS view verbatim, plus the row-greying helper.
     *
     * Zabbix resolves `monitoring.latest.view.js.php` through the module's own
     * views directory first, which is how this override gets a look in at all -
     * the same mechanism on 6.0 and on 7.4.
     */
    public static function showJs($view, array $data): void {
        self::includeCore($view, $data, 'js/monitoring.latest.view.js.php', true);

        echo self::styles();
        echo self::script();
    }

    /** @return string[] */
    private static function disabledItemIds(array $items): array {
        $ids = [];

        foreach ($items as $itemid => $item) {
            if (($item['status'] ?? null) == ITEM_STATUS_DISABLED) {
                $ids[] = (string) $itemid;
            }
        }

        return $ids;
    }

    private static function styles(): string {
        return <<<'HTML'
            <style>
            [uxmax-latest-show-disabled='on'] tr.uxmax-row-disabled td,
            [uxmax-latest-show-disabled='on'] tr.uxmax-row-disabled td a,
            [uxmax-latest-show-disabled='on'] tr.uxmax-row-disabled td span {
                color: #888 !important;
                opacity: 0.65;
            }
            [uxmax-latest-show-disabled='on'] tr.uxmax-row-disabled td input[type="checkbox"] {
                opacity: 0.65;
            }
            [uxmax-latest-show-disabled='on'] .uxmax-disabled-badge {
                display: inline-block;
                width: 16px;
                height: 16px;
                padding: 0;
                background: #b5b5b5;
                color: #fff !important;
                font-weight: bold;
                font-style: italic;
                font-size: 11px;
                line-height: 16px;
                text-align: center;
                border-radius: 2px;
                opacity: 1;
                vertical-align: middle;
            }
            </style>
            HTML;
    }

    private static function script(): string {
        $badge_title = json_encode(ModuleTranslator::translate('latest.item-is-disabled'));

        return <<<JS
            <script>
            (function() {
                const ROW_CLASS = 'uxmax-row-disabled';
                const BADGE_CLASS = 'uxmax-disabled-badge';
                const BADGE_TITLE = {$badge_title};

                function isEnabled() {
                    // Checked live: the attribute is set by zbx_add_post_js, which
                    // runs AFTER this IIFE is parsed. Gating at the IIFE level
                    // would bail out before the attribute exists.
                    return document.documentElement.getAttribute('uxmax-latest-show-disabled') === 'on';
                }

                function disabledSet() {
                    const ids = window.uxmaxDisabledItemIds;
                    return Array.isArray(ids) ? new Set(ids.map(String)) : new Set();
                }

                function applyDisabledRowClass() {
                    if (!isEnabled()) {
                        return;
                    }

                    const set = disabledSet();

                    // Rows expose their itemid through the checkbox name "itemids[ID]"
                    // - unchanged from 6.0 through 7.4.
                    document.querySelectorAll('input[type="checkbox"][name^="itemids["]').forEach(function (cb) {
                        const m = cb.name.match(/^itemids\[(\d+)\]$/);
                        if (!m) return;
                        const row = cb.closest('tr');
                        if (!row) return;

                        const is_disabled = set.has(m[1]);
                        row.classList.toggle(ROW_CLASS, is_disabled);

                        const info_cell = row.querySelector('td:last-child');
                        if (!info_cell) return;

                        let badge = info_cell.querySelector('.' + BADGE_CLASS);
                        if (is_disabled && !badge) {
                            badge = document.createElement('span');
                            badge.className = BADGE_CLASS;
                            badge.textContent = 'D';
                            badge.title = BADGE_TITLE;
                            info_cell.appendChild(badge);
                        }
                        else if (!is_disabled && badge) {
                            badge.remove();
                        }
                    });
                }

                // Keep the id list in step with the auto-refresh. Latest data
                // refreshes through jQuery's \$.ajax on EVERY supported version
                // (6.0 - 7.4), so a window.fetch hook - which is what this used
                // to be - never fired anywhere: the page kept whatever ids the
                // first render happened to produce.
                if (window.jQuery) {
                    jQuery(document).ajaxSuccess(function (event, xhr, settings) {
                        if (String(settings.url || '').indexOf('latest.view.refresh') === -1) {
                            return;
                        }

                        let payload = xhr.responseJSON;
                        if (payload === undefined) {
                            try { payload = JSON.parse(xhr.responseText); } catch (e) { return; }
                        }
                        if (payload && Array.isArray(payload.uxmax_disabled_itemids)) {
                            window.uxmaxDisabledItemIds = payload.uxmax_disabled_itemids;
                        }
                    });
                }

                if (document.readyState === 'loading') {
                    document.addEventListener('DOMContentLoaded', applyDisabledRowClass);
                }
                else {
                    applyDisabledRowClass();
                }

                // Re-apply whenever the table is re-rendered (auto-refresh,
                // subfilter expansion, sort change).
                new MutationObserver(applyDisabledRowClass).observe(document.body, {
                    childList: true,
                    subtree: true
                });
            })();
            </script>
            JS;
    }
}
