<?php

/*****************************************************************************
 *
 * GlobalBackendmklivestatus.php
 *
 * Backend class for handling object and state information using the
 * livestatus NEB module. For mor information about CheckMK's Livestatus
 * Module please visit: https://docs.checkmk.com/latest/en/livestatus.html
 *
 * Copyright (c) 2010 NagVis Project  (Contact: info@nagvis.org),
 *                    Mathias Kettner (Contact: mk@mathias-kettner.de)
 *
 * License:
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *****************************************************************************/

/**
 * @author  Mathias Kettner <mk@mathias-kettner.de>
 * @author  Lars Michelsen  <lm@larsmichelsen.com>
 *
 * For mor information about CheckMK's Livestatus Module
 * please visit: https://docs.checkmk.com/latest/en/livestatus.html
 */
class GlobalBackendmklivestatus implements GlobalBackendInterface
{
    /** @var string */
    private $backendId = '';

    /** @var string */
    private $CONNECT_ERR = "";

    /** @var Exception|null */
    private $CONNECT_EXC = null;

    /** @var false|resource|null  */
    private $SOCKET = null;

    /** @var string */
    private $socketType = '';

    /** @var string */
    private $socketPath = '';

    /** @var string */
    private $socketAddress = '';

    /** @var int */
    private $socketPort = 0;

    /** @var string */
    private $socketSpec = '';

    /** @var array These are the backend local configuration options */
    private static $validConfig = [
        'socket' => [
            'must' => 1,
            'editable' => 1,
            'default' => 'unix:/usr/local/nagios/var/rw/live',
            'match' => MATCH_SOCKET,
        ],
        'verify_tls_peer' => [
            'must' => 0,
            'editable' => 1,
            'default' => 1,
            'match' => MATCH_BOOLEAN,
            'field_type' => 'boolean',
        ],
        'verify_tls_ca_path' => [
            'must' => 0,
            'editable' => 1,
            'default' => '',
            'match' => MATCH_STRING_PATH,
        ],
        'timeout' => [
            'must' => 1,
            'editable' => 1,
            'default' => 5,
            'match' => MATCH_INTEGER,
        ],
    ];

    /**
     * PUBLIC class constructor
     *
     * @param string $backendId ID if the backend
     * @throws BackendConnectionProblem
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     */
    public function __construct($backendId)
    {
        $this->backendId = $backendId;

        // Parse the socket params
        $this->socketSpec = cfg('backend_' . $backendId, 'socket');
        $this->parseSocket($this->socketSpec);

        // Run preflight checks
        if ($this->socketType == 'unix' && !$this->checkSocketExists()) {
            throw new BackendConnectionProblem(
                l(
                    'Unable to connect to livestatus socket. The socket [SOCKET] in backend [BACKENDID] does not exist. Maybe Nagios is not running or restarting.',
                    ['BACKENDID' => $this->backendId, 'SOCKET' => $this->socketPath]
                )
            );
        }

        if (!function_exists('fsockopen')) {
            throw new BackendConnectionProblem(
                l(
                    'The PHP function fsockopen is not available. Needed by backend [BACKENDID].',
                    ['BACKENDID' => $this->backendId, 'SOCKET' => $this->socketSpec]
                )
            );
        }

        return true;
    }

    /**
     * The descrutcor closes the socket when some is open
     * at the moment when the class is destroyed. It is
     * important to close the socket in a clean way.
     *
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    public function __destruct()
    {
        if ($this->SOCKET !== null) {
            fclose($this->SOCKET);
            $this->SOCKET = null;
        }
    }

    /**
     * Parses and sets the socket options
     *
     * @param string $socket
     * @throws BackendConnectionProblem
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    private function parseSocket($socket)
    {
        // Explode the given socket definition
        list($type, $address) = explode(':', $socket, 2);

        if ($type === 'unix') {
            $this->socketType = $type;
            $this->socketPath = $address;
        } elseif ($type === 'tcp' || $type === 'tcp-tls') {
            $this->socketType = $type;

            // Extract address and port
            list($address, $port) = explode(':', $address, 2);

            $this->socketAddress = $address;
            $this->socketPort = $port;

        } else {
            throw new BackendConnectionProblem(
                l('Unknown socket type given in backend [BACKENDID]', ['BACKENDID' => $this->backendId])
            );
        }
    }

    /**
     * Returns the valid config for this backend
     *
     * @return	array
     * @author	Lars Michelsen <lm@larsmichelsen.com>
     */
    public static function getValidConfig()
    {
        return self::$validConfig;
    }

    /**
     * Checks if the socket exists
     *
     * @return  bool
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    private function checkSocketExists()
    {
        return file_exists($this->socketPath);
    }

    /**
     * Connects to the livestatus socket when no connection is open
     *
     * @throws Exception
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    private function connectSocket()
    {
        // Only try to connect once per page. Re-raise the connection exception on
        // later tries to connect
        if ($this->CONNECT_EXC != null) {
            throw $this->CONNECT_EXC;
        }

        set_error_handler([$this, 'connectErrorHandler'], E_WARNING | E_NOTICE);

        // Connect to the socket
        // don't want to see the connection error messages - want to handle the
        // errors later with an own error message
        // FIXME: Maybe use pfsockopen in the future to use persistent connections
        if ($this->socketType === 'unix') {
            $this->SOCKET = fsockopen(
                'unix://' . $this->socketPath,
                -1,
                $errno,
                $errstr,
                (float) cfg('backend_' . $this->backendId, 'timeout')
            );

        } elseif ($this->socketType === 'tcp-tls') {
            if (cfg('backend_' . $this->backendId, 'verify_tls_peer')) {
                $ssl_options = [
                    'verify_peer' => true,
                    'verify_peer_name' => false,
                    'verify_depth' => 1,
                ];

                $ca_path = cfg('backend_' . $this->backendId, 'verify_tls_ca_path');
                if ($ca_path) {
                    $ssl_options['cafile'] = $ca_path;
                }
                $context = stream_context_create([
                    'ssl' => $ssl_options
                ]);
            } else {
                $context = stream_context_create([
                    'ssl' => [
                        'verify_peer' => false,
                        'verify_peer_name' => false
                    ]
                ]);
            }

            $this->SOCKET = stream_socket_client(
                "tls://" . $this->socketAddress . ":" . $this->socketPort,
                $errno,
                $errstr,
                (float) cfg('backend_' . $this->backendId, 'timeout'),
                STREAM_CLIENT_CONNECT,
                $context
            );

        } elseif ($this->socketType === 'tcp') {
            $this->SOCKET = fsockopen(
                $this->socketAddress,
                $this->socketPort,
                $errno,
                $errstr,
                (float) cfg('backend_' . $this->backendId, 'timeout')
            );
        }

        restore_error_handler();

        if (!$this->SOCKET) {
            if ($errno === 0) {
                $error_msg = $this->CONNECT_ERR;
            } else {
                $error_msg = $errstr;
            }

            $this->SOCKET = null;
            $this->CONNECT_EXC = new BackendConnectionProblem(l(
                'Unable to connect to the [SOCKET] in backend [BACKENDID]: [MSG]',
                [
                    'BACKENDID' => $this->backendId,
                    'SOCKET' => $this->socketSpec,
                    'MSG' => $error_msg
                ]
            ));
            throw $this->CONNECT_EXC;
        }
    }

    /**
     * Catch PHP errors occured during connect
     *
     * @param int $errno
     * @param string $errstr
     * @return bool
     */
    public function connectErrorHandler($errno, $errstr)
    {
        if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) {
            return false; // use default error handler
        }
        $this->CONNECT_ERR .= $errstr . "\n";
        return true;
    }

    /*private function verifyLivestatusVersion() {
        $result = $this->queryLivestatusSingleColumn("GET status\nColumns: livestatus_version\n");
        $result[0] = '1.1.7rc1';
        $version = str_replace('.', '0',        $result[0]);
        $version = str_replace('i',  '0',       $version);
        $version = str_replace('b',  '1',       $version);
        $version = (int) str_replace('rc', '2', $version);
        if ($version < 1010903) {
            throw new BackendConnectionProblem(
               l('The livestatus version [VERSION] used in backend [BACKENDID] is too old. Please update.',
                 Array('BACKENDID' => $this->backendId, 'VERSION' => $result[0])));
        }
    }*/

    /**
     * PRIVATE queryLivestatus()
     *
     * Queries the livestatus socket and returns the result as array
     *
     * @param string $query Query to send to the socket
     * @param bool $response
     * @return  array|void    Results of the query
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @throws Exception
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    private function queryLivestatus($query, $response = true)
    {
        // Only connect when no connection opened yet
        if ($this->SOCKET === null) {
            $this->connectSocket();

            // Check if the livestatus version is OK
            //$this->verifyLivestatusVersion();
        }

        //$fh = fopen('/tmp/live', 'a');
        //fwrite($fh, $query."\n\n");
        //fclose($fh);

        //Add authorization data to mk livestatus query
        if (cfg('global', 'only_permitted_objects')) {
            global $AUTH;
            $userName = $AUTH->getUser();
            $query .= "AuthUser: $userName\n";
        }

        // Query to get a json formated array back
        // Use KeepAlive with fixed16 header
        if ($response) {
            $query .= "OutputFormat: json\nKeepAlive: on\nResponseHeader: fixed16\n\n";
        }
        // Disable regular error reporting to suppress php error messages
        $oldLevel = error_reporting(0);
        $write = fwrite($this->SOCKET, $query);
        error_reporting($oldLevel);

        if ($write === false) {
            throw new BackendConnectionProblem(
                l(
                    'Problem while writing to socket [SOCKET] in backend [BACKENDID]: [MSG]',
                    [
                        'BACKENDID' => $this->backendId,
                        'SOCKET' => $this->socketSpec,
                        'MSG' => 'Error while sending query to socket.'
                    ]
                )
            );
        }

        if ($write !== strlen($query)) {
            throw new BackendConnectionProblem(l(
                'Problem while writing to socket [SOCKET] in backend [BACKENDID]: [MSG]',
                [
                    'BACKENDID' => $this->backendId,
                    'SOCKET' => $this->socketSpec,
                    'MSG' => 'Connection terminated.'
                ]
            ));
        }

        // Return here if no answer is expected
        if (!$response) {
            return;
        }

        // Read 16 bytes to get the status code and body size
        $read = $this->readSocket(16);

        // Catch problem while reading
        if ($read === false) {
            throw new BackendConnectionProblem(l(
                'Problem while reading from socket [SOCKET] in backend [BACKENDID]: [MSG]',
                [
                    'BACKENDID' => $this->backendId,
                    'SOCKET' => $this->socketSpec,
                    'MSG' => 'Error while reading socket (header)'
                ]
            ));
        }

        // Extract status code
        $status = substr($read, 0, 3);

        // Extract content length
        $len = intval(trim(substr($read, 4, 11)));

        // Read socket until end of data
        $read = $this->readSocket($len);

        // Catch problem while reading
        if ($read === false) {
            throw new BackendConnectionProblem(l(
                'Problem while reading from socket [SOCKET] in backend [BACKENDID]: [MSG]',
                [
                    'BACKENDID' => $this->backendId,
                    'SOCKET' => $this->socketSpec,
                    'MSG' => 'Error while reading socket (content)'
                ]
            ));
        }

        // Catch errors (Like HTTP 200 is OK)
        if ($status != "200") {
            throw new BackendConnectionProblem(l(
                'Problem while reading from socket [SOCKET] in backend [BACKENDID]: [MSG]',
                [
                    'BACKENDID' => $this->backendId,
                    'SOCKET' => $this->socketSpec,
                    'MSG' => $read
                ]
            ));
        }

        //$fh = fopen('/tmp/live', 'a');
        //fwrite($fh, $read."\n\n");
        //fclose($fh);

        // Decode the json response
        $obj = json_decode(iso8859_1_to_utf8($read));

        // TEST: Disable KeepAlive:
        //fclose($this->SOCKET);
        //$this->SOCKET = null;

        // json_decode returns null on syntax problems
        if ($obj === null) {
            throw new BackendInvalidResponse(l(
                'The response has an invalid format in backend [BACKENDID].',
                ['BACKENDID' => $this->backendId]
            ));
        } else {
            // Return the response object
            return $obj;
        }
    }

    /**
     * Method for reading a fixed amount of bytest from the socket
     *
     * @param   int $len Number of bytes to read
     * @return  string   The read bytes
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    private function readSocket($len)
    {
        $offset = 0;
        $socketData = '';

        while ($offset < $len) {
            if (($data = @fread($this->SOCKET, $len - $offset)) === false) {
                return false;
            }

            if (($dataLen = strlen($data)) === 0) {
                break;
            }

            $offset += $dataLen;
            $socketData .= $data;
        }

        return $socketData;
    }

    /**
     * PRIVATE queryLivestatusSingleRow()
     *
     * Queries the livestatus socket for a single row
     *
     * @param string $query Query to send to the socket
     * @return  array    Results of the query
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    private function queryLivestatusSingleRow($query)
    {
        $l = $this->queryLivestatus($query);
        if (isset($l[0])) {
            return $l[0];
        } else {
            return [];
        }
    }

    /**
     * Queries the livestatus socket for a single column in several rows
     *
     * @param string $query Query to send to the socket
     * @return  array    Results of the query
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    private function queryLivestatusSingleColumn($query)
    {
        $l = $this->queryLivestatus($query);

        $result = [];
        foreach ($l as $line) {
            $result[] = $line[0];
        }

        return $result;
    }

    /**
     * Queries the livestatus socket for a list of objects
     *
     * @param string $query Query to send to the socket
     * @return  array    Results of the query
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    private function queryLivestatusList($query)
    {
        $l = $this->queryLivestatus($query);

        $result = [];
        foreach ($l as $line) {
            $result = array_merge($result, $line[0]);
        }

        return $result;
    }

    /**
     * This is a special method which is currently unused within NagVis.
     * It has been added as interface to the std_lq.php script.
     *
     * @param string $type
     * @param string $query
     * @return array|null
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     */
    public function query($type, $query)
    {
        switch ($type) {
            case 'column':
                return $this->queryLivestatusSingleColumn($query);
            case 'row':
                return $this->queryLivestatusSingleRow($query);
            default:
                return $this->queryLivestatus($query);
        }
    }

    /**
     * PUBLIC getObjects()
     *
     * Queries the livestatus socket for a list of objects
     *
     * @param string $type Type of object
     * @param string $name1Pattern Name1 of the objecs
     * @param string $name2Pattern Name2 of the objecs
     * @param string $add_filter
     * @return  array    Results of the query
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    public function getObjects($type, $name1Pattern = '', $name2Pattern = '', $add_filter = '')
    {
        $ret = [];
        $filter = '';
        $sFile = '';

        switch ($type) {
            case 'host':
            case 'hostgroup':
            case 'servicegroup':
                $l = $this->queryLivestatus("GET " . $type . "s\nColumns: name alias\n" . $add_filter);
                break;
            case 'service':
                $query = "GET services\nColumns: host_name description\n";

                if ($name1Pattern) {
                    $query .= "Filter: host_name = " . $name1Pattern . "\n";
                }
                $query .= $add_filter;

                $l = $this->queryLivestatus($query);
                break;
            default:
                return [];
        }
        reset($l);

        $result = [];
        foreach ($l as $entry) {
            $result[] = ['name1' => $entry[0], 'name2' => $entry[1]];
        }

        return $result;
    }

    /**
     * Parses the filter array to backend
     *
     * @param array<array<NagVisObject>> $objects List of objects to query
     * @param array $filters List of filters to apply
     * @return  string    Parsed filters
     * @throws BackendConnectionProblem
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    private function parseFilter(
        $objects,
        $filters,
        $isMemberQuery = false,
        $isCountQuery = false,
        $isHostQuery = true
    ) {
        $aFilters = [];
        foreach ($objects as $OBJS) {
            $objFilters = [];
            foreach ($filters as $filter) {
                if ($isHostQuery && $filter['key'] == 'host_name') {
                    $key = 'name';
                } else {
                    $key = $filter['key'];
                }

                // Array('key' => 'host_name', 'operator' => '=', 'name'),
                switch ($key) {
                    case 'name':
                    case 'host_name':
                    case 'host_groups':
                    case 'service_description':
                    case 'groups':
                    case 'service_groups':
                    case 'hostgroup_name':
                    case 'group_name':
                    case 'servicegroup_name':
                        if ($key != 'service_description') {
                            $val = $OBJS[0]->getName();
                        } else {
                            $val = $OBJS[0]->getServiceDescription();
                        }

                        $objFilters[] = 'Filter: ' . $key . ' ' . $filter['op'] . ' ' . $val . "\n";
                        break;
                    default:
                        throw new BackendConnectionProblem('Invalid filter key (' . $key . ')');
                }
            }

            // is this a dynamic group with already compiled filters?
            if ($isMemberQuery && $OBJS[0]->getType() == 'dyngroup') {
                return $OBJS[0]->getObjectFilter();
            }

            // Are there child exclude filters defined for this object?
            // The objType is the type of the objects to query the data for
            if ($isMemberQuery && $OBJS[0]->hasExcludeFilters($isCountQuery)) {
                $filter = $OBJS[0]->getExcludeFilter($isCountQuery);
                $objType = $OBJS[0]->getType();

                if ($objType == 'host') {
                    $parts = explode('~~', $filter);
                    if (!isset($parts[1])) {
                        $objFilters[] = 'Filter: service_description !~~ ' . $filter . "\n";
                    }

                } elseif ($objType == 'hostgroup' && $isHostQuery) {
                    $parts = explode('~~', $filter);
                    if (!isset($parts[1])) {
                        $objFilters[] = 'Filter: host_name !~~ ' . $parts[0] . "\n";
                    }

                } elseif (($objType == 'hostgroup' && !$isHostQuery) || $objType == 'servicegroup') {
                    $parts = explode('~~', $filter);
                    if (isset($parts[1])) {
                        $objFilters[] = 'Filter: host_name ~~ ' . $parts[0] . "\n"
                            . 'Filter: service_description ~~ ' . $parts[1] . "\n"
                            . "And: 2\n"
                            . "Negate:\n";
                    } else {
                        $objFilters[] = 'Filter: host_name !~~ ' . $parts[0] . "\n";
                    }
                }
            }

            // the object specific filters all need to match
            $count = count($objFilters);
            if ($count > 1) {
                $count = 'And: ' . $count . "\n";
            } else {
                $count = '';
            }

            $aFilters[] = implode('', $objFilters) . $count;
        }

        $count = count($aFilters);
        if ($count > 1) {
            $count = 'Or: ' . $count . "\n";
        } else {
            $count = '';
        }

        return implode('', $aFilters) . $count;
    }

    /**
     * @param string $stateAttr
     * @return string
     */
    private function serviceStateStats($stateAttr)
    {
        $staleness_thresh = cfg('global', 'staleness_threshold');

        return
            // Count PENDING
            "Stats: has_been_checked = 0\n" .
            // Count OK
            "Stats: " . $stateAttr . " = 0\n" .
            "Stats: has_been_checked != 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: host_scheduled_downtime_depth = 0\n" .
            "Stats: staleness < " . $staleness_thresh . "\n" .
            "StatsAnd: 5\n" .
            // Count OK (STALE)
            "Stats: " . $stateAttr . " = 0\n" .
            "Stats: has_been_checked != 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: host_scheduled_downtime_depth = 0\n" .
            "Stats: staleness >= " . $staleness_thresh . "\n" .
            "StatsAnd: 5\n" .
            // Count OK (DOWNTIME)
            "Stats: " . $stateAttr . " = 0\n" .
            "Stats: has_been_checked != 0\n" .
            "Stats: scheduled_downtime_depth > 0\n" .
            "Stats: host_scheduled_downtime_depth > 0\n" .
            "StatsOr: 2\n" .
            "StatsAnd: 3\n" .
            // Count WARNING
            "Stats: " . $stateAttr . " = 1\n" .
            "Stats: acknowledged = 0\n" .
            "Stats: host_acknowledged = 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: host_scheduled_downtime_depth = 0\n" .
            "Stats: staleness < " . $staleness_thresh . "\n" .
            "StatsAnd: 6\n" .
            // Count WARNING (STALE)
            "Stats: " . $stateAttr . " = 1\n" .
            "Stats: acknowledged = 0\n" .
            "Stats: host_acknowledged = 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: host_scheduled_downtime_depth = 0\n" .
            "Stats: staleness >= " . $staleness_thresh . "\n" .
            "StatsAnd: 6\n" .
            // Count WARNING(ACK)
            "Stats: " . $stateAttr . " = 1\n" .
            "Stats: acknowledged = 1\n" .
            "Stats: host_acknowledged = 1\n" .
            "StatsOr: 2\n" .
            "StatsAnd: 2\n" .
            // Count WARNING(DOWNTIME)
            "Stats: " . $stateAttr . " = 1\n" .
            "Stats: scheduled_downtime_depth > 0\n" .
            "Stats: host_scheduled_downtime_depth > 0\n" .
            "StatsOr: 2\n" .
            "StatsAnd: 2\n" .
            // Count CRITICAL
            "Stats: " . $stateAttr . " = 2\n" .
            "Stats: acknowledged = 0\n" .
            "Stats: host_acknowledged = 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: host_scheduled_downtime_depth = 0\n" .
            "Stats: staleness < " . $staleness_thresh . "\n" .
            "StatsAnd: 6\n" .
            // Count CRITICAL (STALE)
            "Stats: " . $stateAttr . " = 2\n" .
            "Stats: acknowledged = 0\n" .
            "Stats: host_acknowledged = 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: host_scheduled_downtime_depth = 0\n" .
            "Stats: staleness >= " . $staleness_thresh . "\n" .
            "StatsAnd: 6\n" .
            // Count CRITICAL(ACK)
            "Stats: " . $stateAttr . " = 2\n" .
            "Stats: acknowledged = 1\n" .
            "Stats: host_acknowledged = 1\n" .
            "StatsOr: 2\n" .
            "StatsAnd: 2\n" .
            // Count CRITICAL(DOWNTIME)
            "Stats: " . $stateAttr . " = 2\n" .
            "Stats: scheduled_downtime_depth > 0\n" .
            "Stats: host_scheduled_downtime_depth > 0\n" .
            "StatsOr: 2\n" .
            "StatsAnd: 2\n" .
            // Count UNKNOWN
            "Stats: " . $stateAttr . " = 3\n" .
            "Stats: acknowledged = 0\n" .
            "Stats: host_acknowledged = 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: host_scheduled_downtime_depth = 0\n" .
            "Stats: staleness < " . $staleness_thresh . "\n" .
            "StatsAnd: 6\n" .
            // Count UNKNOWN (STALE)
            "Stats: " . $stateAttr . " = 3\n" .
            "Stats: acknowledged = 0\n" .
            "Stats: host_acknowledged = 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: host_scheduled_downtime_depth = 0\n" .
            "Stats: staleness >= " . $staleness_thresh . "\n" .
            "StatsAnd: 6\n" .
            // Count UNKNOWN(ACK)
            "Stats: " . $stateAttr . " = 3\n" .
            "Stats: acknowledged = 1\n" .
            "Stats: host_acknowledged = 1\n" .
            "StatsOr: 2\n" .
            "StatsAnd: 2\n" .
            // Count UNKNOWN(DOWNTIME)
            "Stats: " . $stateAttr . " = 3\n" .
            "Stats: scheduled_downtime_depth > 0\n" .
            "Stats: host_scheduled_downtime_depth > 0\n" .
            "StatsOr: 2\n" .
            "StatsAnd: 2\n";
    }

    /**
     * PUBLIC getHostState()
     *
     * Queries the livestatus socket for the state of one or several hosts
     *
     * @param array $objects List of objects to query
     * @param int $options
     * @param array $filters List of filters to apply
     * @param bool $isMemberQuery
     * @return array
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    public function getHostState($objects, $options, $filters, $isMemberQuery = false)
    {
        $objFilter = $this->parseFilter($objects, $filters, $isMemberQuery, !COUNT_QUERY, HOST_QUERY);

        if ($options & 1) {
            $stateAttr = 'hard_state';
        } else {
            $stateAttr = 'state';
        }

        $q = "GET hosts\n" .
            "Columns: " . $stateAttr . " plugin_output alias display_name " .
            "address notes last_check next_check state_type " .
            "current_attempt max_check_attempts last_state_change " .
            "last_hard_state_change perf_data acknowledged " .
            "scheduled_downtime_depth has_been_checked name " .
            "check_command custom_variable_names custom_variable_values staleness\n" .
            $objFilter;

        $l = $this->queryLivestatus($q);

        $arrReturn = [];

        if (is_array($l) && count($l) > 0) {
            foreach ($l as $e) {
                // Catch unchecked objects
                // $e[16]: has_been_checked
                // $e[0]:  state
                if ($e[16] == 0 || $e[0] === '') {
                    $arrReturn[$e[17]] = [
                        UNCHECKED,
                        l('hostIsPending', ['HOST' => $e[17]]),
                        null,
                        null,
                        null,
                    ];
                    continue;
                }

                switch ($e[0]) {
                    case "0":
                        $state = UP;
                        break;
                    case "1":
                        $state = DOWN;
                        break;
                    case "2":
                        $state = UNREACHABLE;
                        break;
                    default:
                        $state = UNKNOWN;
                        break;
                }

                // 15: acknowledged
                $acknowledged = $state != UP && $e[14] == 1;

                // 19: keys, 20: values
                if (isset($e[19][0]) && isset($e[20][0])) {
                    $custom_vars = array_combine($e[19], $e[20]);
                } else {
                    $custom_vars = null;
                }

                // If there is a downtime for this object, save the data
                // $e[15]: scheduled_downtime_depth
                $dt_details = [null, null, null, null];
                if (isset($e[15]) && $e[15] > 0) {
                    $in_downtime = true;

                    // This handles only the first downtime. But this is not backend
                    // specific. The other backends do this as well.
                    $data = $this->queryLivestatusSingleRow(
                        "GET downtimes\n" .
                        "Columns: author comment start_time end_time\n" .
                        "Filter: host_name = " . $e[17] . "\n"
                    );
                    if (isset($data[0])) {
                        $dt_details = $data;
                    }
                } else {
                    $in_downtime = false;
                }

                $arrReturn[$e[17]] = [
                    $state,
                    $e[1],  // output
                    $acknowledged,
                    $in_downtime,
                    (float)$e[21], // staleness
                    $e[8],  // state type
                    $e[9],  // current attempt
                    $e[10], // max attempts
                    $e[6],  // last check
                    $e[7],  // next check
                    $e[12], // last hard state change
                    $e[11], // last state change
                    $e[13], // perfdata
                    $e[3],  // display name
                    $e[2], // alias
                    $e[4],  // address
                    $e[5],  // notes
                    $e[18], // check command
                    $custom_vars,
                    $dt_details[0], // downtime author
                    $dt_details[1], // downtime comment
                    $dt_details[2], // downtime start
                    $dt_details[3], // downtime end
                ];
            }
        }

        return $arrReturn;
    }

    /**
     * Queries the livestatus socket for a specific service
     * or all services of a host
     *
     * @param array $objects List of objects to query
     * @param int $options
     * @param array $filters List of filters to apply
     * @param bool $isMemberQuery
     * @return array
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    public function getServiceState($objects, $options, $filters, $isMemberQuery = false)
    {
        $objFilter = $this->parseFilter($objects, $filters, $isMemberQuery, !COUNT_QUERY, !HOST_QUERY);

        if ($options & 1) {
            $stateAttr = 'last_hard_state';
        } else {
            $stateAttr = 'state';
        }

        $l = $this->queryLivestatus(
            "GET services\n" .
            $objFilter .
            "Columns: description display_name " . $stateAttr . " " .
            "host_alias host_address plugin_output notes last_check next_check " .
            "state_type current_attempt max_check_attempts last_state_change " .
            "last_hard_state_change perf_data scheduled_downtime_depth " .
            "acknowledged host_acknowledged host_scheduled_downtime_depth " .
            "has_been_checked host_name check_command custom_variable_names custom_variable_values " .
            "staleness\n"
        );

        $arrReturn = [];
        if (is_array($l) && count($l) > 0) {
            foreach ($l as $e) {
                // test for the correct key
                if (isset($objects[$e[20] . '~~' . $e[0]])) {
                    $specific = true;
                    $key = $e[20] . '~~' . $e[0];
                } else {
                    $specific = false;
                    $key = $e[20];
                }

                // Catch pending objects
                // $e[19]: has_been_checked
                // $e[2]:  state
                if ($e[19] == 0 || $e[2] === '') {
                    $svc = array_fill(0, EXT_STATE_SIZE, null);
                    $svc[DESCRIPTION] = $e[0];
                    $svc[DISPLAY_NAME] = $e[1];
                    $svc[STATE] = PENDING;
                    $svc[OUTPUT] = l('serviceNotChecked', ['SERVICE' => $e[0]]);
                } else {
                    switch ($e[2]) {
                        case "0":
                            $state = OK;
                            break;
                        case "1":
                            $state = WARNING;
                            break;
                        case "2":
                            $state = CRITICAL;
                            break;
                        case "3":
                        default:
                            $state = UNKNOWN;
                            break;
                    }

                    /**
                     * Handle host/service acks
                     *
                     * If state is not OK (=> WARN, CRIT, UNKNOWN) and service is not
                     * acknowledged => check for acknowledged host
                     */
                    // $e[16]: acknowledged
                    // $e[17]: host_acknowledged
                    $acknowledged = $state != OK && ($e[16] == 1 || $e[17] == 1);

                    // Handle host/service downtimes
                    // $e[15]: scheduled_downtime_depth
                    // $e[18]: host_scheduled_downtime_depth
                    $dt_details = [null, null, null, null];
                    if ((isset($e[15]) && $e[15] > 0) || (isset($e[18]) && $e[18] > 0)) {
                        $in_downtime = true;

                        // This handles only the first downtime. But this is not backend
                        // specific. The other backends do this as well.

                        // Handle host/service downtime difference
                        if (isset($e[15]) && $e[15] > 0) {
                            // Service downtime
                            $data = $this->queryLivestatusSingleRow(
                                "GET downtimes\n" .
                                "Columns: author comment start_time end_time\n" .
                                "Filter: host_name = " . $e[20] . "\n" .
                                "Filter: service_description = " . $e[0] . "\n"
                            );
                        } else {
                            // Host downtime
                            $data = $this->queryLivestatusSingleRow(
                                "GET downtimes\n" .
                                "Columns: author comment start_time end_time\n" .
                                "Filter: host_name = " . $e[20] . "\n"
                            );
                        }
                        if (isset($data[0])) {
                            $dt_details = $data;
                        }
                    } else {
                        $in_downtime = false;
                    }

                    if (isset($e[22][0]) && isset($e[23][0])) {
                        $custom_vars = array_combine($e[22], $e[23]);
                    } else {
                        $custom_vars = null;
                    }

                    $svc = [
                        $state,
                        $e[5],  // output
                        $acknowledged,
                        $in_downtime,
                        (float)$e[24], // staleness
                        $e[9],  // state type
                        $e[10], // current attempt
                        $e[11], // max check attempts
                        $e[7],  // last check
                        $e[8],  // next check
                        $e[13], // last hard state change
                        $e[12], // last state change
                        $e[14], // perfdata
                        $e[1],  // display name
                        $e[3],  // alias
                        $e[4],  // address
                        $e[6],  // notes
                        $e[21], // check command
                        $custom_vars,
                        $dt_details[0], // dt author
                        $dt_details[1], // dt data
                        $dt_details[2], // dt start
                        $dt_details[3], // dt end
                        $e[0], // descr
                    ];
                }

                if ($specific) {
                    $arrReturn[$key] = $svc;
                } else {
                    if (!isset($arrReturn[$key])) {
                        $arrReturn[$key] = [];
                    }

                    $arrReturn[$key][] = $svc;
                }
            }
        }

        return $arrReturn;
    }

    /**
     * Queries the livestatus socket for host state counts. The information
     * are used to calculate the summary output and the summary state of a
     * host and a well performing alternative to the existing recurisve
     * algorithm.
     *
     * @param array $objects
     * @param int $options
     * @param array $filters
     * @return array
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     */
    public function getHostMemberCounts($objects, $options, $filters)
    {
        $objFilter = $this->parseFilter($objects, $filters, MEMBER_QUERY, COUNT_QUERY, !HOST_QUERY);

        if ($options & 1) {
            $stateAttr = 'last_hard_state';
        } else {
            $stateAttr = 'state';
        }

        // Get service information
        $l = $this->queryLivestatus("GET services\n" .
            $objFilter .
            $this->serviceStateStats($stateAttr) .
            "Columns: host_name host_alias\n");

        $arrReturn = [];
        if (is_array($l) && count($l) > 0) {
            // livestatus previous 1.1.9i3 answers without host_alias - these users should update.
            if (!isset($l[0][13])) {
                throw new BackendInvalidResponse(l(
                    'Livestatus version used in backend [BACKENDID] is too old. Please update.',
                    ['BACKENDID' => $this->backendId]
                ));
            }

            foreach ($l as $e) {
                // Workaround for Icinga 2 which answers stats queries for not existing objects with
                // 0 stats but with missing host_name and host_alias columns. So the number of answer
                // columns is different leading to an exception in the code below.
                if (count($e) != 18) {
                    continue;
                }

                $arrReturn[$e[0]] = [
                    //'details' => Array('alias' => $e[1]),
                    'counts' => [
                        PENDING => [
                            'normal' => intval($e[2]),
                        ],
                        OK => [
                            'normal' => intval($e[3]),
                            'stale' => intval($e[4]),
                            'downtime' => intval($e[5]),
                        ],
                        WARNING => [
                            'normal' => intval($e[6]),
                            'stale' => intval($e[7]),
                            'ack' => intval($e[8]),
                            'downtime' => intval($e[9]),
                        ],
                        CRITICAL => [
                            'normal' => intval($e[10]),
                            'stale' => intval($e[11]),
                            'ack' => intval($e[12]),
                            'downtime' => intval($e[13]),
                        ],
                        UNKNOWN => [
                            'normal' => intval($e[14]),
                            'stale' => intval($e[15]),
                            'ack' => intval($e[16]),
                            'downtime' => intval($e[17]),
                        ],
                    ]
                ];
            }
        }

        return $arrReturn;
    }

    /**
     * Queries the livestatus socket a bunch of services matching a given livestatus filter.
     * It does not return all objects, instead it returns the state counts.
     *
     * @param int $options
     * @param string $filter
     * @return array|array[]
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     */
    public function getServiceListCounts($options, $filter)
    {
        if ($options & 1) {
            $stateAttr = 'last_hard_state';
        } else {
            $stateAttr = 'state';
        }

        // Get service information
        $l = $this->queryLivestatus(
            "GET services\n" .
            $filter .
            $this->serviceStateStats($stateAttr)
        );

        $counts = [];
        if (!is_array($l) || count($l) != 1) {
            return [];
        }
        $e = $l[0];
        return [
            PENDING => [
                'normal' => intval($e[0]),
            ],
            OK => [
                'normal' => intval($e[1]),
                'stale' => intval($e[2]),
                'downtime' => intval($e[3]),
            ],
            WARNING => [
                'normal' => intval($e[4]),
                'stale' => intval($e[5]),
                'ack' => intval($e[6]),
                'downtime' => intval($e[7]),
            ],
            CRITICAL => [
                'normal' => intval($e[8]),
                'stale' => intval($e[9]),
                'ack' => intval($e[10]),
                'downtime' => intval($e[11]),
            ],
            UNKNOWN => [
                'normal' => intval($e[12]),
                'stale' => intval($e[13]),
                'ack' => intval($e[14]),
                'downtime' => intval($e[15]),
            ],
        ];
    }

    /**
     * @param int $options
     * @param string $host_filter
     * @param string $service_filter
     * @param bool $by_group
     * @return array|array[]
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     */
    public function getHostAndServiceCounts(
        $options,
        $host_filter,
        $service_filter,
        $by_group = true
    ) {
        if ($by_group) {
            $host_suffix = 'bygroup';
            $service_suffix = 'byhostgroup';

            $host_grouping = "Columns: hostgroup_name hostgroup_alias\n";
            $service_grouping = "Columns: hostgroup_name\n";

            $hoffset = 2;
            $soffset = 1;
        } else {
            $host_suffix = '';
            $service_suffix = '';

            $host_grouping = '';
            $service_grouping = '';

            $hoffset = 0;
            $soffset = 0;
        }

        $staleness_thresh = cfg('global', 'staleness_threshold');

        if ($options & 1) {
            $stateAttr = 'hard_state';
        } else {
            $stateAttr = 'state';
        }

        // Get host information
        $q = "GET hosts" . $host_suffix . "\n" .
            $host_filter .
            // Count UNCHECKED
            "Stats: has_been_checked = 0\n" .
            // Count UP
            "Stats: " . $stateAttr . " = 0\n" .
            "Stats: has_been_checked != 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: staleness < " . $staleness_thresh . "\n" .
            "StatsAnd: 4\n" .
            // Count UP(STALE)
            "Stats: " . $stateAttr . " = 0\n" .
            "Stats: has_been_checked != 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: staleness >= " . $staleness_thresh . "\n" .
            "StatsAnd: 4\n" .
            // Count UP (DOWNTIME)
            "Stats: " . $stateAttr . " = 0\n" .
            "Stats: has_been_checked != 0\n" .
            "Stats: scheduled_downtime_depth > 0\n" .
            "StatsAnd: 3\n" .
            // Count DOWN
            "Stats: " . $stateAttr . " = 1\n" .
            "Stats: acknowledged = 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: staleness < " . $staleness_thresh . "\n" .
            "StatsAnd: 4\n" .
            // Count DOWN(STALE)
            "Stats: " . $stateAttr . " = 1\n" .
            "Stats: acknowledged = 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: staleness >= " . $staleness_thresh . "\n" .
            "StatsAnd: 4\n" .
            // Count DOWN(ACK)
            "Stats: " . $stateAttr . " = 1\n" .
            "Stats: acknowledged = 1\n" .
            "StatsAnd: 2\n" .
            // Count DOWN(DOWNTIME)
            "Stats: " . $stateAttr . " = 1\n" .
            "Stats: scheduled_downtime_depth > 0\n" .
            "StatsAnd: 2\n" .
            // Count UNREACHABLE
            "Stats: " . $stateAttr . " = 2\n" .
            "Stats: acknowledged = 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: staleness < " . $staleness_thresh . "\n" .
            "StatsAnd: 4\n" .
            // Count UNREACHABLE(STALE)
            "Stats: " . $stateAttr . " = 2\n" .
            "Stats: acknowledged = 0\n" .
            "Stats: scheduled_downtime_depth = 0\n" .
            "Stats: staleness >= " . $staleness_thresh . "\n" .
            "StatsAnd: 4\n" .
            // Count UNREACHABLE(ACK)
            "Stats: " . $stateAttr . " = 2\n" .
            "Stats: acknowledged = 1\n" .
            "StatsAnd: 2\n" .
            // Count UNREACHABLE(DOWNTIME)
            "Stats: " . $stateAttr . " = 2\n" .
            "Stats: scheduled_downtime_depth > 0\n" .
            "StatsAnd: 2\n" .
            $host_grouping;
        $l = $this->queryLivestatus($q);

        // If the method should fetch several objects and did not find
        // any object, don't return anything => The message
        // that the objects were not found is added by the core
        $arrReturn = [];
        if (is_array($l) && count($l) > 0) {
            // livestatus previous 1.1.9i3 answers without hostgroup_alias - these users should update.
            if (!isset($l[0][8 + $hoffset])) {
                throw new BackendInvalidResponse(l(
                    'Livestatus version used in backend [BACKENDID] is too old. Please update.',
                    ['BACKENDID' => $this->backendId]
                ));
            }
            foreach ($l as $e) {
                // Workaround for Icinga 2 which answers stats queries for not existing objects with
                // 0 stats but with missing host_name and host_alias columns. So the number of answer
                // columns is different leading to an exception in the code below.
                if (count($e) != 12 + $hoffset) {
                    continue;
                }

                $counts = [
                    UNCHECKED => [
                        'normal' => intval($e[0 + $hoffset]),
                    ],
                    UP => [
                        'normal' => intval($e[1 + $hoffset]),
                        'stale' => intval($e[2 + $hoffset]),
                        'downtime' => intval($e[3 + $hoffset]),
                    ],
                    DOWN => [
                        'normal' => intval($e[4 + $hoffset]),
                        'stale' => intval($e[5 + $hoffset]),
                        'ack' => intval($e[6 + $hoffset]),
                        'downtime' => intval($e[7 + $hoffset]),
                    ],
                    UNREACHABLE => [
                        'normal' => intval($e[8 + $hoffset]),
                        'stale' => intval($e[9 + $hoffset]),
                        'ack' => intval($e[10 + $hoffset]),
                        'downtime' => intval($e[11 + $hoffset]),
                    ],
                ];

                if (!$by_group) {
                    $arrReturn = $counts;
                } else {
                    $arrReturn[$e[0]] = [
                        'details' => [ALIAS => $e[1]],
                        'counts' => $counts
                    ];
                }
            }
        }

        // If recognize_services are disabled don't fetch service information
        if ($options & 2) {
            return $arrReturn;
        }

        if ($options & 1) {
            $stateAttr = 'last_hard_state';
        } else {
            $stateAttr = 'state';
        }

        // Get service information
        $l = $this->queryLivestatus("GET services" . $service_suffix . "\n" .
            $service_filter .
            $this->serviceStateStats($stateAttr) .
            $service_grouping);

        if (is_array($l) && count($l) > 0) {
            foreach ($l as $e) {
                // Workaround for Icinga 2 which answers stats queries for not existing objects with
                // 0 stats but with missing host_name and host_alias columns. So the number of answer
                // columns is different leading to an exception in the code below.
                if (count($e) != 16 + $soffset) {
                    continue;
                }

                $counts = [
                    PENDING => [
                        'normal' => intval($e[0 + $soffset])
                    ],
                    OK => [
                        'normal' => intval($e[1 + $soffset]),
                        'stale' => intval($e[2 + $soffset]),
                        'downtime' => intval($e[3 + $soffset]),
                    ],
                    WARNING => [
                        'normal' => intval($e[4 + $soffset]),
                        'stale' => intval($e[5 + $soffset]),
                        'ack' => intval($e[6 + $soffset]),
                        'downtime' => intval($e[7 + $soffset]),
                    ],
                    CRITICAL => [
                        'normal' => intval($e[8 + $soffset]),
                        'stale' => intval($e[9 + $soffset]),
                        'ack' => intval($e[10 + $soffset]),
                        'downtime' => intval($e[11 + $soffset]),
                    ],
                    UNKNOWN => [
                        'normal' => intval($e[12 + $soffset]),
                        'stale' => intval($e[13 + $soffset]),
                        'ack' => intval($e[14 + $soffset]),
                        'downtime' => intval($e[15 + $soffset]),
                    ],
                ];

                if (!$by_group) {
                    $arrReturn += $counts;
                } else {
                    $arrReturn[$e[0]]['counts'] += $counts;
                }
            }
        }

        return $arrReturn;
    }

    /**
     * PUBLIC getHostgroupStateCounts()
     *
     * Queries the livestatus socket for hostgroup state counts. The information
     * are used to calculate the summary output and the summary state of a
     * hostgroup and a well performing alternative to the existing recurisve
     * algorithm.
     *
     * @param array $objects List of objects to query
     * @param int $options
     * @param array $filters List of filters to apply
     * @return  array     List of states and counts
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    public function getHostgroupStateCounts($objects, $options, $filters)
    {
        $host_filter = $this->parseFilter($objects, $filters, MEMBER_QUERY, COUNT_QUERY, HOST_QUERY);

        $service_filter = $this->parseFilter($objects, $filters, MEMBER_QUERY, COUNT_QUERY, !HOST_QUERY);
        // Little hack to correct the different field names
        $service_filter = str_replace(' groups ', ' host_groups ', $service_filter);

        return $this->getHostAndServiceCounts($options, $host_filter, $service_filter);
    }

    /**
     * PUBLIC getServicegroupStateCounts()
     *
     * Queries the livestatus socket for servicegroup state counts. The information
     * are used to calculate the summary output and the summary state of a
     * servicegroup and a well performing alternative to the existing recurisve
     * algorithm.
     *
     * @param array $objects List of objects to query
     * @param int $options
     * @param array $filters List of filters to apply
     * @return  array     List of states and counts
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    public function getServicegroupStateCounts($objects, $options, $filters)
    {
        $objFilter = $this->parseFilter($objects, $filters, MEMBER_QUERY, COUNT_QUERY, !HOST_QUERY);

        if ($options & 1) {
            $stateAttr = 'last_hard_state';
        } else {
            $stateAttr = 'state';
        }

        // Get service information
        $l = $this->queryLivestatus("GET servicesbygroup\n" .
            $objFilter .
            $this->serviceStateStats($stateAttr) .
            "Columns: servicegroup_name servicegroup_alias\n");

        // If the method should fetch several objects and did not find
        // any object, don't return anything => The message
        // that the objects were not found is added by the core
        $arrReturn = [];
        if (is_array($l) && count($l) > 0) {
            // livestatus previous 1.1.9i3 answers without servicegroup_alias - these users should update.
            if (!isset($l[0][13])) {
                throw new BackendInvalidResponse(l(
                    'Livestatus version used in backend [BACKENDID] is too old. Please update.',
                    ['BACKENDID' => $this->backendId]
                ));
            }
            foreach ($l as $e) {
                $arrReturn[$e[0]] = [
                    'details' => [ALIAS => $e[1]],
                    'counts' => [
                        PENDING => [
                            'normal' => intval($e[2]),
                        ],
                        OK => [
                            'normal' => intval($e[3]),
                            'stale' => intval($e[4]),
                            'downtime' => intval($e[5]),
                        ],
                        WARNING => [
                            'normal' => intval($e[6]),
                            'stale' => intval($e[7]),
                            'ack' => intval($e[8]),
                            'downtime' => intval($e[9]),
                        ],
                        CRITICAL => [
                            'normal' => intval($e[10]),
                            'stale' => intval($e[11]),
                            'ack' => intval($e[12]),
                            'downtime' => intval($e[13]),
                        ],
                        UNKNOWN => [
                            'normal' => intval($e[14]),
                            'stale' => intval($e[15]),
                            'ack' => intval($e[16]),
                            'downtime' => intval($e[17]),
                        ],
                    ]
                ];
            }
        }

        return $arrReturn;
    }

    /**
     * PUBLIC getHostNamesWithNoParent()
     *
     * Queries the livestatus socket for all hosts without parent
     *
     * @return  array    List of hostnames which have no parent
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    public function getHostNamesWithNoParent()
    {
        return $this->queryLivestatusSingleColumn("GET hosts\nColumns: name\nFilter: parents =\n");
    }

    /**
     * PUBLIC getDirectChildNamesByHostName()
     *
     * Queries the livestatus socket for all direct childs of a host
     *
     * @param string $hostName Hostname
     * @return  array    List of hostnames
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    public function getDirectChildNamesByHostName($hostName)
    {
        return $this->queryLivestatusList("GET hosts\nColumns: childs\nFilter: name = " . $hostName . "\n");
    }

    /**
     * Queries the livestatus socket for all direct parents of a host
     *
     * @param string $hostName Hostname
     * @return  array    List of hostnames
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    public function getDirectParentNamesByHostName($hostName)
    {
        return $this->queryLivestatusList("GET hosts\nColumns: parents\nFilter: name = " . $hostName . "\n");
    }

    /**
     * @param string $name
     * @return array|mixed
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     */
    public function getHostNamesInHostgroup($name)
    {
        $r = $this->queryLivestatusSingleColumn("GET hostgroups\nColumns: members\nFilter: name = " . $name . "\n");
        if (isset($r[0])) {
            return $r[0];
        } else {
            return [];
        }
    }

    /**
     * Returns all hostnames which have a state != UP or a service != OK
     *
     * @return array
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     */
    public function getHostNamesProblematic()
    {
        $r = $this->queryLivestatusSingleColumn("GET hosts\nColumns: name\nFilter: state != 0\n");
        $r = array_merge(
            $r,
            $this->queryLivestatusSingleColumn("GET services\nColumns: host_name\nFilter: state != 0\n")
        );
        return $r;
    }

    /**
     * @return int|mixed
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     */
    public function getProgramStart()
    {
        $r = $this->queryLivestatusSingleColumn("GET status\nColumns: program_start\n");
        if (isset($r[0])) {
            return $r[0];
        } else {
            return -1;
        }
    }

    /**
     * @param string|null $filterHostgroup
     * @return array
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     */
    public function getGeomapHosts($filterHostgroup = null)
    {
        $query = "GET hosts\nColumns: name custom_variable_names custom_variable_values alias\n";
        if ($filterHostgroup) {
            $query .= "Filter: groups >= " . $filterHostgroup . "\n";
        }
        $r = $this->queryLivestatus($query);
        $hosts = [];
        foreach ($r as $row) {
            if ($row[1] && $row[2]) {
                $custom_variables = array_combine($row[1], $row[2]);
                if (isset($custom_variables['LAT']) && isset($custom_variables['LONG'])) {
                    $hosts[] = [
                        'name' => $row[0],
                        'lat' => $custom_variables['LAT'],
                        'long' => $custom_variables['LONG'],
                        'alias' => $row[3],
                        'backend_id' => $this->backendId,
                    ];
                }
            }
        }
        return $hosts;
    }

    /**
     * @param string $cmd
     * @return array|null
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     */
    private function command($cmd)
    {
        return $this->queryLivestatus('COMMAND [' . time() . '] ' . $cmd . "\n", false);
    }

    /**
     * Sends acknowledgement command to monitoring core
     *
     * @param string $what
     * @param string $spec
     * @param string $comment
     * @param bool $sticky
     * @param bool $notify
     * @param bool $persist
     * @param string $user
     * @return void
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     */
    public function actionAcknowledge($what, $spec, $comment, $sticky, $notify, $persist, $user)
    {
        if ($what == 'host') {
            $what = 'HOST';
        } elseif ($what == 'service') {
            $what = 'SVC';
        }

        $sticky = $sticky ? '2' : '0';
        $notify = $notify ? '1' : '0';
        $persist = $notify ? '1' : '0';

        $this->command(
            'ACKNOWLEDGE_'
            . $what
            . '_PROBLEM;'
            . $spec
            . ';'
            . $sticky
            . ';'
            . $notify
            . ';'
            . $persist
            . ';'
            . $user
            . ';'
            . $comment
        );
    }

    /**
     * Queries the livestatus socket for all direct childs dependencies of a host
     *
     * @param string $hostName Hostname
     * @param bool $min_business_impact
     * @return  array    List of hostnames
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Thibault Cohen <thibault.cohen@savoirfairelinux.com>
     */
    public function getDirectChildDependenciesNamesByHostName(
        $hostName,
        $min_business_impact = false
    ) {
        $query = "GET hosts\nColumns: child_dependencies\nFilter: name = " . $hostName . "\n";
        $raw_result = $this->queryLivestatusSingleColumn($query);
        if ($min_business_impact) {
            $query = "GET hosts\nColumns: name\nFilter: name = $raw_result[0][1]\n";
            foreach ($raw_result[0] as &$value) {
                $query = $query . "Filter: name = $value\nOr: 2\n";
            }
            $query = $query . "Filter: business_impact >= $min_business_impact\nAnd: 2\n";
            $result = $this->queryLivestatusSingleColumn($query);
        } else {
            $result = [];
            foreach ($raw_result[0] as &$value) {
                // todo: is this intentional use of ==
                if (strpos($value, "/") == false) {
                    $result[] = $value;
                }
            }
        }
        return $result;
    }

    /**
     * Queries the livestatus socket for all direct parents of a host
     *
     * @param string   Hostname
     * @param bool $min_business_impact
     * @return  array    List of hostnames
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     * @author  Mathias Kettner <mk@mathias-kettner.de>
     * @author  Lars Michelsen <lm@larsmichelsen.com>
     */
    public function getDirectParentDependenciesNamesByHostName(
        $hostName,
        $min_business_impact = false
    ) {
        $query = "GET hosts\nColumns: parent_dependencies\nFilter: name = " . $hostName . "\n";
        $raw_result = $this->queryLivestatusSingleColumn($query);
        if ($min_business_impact) {
            $query = "GET hosts\nColumns: name\nFilter: name = $raw_result[0][1]\n";
            foreach ($raw_result[0] as &$value) {
                $query = $query . "Filter: name = $value\nOr: 2\n";
            }
            $query = $query . "Filter: business_impact >= $min_business_impact\nAnd: 2\n";
            $result = $this->queryLivestatusSingleColumn($query);
        } else {
            $result = [];
            foreach ($raw_result[0] as &$value) {
                // todo: is this intentional use of ==
                if (strpos($value, "/") == false) {
                    $result[] = $value;
                }
            }
        }
        return $result;
    }

    /**
     * Returns an assoziative array with contact names as keys and
     * a list of their contactgroups as value
     *
     * @return array
     * @throws BackendConnectionProblem
     * @throws BackendInvalidResponse
     */
    public function getContactsWithGroups()
    {
        $contacts = [];
        $query = "GET contactgroups\nColumns: name members\n";
        foreach ($this->queryLivestatus($query) as $row) {
            foreach ($row[1] as $contact) {
                if (!isset($contacts[$contact])) {
                    $contacts[$contact] = [];
                }
                $contacts[$contact][] = $row[0];
            }
        }
        return $contacts;
    }
}
