<?php

class CfActions
{
    public const PG_CHANNEL_REPORT_COLLECT = 'report_collect';

    /** @var PDO $connection */
    private $cfdbConnection;

    private $username;
    private $allowedPermissions;
    private $hubAddress = 'localhost';

    public function __construct($cfdbConnection, $username)
    {
        $this->cfdbConnection = $cfdbConnection;
        $this->username = $username;
        $userRoles = (new CfUsers($username))->getUserRoles();
        $this->allowedPermissions = (new CfRBAC())->getPermissionsByRoles($userRoles);
        set_time_limit(60);
    }

    /**
     * @rbacName Report collection
     * @rbacDescription Triggering report collection by a host key
     * @rbacGroup Actions
     * @rbacAlias actions.report_collection
     * @rbacAllowedByDefault
     */
    public function report_collection($data)
    {
        $this->checkAccess('actions.report_collection');
        if (!property_exists($data, 'hostkey') || empty($data->hostkey) || !is_string($data->hostkey)) {
            throw new Exception('`hostkey` field required');
        }

        // get a host by hostkey and check if exists
        // if user has no access (RBAC) to the host then response will be empty
        $response = json_decode(cfapi_host_get($this->username, $data->hostkey), true);
        if (!isset($response['data'][0]['id']) || $response['data'][0]['id'] != $data->hostkey) {
            throw new Pest_NotFound("Could not find the host `{$data->hostkey}`");
        }

        $stmt = $this->cfdbConnection->prepare("SELECT pg_notify(?, ?)");
        $stmt->execute([self::PG_CHANNEL_REPORT_COLLECT, $data->hostkey]);
        return $stmt->fetch();
    }

    private function checkAccess($action)
    {
        if (!RbacAccessService::isActionAllowed($action, $this->allowedPermissions)) {
            throw new Exception("Action `$action` is not allowed");
        }
    }

    public function agent_run($data)
    {
        $this->checkAccess('actions.agent_run');
        $this->checkRequired($data, 'hostkey');

        if (!is_string($data->hostkey)) {
            throw new InvalidArgumentException('`hostkey` should be a string');
        }

        // get a host by hostkey and check if exists
        // if user has no access (RBAC) to the host then response will be empty
        $response = json_decode(cfapi_host_get($this->username, $data->hostkey), true);
        if (!isset($response['data'][0]['id']) || $response['data'][0]['id'] != $data->hostkey) {
            throw new Pest_NotFound("Could not find the host `{$data->hostkey}`");
        }

        $ip = $this->getIpByHostkey($data->hostkey);
        if ($ip === null) {
            throw new Exception("Could not find IP address of `{$data->hostkey}` host");
        }
        $message = $ip . PHP_EOL;

        $socket = socket_create(AF_UNIX, SOCK_STREAM, 0);
        if (@socket_connect($socket, CF_RUN_AGENT_SOCKET) === false) {
            $error = 'Cannot connect to ' . CF_RUN_AGENT_SOCKET . ', err: ' . socket_strerror(socket_last_error());
            throw new SocketConnectionException($error);
        }

        socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array('sec' => 50, 'usec' => 0));

        if (socket_write($socket, $message, strlen($message)) === false) {
            $error = 'Cannot write to socket, err: ' . socket_strerror(socket_last_error());
            throw new Exception($error);
        }

        $max_tries = 24; # at 5 second intervals that is 2 minutes which roughly matches the new Timeout in httpd.conf in masterfiles
        $tries = 0;
        $retMsg = false;
        while ($retMsg === false && $tries < $max_tries) {
            # Note here that we need to read whatever is output by update.cf and promises.cf according to commands in controls/cf_serverd.cf
            $retMsg = socket_read($socket, 4096);
            $code = socket_last_error($socket);
            if ($retMsg === false && $code === SOCKET_EAGAIN) {
                sleep(5);
                $tries++;
                continue;
            }
            if ($retMsg === false) {
                $error = 'Cannot read from socket, err: ' . socket_strerror(socket_last_error());
                throw new Exception($error);
            }
        }

        ['output' => $output, 'exit_code' => $exit_code] = json_decode($retMsg, true);
        if ($exit_code !== 0) {
            $error = array_filter(
                explode(PHP_EOL, $output),
                fn ($line) => strstr($line, 'error')
            );
            $error = $error ? implode(PHP_EOL, array_map('trim', $error)) : trim($output);
            throw new ResponseException($error, Response::NOTACCEPTABLE);
        }

        socket_close($socket);

        return $retMsg;
    }

    /**
     * @param $data
     * @param $property
     * @throws Exception
     */
    private function checkRequired($data, $property)
    {
        if (!property_exists($data, $property) || empty($data->{$property})) {
            throw new Exception("`$property` field required");
        }
    }

    private function getIpByHostkey(string $hostkey)
    {
        $hubModel = new CfHub(CfdbPdo::getInstance()->getConnection());

        if ($hubModel->isHub($hostkey)) {
            return $this->hubAddress;
        }

        $quotedHostkey = DatabaseHelper::quote($hostkey);
        $query = "
            SELECT remotehostip FROM lastseenhosts
            WHERE remotehostkey = $quotedHostkey
        ";

        $data = json_decode(cfapi_query_post($this->username, $query, '', false, -1, 1, [], []), true);
        if (!isset($data['data'][0]['rows']) || empty($data['data'][0]['rows'])) {
            syslog(
                LOG_DEBUG,
                'CfActions::getIpFromLastSeenByHostkey cannot find IP address by ' . $hostkey . 'to trigger agent run'
            );
            return null;
        }
        return $data['data'][0]['rows'][0][0];
    }
}
