<?php

// SPDX-FileCopyrightText: 2018 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

namespace Icinga\Module\Graphite\Web\Widget;

use Icinga\Application\Config;
use Icinga\Application\Icinga;
use Icinga\Authentication\Auth;
use Icinga\Exception\ConfigurationError;
use Icinga\Module\Graphite\Graphing\Chart;
use Icinga\Module\Graphite\Graphing\GraphingTrait;
use Icinga\Module\Graphite\Graphing\Template;
use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT;
use Icinga\Module\Graphite\Util\TimeRangePickerTools;
use Icinga\Module\Graphite\Web\Widget\Graphs\Host as HostGraphs;
use Icinga\Module\Graphite\Web\Widget\Graphs\Service as ServiceGraphs;
use Icinga\Module\Monitoring\Object\Host;
use Icinga\Module\Monitoring\Object\MonitoredObject;
use Icinga\Module\Monitoring\Object\Service;
use Icinga\Web\Request;
use Icinga\Web\Url;
use Icinga\Web\View;
use Icinga\Web\Widget\AbstractWidget;
use ipl\Orm\Model;
use Icinga\Module\Icingadb\Model\Host as IcingadbHost;
use Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb\IcingadbHost as IcingadbHostGraphs;
use Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb\IcingadbService as IcingadbServiceGraphs;

abstract class Graphs extends AbstractWidget
{
    use GraphingTrait;

    /**
     * The Icinga custom variable with the "real" check command (if any) of objects we display graphs for
     *
     * @var string
     */
    protected static $obscuredCheckCommandCustomVar;

    /**
     * The type of the object to render the graphs for
     *
     * @var string
     */
    protected $objectType;

    /**
     * The object to render the graphs for
     *
     * @var MonitoredObject|Model
     */
    protected $object;

    /**
     * Graph image width
     *
     * @var string
     */
    protected $width = '350';

    /**
     * Graph image height
     *
     * @var string
     */
    protected $height = '200';

    /**
     * Graph range start
     *
     * @var string
     */
    protected $start;

    /**
     * Graph range end
     *
     * @var string
     */
    protected $end;

    /**
     * Whether to render as compact as possible
     *
     * @var bool
     */
    protected $compact = false;

    /**
     * The check command of the monitored object we display graphs for
     *
     * @var string
     */
    protected $checkCommand;

    /**
     * The "real" check command (if any) of the monitored object we display graphs for
     *
     * E.g. the command executed remotely via check_by_ssh
     *
     * @var string|null
     */
    protected $obscuredCheckCommand;

    /**
     * Additional CSS classes for the <div/>s around the images
     *
     * @var string[]
     */
    protected $classes = [];

    /**
     * Whether to serve a transparent dummy image first and let the JS code load the actual graph
     *
     * @var bool
     */
    protected $preloadDummy = false;

    /**
     * Whether to render the graphs inline
     *
     * @var bool
     */
    protected $renderInline;

    /**
     * Whether to explicitly display that no graphs were found
     *
     * @var bool|null
     */
    protected $showNoGraphsFound;

    /**
     * Factory, based on the given monitoring object
     *
     * @param   MonitoredObject $object
     *
     * @return  ?static
     */
    public static function forMonitoredObject(MonitoredObject $object)
    {
        switch ($object->getType()) {
            case 'host':
                /** @var Host $object */
                return new HostGraphs($object);

            case 'service':
                /** @var Service $object */
                return new ServiceGraphs($object);
        }
    }

    /**
     * Factory, based on the given icingadb object
     *
     * @param Model $object
     *
     * @return static
     */
    public static function forIcingadbObject(Model $object)
    {
        if ($object instanceof IcingadbHost) {
            return new IcingadbHostGraphs($object);
        }

        return new IcingadbServiceGraphs($object);
    }

    /**
     * Get the Icinga custom variable with the "real" check command (if any) of monitored objects we display graphs for
     *
     * @return string
     */
    public static function getObscuredCheckCommandCustomVar()
    {
        if (static::$obscuredCheckCommandCustomVar === null) {
            static::$obscuredCheckCommandCustomVar = Config::module('graphite')
                ->get('icinga', 'customvar_obscured_check_command', 'check_command');
        }

        return static::$obscuredCheckCommandCustomVar;
    }

    /**
     * Constructor
     *
     * @param MonitoredObject|Model $object    The object to render the graphs for
     */
    public function __construct($object)
    {
        $this->object = $object;
        $this->renderInline = Url::fromRequest()->getParam('format') === 'pdf';

        if ($object instanceof Model) {
            $this->checkCommand = $object->checkcommand_name;
            $this->obscuredCheckCommand = $object->vars[Graphs::getObscuredCheckCommandCustomVar()] ?? null;
        } else {
            $this->checkCommand = $object->{"{$this->objectType}_check_command"};
            $this->obscuredCheckCommand = $object->{
                "_{$this->objectType}_" . Graphs::getObscuredCheckCommandCustomVar()
            };
        }
    }

    /**
     * Process the given request using this widget
     *
     * @param   ?Request $request    The request to be processed
     *
     * @return  $this
     */
    public function handleRequest(?Request $request = null)
    {
        if ($request === null) {
            $request = Icinga::app()->getRequest();
        }

        $params = $request->getUrl()->getParams();
        list($this->start, $this->end) = $this->getRangeFromTimeRangePicker($request);
        $this->width  = $params->shift('width', $this->width);
        $this->height = $params->shift('height', $this->height);

        return $this;
    }

    /**
     * Render the graphs list
     *
     * @return string
     */
    protected function getGraphsList()
    {
        $result = []; // kind of string builder
        $imageBaseUrl = $this->getImageBaseUrl();
        $allTemplates = $this->getAllTemplates();
        $actualCheckCommand = $this->obscuredCheckCommand === null ? $this->checkCommand : $this->obscuredCheckCommand;
        $concreteTemplates = $allTemplates->getTemplates($actualCheckCommand);

        $excludedMetrics = [];

        foreach ($concreteTemplates as $concreteTemplate) {
            foreach ($concreteTemplate->getCurves() as $curve) {
                $excludedMetrics[] = $curve[0];
            }
        }

        IPT::recordf("Icinga check command: %s", $this->checkCommand);
        IPT::recordf("Obscured check command: %s", $this->obscuredCheckCommand);

        foreach (
            [
            ['template', $concreteTemplates, []],
            ['default_template', $allTemplates->getDefaultTemplates(), $excludedMetrics],
            ] as $templateSet
        ) {
            list($urlParam, $templates, $excludeMetrics) = $templateSet;

            if ($urlParam === 'template') {
                IPT::recordf('Applying templates for check command %s', $actualCheckCommand);
            } else {
                IPT::recordf('Applying default templates, excluding previously used metrics');
            }

            IPT::indent();

            foreach ($templates as $templateName => $template) {
                if ($this->designedForObjectType($template)) {
                    IPT::recordf('Applying template %s', $templateName);
                    IPT::indent();

                    $charts = $template->getCharts(
                        static::getMetricsDataSource(),
                        $this->object,
                        [],
                        $excludeMetrics
                    );

                    if (! empty($charts)) {
                        $currentGraphs = [];

                        foreach ($charts as $chart) {
                            /** @var Chart $chart */

                            $metricVariables = $chart->getMetricVariables();
                            $bestIntersect = -1;
                            $bestPos = count($result);

                            foreach ($result as $graphPos => & $graph) {
                                $currentIntersect = count(array_intersect_assoc($graph[1], $metricVariables));

                                if ($currentIntersect >= $bestIntersect) {
                                    $bestIntersect = $currentIntersect;
                                    $bestPos = $graphPos + 1;
                                }
                            }
                            unset($graph);

                            $urlParams = $template->getUrlParams();
                            if (array_key_exists("height", $urlParams)) {
                                $actheight = $urlParams["height"]->resolve(['height']);
                                if ($actheight < $this->height) {
                                    $actheight = $this->height;
                                }
                            } else {
                                $actheight = $this->height;
                            }
                            $actwidth = $this->width;
                            $explicitWidth = false;
                            if (array_key_exists("width", $urlParams)) {
                                $actwidth = $urlParams["width"]->resolve(['width']);
                                $explicitWidth = $actwidth;
                            }

                            if ($this->renderInline) {
                                $chart->setFrom($this->start)
                                    ->setUntil($this->end)
                                    ->setWidth((int) $actwidth)
                                    ->setHeight((int) $actheight)
                                    ->setBackgroundColor('white')
                                    ->setForegroundColor('black')
                                    ->setMajorGridLineColor('grey')
                                    ->setMinorGridLineColor('white')
                                    ->setShowLegend(! $this->compact);

                                $img = new InlineGraphImage($chart);
                            } else {
                                $imageUrl = $this->filterImageUrl($imageBaseUrl->with($metricVariables))
                                    ->setParam($urlParam, $templateName)
                                    ->setParam('start', $this->start)
                                    ->setParam('end', $this->end)
                                    ->setParam('width', $actwidth)
                                    ->setParam('height', $actheight);

                                if (! $this->compact) {
                                    $imageUrl->setParam('legend', '1');
                                }

                                if ($this->preloadDummy) {
                                    $src = 'data:image/png;base64,' // 1x1 dummy
                                        . 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRS'
                                        . 'TlMAQObYZgAAAApJREFUeAFjZAAAAAQAAhq+CAMAAAAASUVORK5CYII=';
                                } else {
                                    $src = $imageUrl;
                                }

                                $img = '<img id="graphiteImg-' . md5((string) $imageUrl) . '"'
                                    . " src=\"$src\" data-actualimageurl=\"$imageUrl\" class=\"detach graphiteImg\""
                                    . " alt=\"\" width=\"$actwidth\" height=\"$actheight\"";
                                if ($explicitWidth !== false) {
                                    $img .= " data-width=\"$explicitWidth\"";
                                }

                                $img .= '>';
                            }

                            $currentGraphs[] = [$img, $metricVariables, $bestPos];
                        }

                        foreach (array_reverse($currentGraphs) as $graph) {
                            list($img, $metricVariables, $bestPos) = $graph;
                            array_splice($result, $bestPos, 0, [[$img, $metricVariables]]);
                        }
                    }

                    IPT::unindent();
                } else {
                    IPT::recordf('Not applying template %s', $templateName);
                }
            }

            IPT::unindent();
        }

        if (! empty($result)) {
            foreach ($result as & $graph) {
                $graph = $graph[0];
            }
            unset($graph);

            $currentUrl = Icinga::app()->getRequest()->getUrl();
            $limit = (int) $currentUrl->getParam('graphs_limit', 2);
            $total = count($result);

            if ($limit < 1) {
                $limit = -1;
            }

            if ($limit !== -1 && $total > $limit) {
                $result = array_slice($result, 0, $limit);

                if (! $this->compact) {
                    /** @var View $view */
                    $view = $this->view();

                    $url = $this->getGraphsListBaseUrl();
                    TimeRangePickerTools::copyAllRangeParameters($url->getParams(), $currentUrl->getParams());

                    $result[] = "<p class='load-more'>{$view->qlink(
                        sprintf($view->translate('Load all %d graphs'), $total),
                        $url->setParam('graphs_limit', '-1'),
                        null,
                        ['class' => 'action-link']
                    )}</p>";
                }
            }

            $classes = $this->classes;
            $classes[] = 'images';

            array_unshift($result, '<div class="' . implode(' ', $classes) . '">');
            $result[] = '</div>';
        }

        if ($this->renderInline) {
            foreach ($result as $html) {
                if ($html instanceof InlineGraphImage) {
                    // Errors should occur now or not at all
                    $html->render();
                }
            }
        }

        return implode($result);
    }

    public function render()
    {
        IPT::clear();

        try {
            $result = $this->getGraphsList();
        } catch (ConfigurationError $e) {
            $view = $this->view();

            return "<p>{$view->escape($e->getMessage())}</p>"
                . '<p>' . vsprintf(
                    $view->escape($view->translate('Please %scorrect%s the configuration of the Graphite module.')),
                    Auth::getInstance()->hasPermission('config/modules')
                        ? explode(
                            '$LINK_TEXT$',
                            $view->qlink('$LINK_TEXT$', 'graphite/config/backend', null, ['class' => 'action-link'])
                        )
                        : ['', '']
                ) . '</p>';
        }

        $view = $this->view();

        if ($result === '' && $this->getShowNoGraphsFound()) {
            $result = "<p>{$view->escape($view->translate('No graphs found'))}</p>";
        }

        if (IPT::enabled()) {
            $result .= "<h3>{$view->escape($view->translate('Graphs assembling process record'))}</h3>"
                . "<pre>{$view->escape(IPT::dump())}</pre>";
        }

        return $result;
    }

    /**
     * Get time range parameters for Graphite from the URL
     *
     * @param   Request $request    The request to be used
     *
     * @return  string[]
     */
    protected function getRangeFromTimeRangePicker(Request $request)
    {
        $params = $request->getUrl()->getParams();
        $relative = $params->get(TimeRangePickerTools::getRelativeRangeParameter());
        if ($relative !== null) {
            return ["-$relative", null];
        }

        $absolute = TimeRangePickerTools::getAbsoluteRangeParameters();
        $start = $params->get($absolute['start']);
        return [
            $start === null ? -TimeRangePickerTools::getDefaultRelativeTimeRange() : $start,
            $params->get($absolute['end'])
        ];
    }

    /**
     * Return a identifier specifying the monitored object we display graphs for
     *
     * @return  string
     */
    abstract protected function getMonitoredObjectIdentifier();

    /**
     * Get the base URL to a graph specifying just the monitored object kind
     *
     * @return Url
     */
    abstract protected function getImageBaseUrl();

    /**
     * Get the base URL to the monitored object's graphs list
     *
     * @return Url
     */
    abstract protected function getGraphsListBaseUrl();

    /**
     * Extend the {@link getImageBaseUrl()}'s result's parameters with the concrete monitored object
     *
     * @param   Url $url    The URL to extend
     *
     * @return  Url         The given URL
     */
    abstract protected function filterImageUrl(Url $url);

    /**
     * Return whether the given template is designed for the type of the object we display graphs for
     *
     * @param   Template    $template
     *
     * @return  bool
     */
    abstract protected function designedForObjectType(Template $template);

    /**
     * Get {@link compact}
     *
     * @return bool
     */
    public function getCompact()
    {
        return $this->compact;
    }

    /**
     * Set {@link compact}
     *
     * @param bool $compact
     *
     * @return $this
     */
    public function setCompact($compact = true)
    {
        $this->compact = $compact;
        return $this;
    }

    /**
     * Get the graph image width
     *
     * @return string
     */
    public function getWidth()
    {
        return $this->width;
    }

    /**
     * Set the graph image width
     *
     * @param string $width
     *
     * @return $this
     */
    public function setWidth($width)
    {
        $this->width = $width;

        return $this;
    }

    /**
     * Get the graph image height
     *
     * @return string
     */
    public function getHeight()
    {
        return $this->height;
    }

    /**
     * Set the graph image height
     *
     * @param string $height
     *
     * @return $this
     */
    public function setHeight($height)
    {
        $this->height = $height;

        return $this;
    }

    /**
     * Get additional CSS classes for the <div/>s around the images
     *
     * @return string[]
     */
    public function getClasses()
    {
        return $this->classes;
    }

    /**
     * Set additional CSS classes for the <div/>s around the images
     *
     * @param string[] $classes
     *
     * @return $this
     */
    public function setClasses($classes)
    {
        $this->classes = $classes;

        return $this;
    }

    /**
     * Get whether to serve a transparent dummy image first and let the JS code load the actual graph
     *
     * @return bool
     */
    public function getPreloadDummy()
    {
        return $this->preloadDummy;
    }

    /**
     * Set whether to serve a transparent dummy image first and let the JS code load the actual graph
     *
     * @param bool $preloadDummy
     *
     * @return $this
     */
    public function setPreloadDummy($preloadDummy = true)
    {
        $this->preloadDummy = $preloadDummy;

        return $this;
    }

    /**
     * Get whether to render the graphs inline
     *
     * @return bool
     */
    public function getRenderInline()
    {
        return $this->renderInline;
    }

    /**
     * Set whether to render the graphs inline
     *
     * @param bool $renderInline
     *
     * @return $this
     */
    public function setRenderInline($renderInline = true)
    {
        $this->renderInline = $renderInline;

        return $this;
    }

    /**
     * Get whether to explicitly display that no graphs were found
     *
     * @return bool
     */
    public function getShowNoGraphsFound()
    {
        if ($this->showNoGraphsFound === null) {
            $this->showNoGraphsFound = ! Config::module('graphite')->get('ui', 'disable_no_graphs_found');
        }

        return $this->showNoGraphsFound;
    }

    /**
     * Set whether to explicitly display that no graphs were found
     *
     * @param bool $showNoGraphsFound
     *
     * @return $this
     */
    public function setShowNoGraphsFound($showNoGraphsFound = true)
    {
        $this->showNoGraphsFound = $showNoGraphsFound;

        return $this;
    }
}
