<?php
require_once APPPATH . 'modules/dashboard/models/entities/CF_Rule.php';
require_once APPPATH . 'modules/dashboard/models/entities/CF_RuleFilterData.php';
require_once APPPATH . 'models/traits/InventoryLibTrait.php';

class rules_model extends Cf_Model
{
    use InventoryLibTrait;
    public $collectionName = 'dashboard_rules';
    public $variables_dictionary = 'variables_dictionary';

    public $defaultFilter;

    public function __construct()
    {
        parent::__construct();

        $this->load->library('dashboard/InventoryAlert');
        $this->load->library('dashboard/PolicyAlert');
        $this->load->library('dashboard/SoftwareUpdateAlert');
        $this->load->library('dashboard/CustomAlert');
        $this->load->library('dashboard/FileChangedAlert');
        $this->load->model('groups_app/Shared_groups_model', 'shared_groups_model', true);
        $this->loadInventoryLib();
    }

    public function setDefaultFilter($filter)
    {
        $this->defaultFilter = $filter;
    }

    public function saverule($data, $overwrite = false, $is_admin = false)
    {
        if (!is_array($data)) {
            $this->setError('Invalid parameter passed to function');
            return false;
        }

        // set username as default filter
        $filter = [];
        $filter['username'] = $data['username'];

        if (isset($data['id']) && !empty($data['id'])) {
            $filter['id'] = $data['id'];
        }

        if (isset($data['export_id']) && !empty($data['export_id'])) {
            $filter['export_id'] = $data['export_id'];
        }

        $this->setDefaultFilter($filter);

        if ($this->validate($data, $overwrite, $is_admin)) {
            $id = '';

            $filter = $this->defaultFilter;

            // if `id` exists no need to use name in filter, because name might be changed and $currentItem will be null
            if (!array_key_exists('id', $filter) && !array_key_exists('export_id', $filter)) {
                $filter['name'] = $data['name'];
            }

            $currentItem = $this->get_all_rules($filter);

            if (!empty($currentItem)) {
                $id = $currentItem[0]->id;
                $reportOwner = $currentItem[0]->getUsername();

                // alert report owner. if this report created by user and get's overwritten by admin, we will try to keep original owner
                // $data['username'] - user who created report, in most cases current logged user
                if ($reportOwner !== $data['username'] && $is_admin === true) {
                    $data['username'] = $reportOwner;
                }
            }

            try {
                $insertData = $this->_map_for_insert($data);
                if ($id == '') {
                    // not exist - insert it will return inserted item

                    $this->db->insert($this->collectionName, $insertData);
                    $id = $this->db->insert_id();
                    log_message('debug', 'Successful save rule.: ' . $id);
                } else {
                    // if exist - update, and then select inserted item
                    $this->db
                        ->where([
                            'id' => $id,
                        ])
                        ->update($this->collectionName, $insertData);
                }
            } catch (Exception $e) {
                $message = $e->getMessage();
                log_message('debug', 'Unable to save rule. Error: ' . $message);
                throw new Exception($message);
            }

            $offset = '';
            $limit = '';
            $order = [];
            // get object from DB
            $ruleObj = $this->get_all_rules(
                [
                    'id' => $id,
                ],
                $offset,
                $limit,
                $order,
                $is_admin,
            );

            if ($ruleObj) {
                return $ruleObj[0];
            }
        }
        // in case of validation error we return false and error can be get from getErrors();

        return false;
    }

    public function _map_for_insert($params)
    {
        if (array_key_exists('policyConditions', $params)) {
            $params['policyConditions'] =
                $params['policyConditions'] == null ? '{}' : json_encode($params['policyConditions']);
        }

        if (array_key_exists('inventoryConditions', $params)) {
            $params['inventoryConditions'] =
                $params['inventoryConditions'] == null ? '{}' : json_encode($params['inventoryConditions']);
        }

        if (array_key_exists('softwareUpdateConditions', $params)) {
            $params['softwareUpdateConditions'] =
                $params['softwareUpdateConditions'] == null ? '{}' : json_encode($params['softwareUpdateConditions']);
        }

        if (array_key_exists('customConditions', $params)) {
            $params['customConditions'] =
                $params['customConditions'] == null ? '{}' : json_encode($params['customConditions']);
        }

        if (array_key_exists('fileChangedConditions', $params)) {
            $params['fileChangedConditions'] =
                $params['fileChangedConditions'] == null ? '{}' : json_encode($params['fileChangedConditions']);
        }
        if (array_key_exists('filterdata', $params)) {
            $params['filterdata'] = $params['filterdata'] == null ? '{}' : json_encode($params['filterdata']);
        }

        return $params;
    }

    public function validate($data, $overwrite = false, $is_admin = false)
    {
        $valid = true;

        if (!isset($data['username']) || trim($data['username']) == '') {
            $valid = false;
            $this->setError('username is empty');
            return false;
        }

        if (!isset($data['name']) || trim($data['name']) == '') {
            $valid = false;
            $this->setError($this->lang->line('dashboard_rule_name_empty'));
            return false;
        }

        if (!isset($data['type']) || trim($data['type']) == '') {
            $valid = false;
            $this->setError($this->lang->line('dashboard_rule_type_empty'));
            return false;
        }

        switch ($data['type']) {
            case 'inventory':
                if (empty($data['inventoryConditions'])) {
                    $valid = false;
                    $this->setError($this->lang->line('dashboard_rule_inventory_data_empty'));
                    return false;
                }
                break;
            // for software updates - all fields can be empty, so no backend validation is needed
        }

        //alert if user admin or owner of this report, otherwise - do not allow to save
        if ($this->_nameAlreadyExist($data) && !(isset($data['export_id']) && $overwrite == true)) {
            $valid = false;
            $this->setError($this->lang->line('dashboard_rule_already_exists'), 422); //422 Unprocessable Entity
        }

        return $valid;
    }

    // TODO: refactor. is_admin is not in use
    public function get_all_rules($filter = [], $offset = '', $limit = '', $order = [], $is_admin = false)
    {
        if (!empty($limit)) {
            $this->db->limit($limit, $offset);
        }
        if (!empty($order)) {
            reset($order);
            $field = key($order);
            $orderValue = current($order);
            $this->db->order_by($field, $orderValue);
        }

        $query = $this->db->where($filter)->get($this->collectionName);
        $ruleResult = $this->getResult($query);
        return $ruleResult;
    }

    public function get_rules_with_ids($ids = [], $extraFilter = [], $order = [])
    {
        $intIds = array_map('intval', $ids);
        $this->db->where($extraFilter);
        if (!empty($intIds)) {
            $this->db->where_in('id', $intIds);
        }

        if (!empty($order)) {
            reset($order);
            $field = key($order);
            $orderValue = current($order);
            $this->db->order_by($field, $orderValue);
        }

        $query = $this->db->get($this->collectionName);
        if (!$query) {
            throw new Exception('Unable to retrieve rules due to database query error.');
        }
        $ruleResult = $this->getResult($query);

        return $ruleResult;
    }

    public function get_rules_with_export_ids(array $exportIds, string $userName)
    {
        $this->db->where_in('export_id', $exportIds);
        $this->db->where('username', $userName);
        if ($query = $this->db->get($this->collectionName)) {
            return $this->getResult($query);
        }
    }

    public function get_all_rules_having_name($names = [], $extraFilter = [])
    {
        $this->db->where_in('name', $names);

        if (!empty($extraFilter)) {
            $this->db->where($extraFilter);
        }

        $query = $this->db->get($this->collectionName);
        $ruleResult = $this->getResult($query);
        return $ruleResult;
    }

    public function getResult($query)
    {
        $result = [];

        foreach ((array) $query->result_array() as $dbResult) {
            $obj = new CF_Rule($dbResult);

            $groupId = $obj->getFilterGroupId();
            if ($groupId !== null) {
                /**
                 * @var CF_SharedGroup
                 */
                $group = $this->shared_groups_model->get($groupId);
                if ($group !== null) {
                    $obj->filterdata->filters = new CF_RuleFilterDataFilters([
                        'filter' => $group->filter['filter'] ?? [],
                        'hostFilter' => $group->filter['hostFilter'] ?? null,
                        'hostContextInclude' => $group->filter['hostContextInclude'] ?? [],
                    ]);
                }
            }

            $result[] = $obj;
        }

        return $result;
    }

    public function deleterule($id, $username, $is_admin)
    {
        // delete from widgets
        // for now we can not share alerts, so we select only user widgets

        $ruleFilter = [];

        // admin can delete any alert
        if ($is_admin !== true) {
            $ruleFilter['username'] = $username;
        }

        //0. check if rule exist
        // database will take care of deleting corresponding alert

        $ruleFilter['id'] = intval($id);

        try {
            $this->db->trans_start();
            $return = $this->db->where($ruleFilter)->delete($this->collectionName);
            $this->deleteRuleFromReports($ruleFilter['id']);
            $this->db->trans_complete();
            return $return;
        } catch (Exception $e) {
            $this->db->trans_rollback();
            log_message('error', $e->getMessage() . ' ' . $e->getFile() . ' line:' . $e->getLine());
            throw $e;
        }
    }

    /**
     * @param int $id
     * Delete rule references from reports
     */
    private function deleteRuleFromReports(int $id)
    {
        $reports = $this->db
            ->query(
                "SELECT * FROM report
             WHERE reportcategory = 'compliance_report'
             AND ((advancedreportsdata->>'SQL')::jsonb->'conditions')::jsonb @> '[$id]'::jsonb",
            )
            ->result_array();

        if (sizeof($reports) > 0) {
            foreach ($reports as $report) {
                // unpack advancedreportsdata
                $advReportsData = json_decode($report['advancedreportsdata'], true);
                $advReportsData['SQL'] = is_array($advReportsData['SQL'])
                    ? $advReportsData['SQL']
                    : json_decode($advReportsData['SQL'], true);

                //remove needed rule id from report conditions array
                $advReportsData['SQL']['conditions'] = array_values(
                    array_diff($advReportsData['SQL']['conditions'], [$id]),
                );

                $advReportsData['SQL'] = json_encode($advReportsData['SQL']);
                $this->db
                    ->where(['id' => $report['id']])
                    ->update('report', ['advancedreportsdata' => json_encode($advReportsData)]);
            }
        }
    }

    /**
     * @param int $groupId
     */
    public function getRulesReferencingGroup(int $groupId): array
    {
        $contains = json_encode(['group' => ['id' => $groupId]]);
        $query = $this->db->query("SELECT id, name FROM {$this->collectionName} WHERE filterdata::jsonb @> ?::jsonb", [
            $contains,
        ]);

        return $query ? $query->result_array() : [];
    }

    /**
     * @param int $groupId
     * @param array $groupFilter The group's filter data to embed into rules
     * Removes group reference from all rules and embeds the group's filter directly
     */
    public function detachGroupFromRules(int $groupId, array $groupFilter): void
    {
        $filterJson = json_encode($groupFilter);
        $contains = json_encode(['group' => ['id' => $groupId]]);
        $this->db->query(
            "UPDATE {$this->collectionName}
             SET filterdata = (filterdata::jsonb - 'group') || jsonb_build_object('filters', ?::jsonb)
             WHERE filterdata::jsonb @> ?::jsonb",
            [$filterJson, $contains],
        );
    }

    public function _nameAlreadyExist($data)
    {
        $alreadyExist = false;
        $filter = $this->defaultFilter;
        $filter['name'] = $data['name'];
        // when ID exists (update action) filter should exclude that ID, otherwise will be false positive
        if (array_key_exists('id', $filter) && !empty($filter['id'])) {
            $filter['id != '] = $filter['id'];
            unset($filter['id']);
        }
        $result = $this->get_all_rules($filter);

        if (!empty($result)) {
            $alreadyExist = true;
        }

        return $alreadyExist;
    }

    public function _ruleOwner($data)
    {
        $isOwner = false;
        $filter = $this->defaultFilter;
        $filter['name'] = $data['name'];

        $currentItem = $this->get_all_rules($filter);

        if (!empty($currentItem)) {
            $owner = $currentItem[0]->getUsername();

            if ($owner === $data['username']) {
                $isOwner = true;
            }
        }

        return $isOwner;
    }

    //************************************
    //
    // TODO: This functions should be revieed and replaced in all project if valuable
    // right now this is a copy/paste from save_search model
    //________________________________________________________

    public function setError($errorText, $errorCode = 406, $level = 'ERROR')
    {
        $i = count($this->errors);
        $this->errors[$i]['text'] = $errorText;
        $this->errors[$i]['errorCode'] = $errorCode;
    }

    public function getErrors($level = 'ERROR')
    {
        return $this->errors;
    }

    public function clearErrors()
    {
        $this->errors = [];
    }

    public function validateCustomSql($sql)
    {
        $errors = [];
        try {
            $queryResult = $this->rest->post('/query', ['query' => $sql]);
        } catch (Exception $e) {
            $errors[] = $e->getMessage();
            return $errors;
        }
        $queryResult = json_decode($queryResult, JSON_OBJECT_AS_ARRAY);

        $isHostKey = false;
        foreach ($queryResult['data'][0]['header'] as $column) {
            if ($column['columnName'] == 'hostkey') {
                $isHostKey = true;
            }
        }
        if (!$isHostKey) {
            $errors[] = 'One of the select columns must be hostkey.';
        }
        return $errors;
    }

    public function getRuleFailHosts(
        CF_Rule $rule,
        $username,
        $includes = [],
        $excludes = [],
        $fetchDataViaAPI = true,
        $inventoryFilter = [],
        $hostFilter = [],
    ) {
        $failHosts = 0;
        $params = $this->getSQL($rule);
        $filterCondition = '';

        if (count($inventoryFilter) > 0 || count($hostFilter) > 0 || $rule->filterdata) {
            $filter = $rule->filterdata->filters->filter ?? [];
            $rulesHostFilter = $rule->filterdata->filters->hostFilter ?? [];
            $jsonPayload = [
                'query' =>
                    'SELECT hostkey FROM hosts INNER JOIN inventory_cte ON inventory_cte.filtered_hostkey = hosts.hostkey',
                'filter' => arrayDeepMerge($filter, $inventoryFilter),
                'hostFilter' => arrayDeepMerge($hostFilter, $rulesHostFilter),
            ];
            $filterParams = json_decode(json_encode($jsonPayload));
            $this->inventoryLib->extendSQLWithInventoryCTE($filterParams);
            $filterCondition = "AND hostkey IN ({$filterParams->query})";
        }

        if (!empty($params['sql'])) {
            $sql =
                $rule->conditionMustBeMet === true
                    ? "SELECT COUNT(*) FROM hosts WHERE hostkey NOT IN ({$params['sql']}) $filterCondition"
                    : "SELECT COUNT(*) FROM hosts WHERE hostkey IN ({$params['sql']}) $filterCondition";
            $result = $this->advancedreports_model->runQuery(
                $username,
                $sql,
                '',
                '',
                0,
                0,
                $includes,
                $excludes,
                $params['filter'],
                fetchDataViaAPI: $fetchDataViaAPI,
            );
            $failHosts = intval($result['rows'][0][0]);
        }

        return $failHosts;
    }

    public function getResultSQL(CF_Rule $rule, $username)
    {
        $params = $this->getSQL($rule);
        $sql = $params['sql'];
        if (sizeof($params['filter']) > 0) {
            $result = $this->advancedreports_model->runQuery(
                $username,
                $params['sql'],
                '',
                '',
                0,
                1,
                [],
                [],
                $params['filter'],
            );
            $sql = $result['query'];
        }
        return $sql;
    }

    public function getSQL(CF_Rule $rule)
    {
        $sql = '';
        $filter = [];
        switch ($rule->type) {
            case 'policy':
                $sql = $this->policyalert->getStatSqlStringForPolicy($rule);
                break;
            case 'inventory':
                $sql =
                    'SELECT hostkey FROM hosts INNER JOIN inventory_cte ON inventory_cte.filtered_hostkey = hosts.hostkey';
                $filter = $this->inventoryalert->getFilterForInventory($rule->inventoryConditions['filters']);
                break;
            case 'softwareupdate':
                $sql = $this->softwareupdatealert->getStatSQLStringForSoftwareUpdate($rule);
                break;
            case 'custom':
                $sql = $this->customalert->getStatSqlString($rule);
                break;
            case 'fileChanged':
                $sql = $this->filechangedalert->getStatSqlString($rule);
                break;
        }

        return [
            'sql' => $sql,
            'filter' => $filter,
        ];
    }

    public function generateExportId(CF_Rule $rule, $username)
    {
        $newExportId = strtolower(str_replace(' ', '-', $rule->name));

        $ruleWithSameId = $this->get_all_rules(
            $filter = ['export_id' => $newExportId, 'username' => $username],
            $offset = 0,
            $limit = 1,
            $order = ['export_id', 'DESC'],
        );

        if ($ruleWithSameId) {
            $newExportId =
                $newExportId . '-' . (intval(preg_replace('/[^0-9]/', '', $ruleWithSameId[0]->export_id)) + 1);
        }

        $sql = 'UPDATE dashboard_rules SET export_id = ? where id = ?';
        $this->db->query($sql, [$newExportId, $rule->id]);
        return $newExportId;
    }

    // END
    // ________________________________________________________
}
