<?php declare(strict_types=1);
/*
** 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\Includes;

use API;
use CSimpleButton;
use CSpan;
use CTag;

/**
 * Every place where Zabbix 6.0 - 7.4 disagree, in ONE file.
 *
 * The rule here is the one the fleet learned the hard way: branch on a
 * CAPABILITY, never on a parsed version string. A customer can upgrade the
 * frontend under an installed module, and several of these differences are not
 * monotonic anyway - NBSP() exists on 6.0, is MISSING on 6.2 and is back from
 * 6.4, so no `>=` test describes the range.
 *
 * Probed in the official frontend images (6.0, 6.2, 6.4, 7.0, 7.2, 7.4):
 *
 *   CHtmlPage            6.4+   (6.0/6.2 use the page-level CWidget)
 *   CTemplateTag         6.4+   (it is a <template> tag and nothing else)
 *   CCsrfTokenHelper     6.4+   (6.0/6.2 guard requests with a `sid` field)
 *   CSRF_TOKEN_NAME      7.0+   as a bare global; 6.4 has the class constant
 *   CButtonLink          7.0+   (= CSimpleButton + ZBX_STYLE_BTN_LINK)
 *   NBSP()               6.0, 6.4+   - missing on 6.2 only
 *   item.get name_resolved  7.0+  (6.x call the field `name`)
 *   data-hintbox-contents   7.0+  (6.x nest a <div class="hint-box"> instead)
 *   disableCsrfValidation() 6.4+  (6.0/6.2: disableSIDvalidation())
 */
final class Compat {

    /** @var array<string,bool> */
    private static $probes = [];

    private static function probe(string $key, callable $test): bool {
        if (!array_key_exists($key, self::$probes)) {
            self::$probes[$key] = (bool) $test();
        }

        return self::$probes[$key];
    }

    /* ---------------------------------------------------------------- page */

    /**
     * The page wrapper. 6.4 renamed CWidget (the PAGE class) to CHtmlPage and
     * freed the old name for the dashboard widget base, so the two names mean
     * completely different things depending on the frontend - which is exactly
     * why nothing outside this file may name either of them.
     *
     * Both expose setTitle()/addItem()/show(), so callers need no branch.
     *
     * @return \CHtmlPage|\CWidget
     */
    public static function page(string $title) {
        $class = class_exists('CHtmlPage') ? 'CHtmlPage' : 'CWidget';

        return (new $class())->setTitle($title);
    }

    /* ------------------------------------------------------------- controls */

    /**
     * The borderless "Add" / "Remove" link-button used inside dynamic-row
     * tables. CButtonLink arrived in 7.0; before it, core wrote out the same
     * thing by hand, so the fallback is not an approximation - it is the very
     * markup 6.0-6.4 core uses for its own Add links.
     */
    public static function buttonLink(string $caption): CSimpleButton {
        if (class_exists('CButtonLink')) {
            return new \CButtonLink($caption);
        }

        return (new CSimpleButton($caption))->addClass(ZBX_STYLE_BTN_LINK);
    }

    /**
     * A <template> element holding row markup or a JSON payload the client
     * reads back with .innerHTML. CTemplateTag is 6.4+ and does nothing beyond
     * `new CTag('template', true)` + setId + addItem, so the fallback renders
     * byte-identical markup on 6.0/6.2.
     *
     * @param string|array|object $value
     */
    public static function templateTag(string $id, $value): CTag {
        if (class_exists('CTemplateTag')) {
            return new \CTemplateTag($id, $value);
        }

        return (new CTag('template', true, $value))->setId($id);
    }

    /**
     * A non-breaking space. NBSP() is one of the backport holes: present on
     * 6.0, absent on 6.2, back from 6.4. Calling it unguarded is a fatal on
     * exactly one version in the middle of the range.
     *
     * Deliberately untyped: NBSP() returns a CHtmlEntity, and pinning the
     * return type to the fallback's class turns a working frontend into a
     * TypeError - which is exactly how this was first shipped.
     *
     * @return \CHtmlEntity|CSpan
     */
    public static function nbsp() {
        if (function_exists('NBSP')) {
            return NBSP();
        }

        return new CSpan("\u{00A0}");
    }

    /* ----------------------------------------------------- request guarding */

    /**
     * Form fields that satisfy the frontend's own request guard.
     *
     * 6.0/6.2 use a `sid` field, which CForm adds to every form it builds, so
     * there is nothing for the caller to add. 6.4 introduced the CSRF token
     * under a class constant and 7.0 promoted it to a global one.
     *
     * @return array<string,string>  field name => value
     */
    public static function csrfVars(string $action): array {
        if (!class_exists('CCsrfTokenHelper')) {
            return [];
        }

        return [\CCsrfTokenHelper::CSRF_TOKEN_NAME => \CCsrfTokenHelper::get($action)];
    }

    /* ------------------------------------------------------------------ API */

    /**
     * The item.get field carrying the resolved item name. 7.0 split `name`
     * (raw, with macros) from `name_resolved`; asking a 6.x API for
     * name_resolved is a hard API error, and the Latest data page then renders
     * nothing at all.
     */
    public static function itemNameField(): string {
        return self::probe('name_resolved', static function () {
            return class_exists('CItemHelper');
        }) ? 'name_resolved' : 'name';
    }

    /**
     * Does core's own Latest data controller return an `items_rw` set?
     * It arrived in 6.2; on 6.0 the view never looks for it. Adding the key
     * there is harmless, so this only decides whether the RIGHTS query - and
     * the CRoleHelper constant it needs, which 6.0 also lacks - runs at all.
     */
    public static function latestHasItemsRw(): bool {
        return self::probe('items_rw', static function () {
            return defined('CRoleHelper::ACTIONS_INVOKE_EXECUTE_NOW');
        });
    }

    /* -------------------------------------------------------------- styling */

    /**
     * Does this frontend put the tag text into a `data-hintbox-contents`
     * attribute? 7.0 changed CTag::setHint() from nesting a
     * <div class="hint-box"> to setting that attribute - and the colour-tag
     * rules are pure CSS attribute selectors, so on 6.0-6.4 they match nothing
     * and the feature is a silent no-op.
     *
     * Probed by RENDERING a hinted tag and looking at the result rather than by
     * naming a version: it is the rendered attribute the stylesheet depends on.
     */
    public static function tagsCarryHintAttribute(): bool {
        return self::probe('hintbox_attr', static function () {
            return strpos((new CSpan('x'))->setHint('x')->toString(), 'data-hintbox-contents') !== false;
        });
    }

    /**
     * Class name of a dashboard widget's header element. Renamed in 7.0
     * (`-head` up to 6.4, `-header` from 7.0), which is why the stylesheet
     * lists BOTH: the rules are inert otherwise, and an inert rule looks
     * exactly like a feature the customer forgot to switch on.
     *
     * @return string[]
     */
    public static function widgetHeaderClasses(): array {
        return ['dashboard-grid-widget-head', 'dashboard-grid-widget-header'];
    }

    /* --------------------------------------------------------------- assets */

    /**
     * Absolute path of a static asset inside the installed package.
     *
     * The two trees keep static files in different places - `assets/` in the
     * 6.4+ package, `public/` in the 6.0/6.2 one, because a v1 frontend serves
     * no module assets on its own and build-edition.py relocates them. Code
     * that reads a file off disk (the font thumbnail) must not hard-code
     * either, or it works on one line and silently produces nothing on the
     * other.
     *
     * @param string $relative  e.g. "img/thumbnail.png"
     */
    public static function assetPath(string $relative): string {
        $root = dirname(__DIR__);

        foreach (['assets', 'public'] as $dir) {
            $candidate = $root.'/'.$dir.'/'.$relative;

            if (is_file($candidate)) {
                return $candidate;
            }
        }

        return $root.'/assets/'.$relative;
    }

    /* ---------------------------------------------------------------- module */

    /**
     * Where the module is installed, relative to the frontend root.
     *
     * The v2 module API answers this itself; the v1 one (6.0/6.2) has no
     * getRelativePath() at all and roots the module manager at the modules
     * directory, so the path has to be reconstructed from the module's own
     * directory.
     *
     * @param \Core\CModule|\Zabbix\Core\CModule $module
     */
    public static function relativePath($module): string {
        if (method_exists($module, 'getRelativePath')) {
            return $module->getRelativePath();
        }

        return 'modules/'.basename($module->getDir());
    }

    /**
     * Persist a module's configuration.
     *
     * setConfig() is v2-only. On 6.0/6.2 the same store is reachable through
     * the module API, which is what the frontend's own module screen uses.
     *
     * @param \Core\CModule|\Zabbix\Core\CModule $module
     */
    public static function setModuleConfig($module, array $config): void {
        if (method_exists($module, 'setConfig')) {
            $module->setConfig($config);

            return;
        }

        $relative_path = basename($module->getDir());
        $db_modules = API::Module()->get([
            'output' => ['moduleid'],
            'filter' => ['relative_path' => $relative_path]
        ]);

        if (!$db_modules) {
            return;
        }

        API::Module()->update([
            'moduleid' => $db_modules[0]['moduleid'],
            'config' => $config
        ]);
    }
}
