<?php

use idoit\Module\Cmdb\Interfaces\CollectionInterface;
use idoit\Module\Cmdb\Interfaces\ObjectBrowserAssignedEntries;
use idoit\Module\Cmdb\Interfaces\ObjectBrowserReceiver;
use idoit\Module\Cmdb\Model\Entry\ObjectCollection;
use idoit\Module\Cmdb\Model\Entry\ObjectEntry;

/**
 * i-doit
 *
 * DAO: global category for virtual objects
 *
 * @package     i-doit
 * @subpackage  CMDB_Categories
 * @author      Dennis Stuecken <dstuecken@synetics.de>
 * @copyright   synetics GmbH
 * @license     http://www.i-doit.com/license
 */
class isys_cmdb_dao_category_g_virtual_object extends isys_cmdb_dao_category_g_virtual implements ObjectBrowserReceiver, ObjectBrowserAssignedEntries
{
    /**
     * Category's name. Will be used for the identifier, constant, main table, and many more.
     *
     * @var  string
     */
    protected $m_category = 'virtual_object';

    /**
     * Category's constant.
     *
     * @var  string
     *
     * @fixme No standard behavior!
     */
    protected $m_category_const = 'C__CATG__OBJECT';

    /**
     * Category's identifier.
     *
     * @fixme  No standard behavior!
     * @var    integer
     * This is removed, because it is done automatically in constructor of dao_category
     */
    //     protected $m_category_id = C__CATG__OBJECT;

    /**
     * Category's list DAO.
     *
     * @var  string
     */
    protected $m_list = 'isys_cmdb_dao_list_catg_object';

    /**
     * Is category multi-valued or single-valued?
     *
     * @var  boolean
     */
    protected $m_multivalued = true;

    /**
     * Flag which defines if the category is only a list with an object browser.
     *
     * @var  boolean
     */
    protected $m_object_browser_category = true;

    /**
     * Property of the object browser
     *
     * @var string
     */
    protected $m_object_browser_property = 'assigned_object';

    /**
     * Category's table.
     *
     * @var  string
     */
    protected $m_table = 'isys_catg_location_list';

    /**
     * @var string
     */
    protected $m_object_id_field = 'isys_catg_location_list__parentid';

    /**
     * @var string
     */
    protected $m_connected_object_id_field = 'isys_catg_location_list__isys_obj__id';

    /**
     * Const for logging action type
     */
    const IS_OBJECT_ADDED = 1;

    /**
     * Const for logging action type
     */
    const IS_OBJECT_REMOVED = 2;

    /**
     * Constructor.
     *
     * @param   isys_component_database $p_db
     *
     * @author  Leonard Fischer <lfischer@i-doit.com>
     */
    public function __construct(isys_component_database $p_db)
    {
        isys_component_signalcollection::get_instance()
            ->connect('mod.cmdb.afterCategoryEntrySave', [$this, 'validate_after_save'])
            ->connect('mod.cmdb.afterCreateCategoryEntry', [$this, 'validate_after_save']);

        return parent::__construct($p_db);
    }

    /**
     *
     * @param   integer $p_obj_id
     *
     * @return  integer
     */
    public function get_count($p_obj_id = null)
    {
        $l_obj_id = ($p_obj_id !== null) ? $p_obj_id : $this->m_object_id;

        $l_sql = "SELECT COUNT(isys_catg_location_list__id) AS `count`
	        FROM isys_catg_location_list
	        INNER JOIN isys_obj ON isys_obj__id =  isys_catg_location_list__parentid
	        WHERE TRUE ";

        if (!empty($l_obj_id)) {
            $l_sql .= " AND isys_catg_location_list__parentid = " . $this->convert_sql_id($l_obj_id);
        }

        $l_sql .= " AND isys_obj__status = " . $this->convert_sql_int(C__RECORD_STATUS__NORMAL) . ';';

        return (int)current($this->retrieve($l_sql)
            ->get_row());
    }

    /**
     * @param null   $p_catg_list_id
     * @param null   $p_obj_id
     * @param string $p_condition
     * @param null   $p_filter
     * @param null   $p_status
     *
     * @return isys_component_dao_result
     * @throws isys_exception_database
     */
    public function get_data($p_catg_list_id = null, $p_obj_id = null, $p_condition = "", $p_filter = null, $p_status = null)
    {
        $l_sql = "SELECT *, ST_AsText(isys_catg_location_list__gps) AS isys_catg_location_list__gps, ST_X(isys_catg_location_list__gps) AS latitude, ST_Y(isys_catg_location_list__gps) AS longitude FROM isys_catg_location_list
			INNER JOIN isys_obj ON isys_obj__id =  isys_catg_location_list__isys_obj__id
			INNER JOIN isys_obj_type ON isys_obj__isys_obj_type__id = isys_obj_type__id
			WHERE TRUE
			AND isys_obj_type__show_in_tree = 1 " . $p_condition . $this->prepare_filter($p_filter);

        if ($p_obj_id !== null) {
            $l_sql .= $this->get_object_condition($p_obj_id);
        }

        if ($p_status !== null) {
            $l_sql .= " AND isys_obj__status = " . $this->convert_sql_int($p_status);
        }

        if ($p_catg_list_id !== null) {
            $l_sql .= " AND isys_catg_location_list__id = " . $this->convert_sql_id($p_catg_list_id);
        }

        return $this->retrieve($l_sql . ";");
    }

    /**
     * Creates the condition to the object table
     *
     * @param   mixed $p_obj_id
     *
     * @return  string
     * @author  Van Quyen Hoang <qhoang@i-doit.de>
     */
    public function get_object_condition($p_obj_id = null, $p_alias = 'isys_obj')
    {
        if (!empty($p_obj_id)) {
            if (is_array($p_obj_id)) {
                return ' AND (isys_catg_location_list__parentid ' . $this->prepare_in_condition($p_obj_id) . ') ';
            } else {
                return ' AND (isys_catg_location_list__parentid = ' . $this->convert_sql_id($p_obj_id) . ') ';
            }
        }

        return '';
    }

    /**
     * Method for returning the properties.
     *
     * @return  array
     */
    protected function properties()
    {
        return [
            'assigned_object' => array_replace_recursive(isys_cmdb_dao_category_pattern::object_browser(), [
                C__PROPERTY__INFO     => [
                    C__PROPERTY__INFO__TITLE       => 'LC__CMDB__LOGBOOK__SOURCE__OBJECT',
                    C__PROPERTY__INFO__DESCRIPTION => 'Assigned Object',
                    C__PROPERTY__INFO__BACKWARD_PROPERTY => 'isys_cmdb_dao_category_g_location::parent',
                ],
                C__PROPERTY__DATA     => [
                    C__PROPERTY__DATA__FIELD  => 'isys_catg_location_list__isys_obj__id',
                    C__PROPERTY__DATA__SELECT => idoit\Module\Report\SqlQuery\Structure\SelectSubSelect::factory(
                        'SELECT CONCAT(isys_obj__title, \' {\', isys_obj__id, \'}\')
                                FROM isys_catg_location_list
                                INNER JOIN isys_obj ON isys_obj__id = isys_catg_location_list__isys_obj__id',
                        'isys_catg_location_list',
                        'isys_catg_location_list__id',
                        'isys_catg_location_list__parentid',
                        '',
                        '',
                        idoit\Module\Report\SqlQuery\Structure\SelectCondition::factory(['isys_obj__status = ' . C__RECORD_STATUS__NORMAL]),
                        idoit\Module\Report\SqlQuery\Structure\SelectGroupBy::factory(['isys_catg_location_list__parentid']),
                        'isys_catg_location_list__isys_obj__id'
                    ),
                    C__PROPERTY__DATA__INDEX  => true
                ],
                C__PROPERTY__UI       => [
                    C__PROPERTY__UI__ID     => 'C__CATG__VIRTUAL_OBJECT__ASSIGNED_OBJECTS',
                    C__PROPERTY__UI__PARAMS => [
                        'multiselection' => true,
                        isys_popup_browser_object_ng::C__DATARETRIEVAL => new isys_callback([
                            'isys_cmdb_dao_category_g_virtual_object',
                            ''
                        ])
                    ]
                ],
                C__PROPERTY__PROVIDES => [
                    // C__PROPERTY__PROVIDES__VIRTUAL    => true, // @see ID-11956
                    C__PROPERTY__PROVIDES__IMPORT     => true,
                    C__PROPERTY__PROVIDES__EXPORT     => true,
                    C__PROPERTY__PROVIDES__MULTIEDIT  => true,
                    C__PROPERTY__PROVIDES__SEARCH     => false,
                    C__PROPERTY__PROVIDES__VALIDATION => false
                ],
                C__PROPERTY__FORMAT   => [
                    C__PROPERTY__FORMAT__CALLBACK => [
                        'isys_export_helper',
                        'object'
                    ]
                ]
            ])
        ];
    }

    public function rank_records($p_objects, $p_direction = C__CMDB__RANK__DIRECTION_DELETE, $p_table = "isys_obj", $p_checkMethod = null, $p_purge = false)
    {
        $handleLocationChanges = isys_tenantsettings::get('cmdb.chassis.handle-location-changes', false);
        $l_dao_relation = new isys_cmdb_dao_category_g_relation($this->m_db);
        $l_dao = new isys_cmdb_dao_location($this->m_db);

        foreach ($p_objects as $l_del) {
            $l_data = $this->get_data($l_del)->__to_array();
            $l_dao->detach($this->get_object_id_by_category_id($l_del, "isys_catg_location_list"));

            $l_dao_relation->delete_relation($l_data["isys_catg_location_list__isys_catg_relation_list__id"]);

            // @see ID-4974
            if ($handleLocationChanges) {
                $chassisDao = isys_cmdb_dao_category_s_chassis::instance($this->m_db);
                $result = $chassisDao->get_slots_by_assiged_object($l_data["isys_obj__id"]);

                while ($row = $result->get_row()) {
                    $chassisDao->relations_remove($row['isys_cats_chassis_list__id'], null, $l_data["isys_obj__id"]);
                }
            }
        }

        $l_dao->save();

        return true;
    }

    /**
     * Sync method.
     *
     * @param   array   $p_category_data
     * @param   integer $p_object_id
     * @param   integer $p_status
     *
     * @return  mixed
     */
    public function sync($p_category_data, $p_object_id, $p_status = 1 /* isys_import_handler_cmdb::C__CREATE */)
    {
        /* @var  $l_dao_location  isys_cmdb_dao_category_g_location */
        $l_dao_location = isys_cmdb_dao_category_g_location::instance($this->m_db);

        $l_parentid = $l_dao_location->get_parent_id_by_object($p_object_id);
        if ($l_parentid === false) {
            // Do not assign childs because the container object does not have a parent
            return true;
        }

        if (isys_import_handler_cmdb::C__CREATE) {
            $l_connected_object = $p_category_data[isys_import_handler_cmdb::C__PROPERTIES]['assigned_object'][C__DATA__VALUE];
            if (is_array($l_connected_object)) {
                $output = $this->attachObjects($p_object_id, $l_connected_object, true);
                return count(array_filter($output)) === count($output);
            }

            $l_sql = "SELECT isys_catg_location_list__id, isys_catg_location_list__parentid
							FROM isys_catg_location_list
							WHERE isys_catg_location_list__isys_obj__id = " . $this->convert_sql_id($l_connected_object) . ";";

            $l_row = $this->retrieve($l_sql)
                ->get_row();

            if (empty($l_row)) {
                $l_dao_location->create($l_connected_object, $p_object_id);
            } else {
                // We only need to save the data, if the object is beeing assigned newly.
                if ($l_row['isys_catg_location_list__parentid'] != $p_object_id) {
                    $l_dao_location->save($l_row["isys_catg_location_list__id"], $l_connected_object, $p_object_id);
                }

                return $l_row['isys_catg_location_list__id'];
            }
        }

        return true;
    }

    /**
     * This method gets called, after "category save" by signal-slot-module.
     *
     * @throws  RuntimeException
     * @author  Leonard Fischer <lfischer@i-doit.com>
     */
    public function validate_after_save()
    {
        global $g_comp_database;

        /**
         * @var $l_dao isys_cmdb_dao_category_g_location
         */
        $l_dao = isys_cmdb_dao_category_g_location::instance($g_comp_database);
        $l_obj_id = $_GET[C__CMDB__GET__OBJECT];

        try {
            // Using "$p_dao" instead of "$this", because this gets called by "call_user_func" (which works statically).
            $l_dao->get_location_path($l_obj_id);
        } catch (RuntimeException $e) {
            // The saved parent location produces a recursion - We set the parent to NULL.
            $l_dao->reset_location($l_obj_id);

            // And now we throw the exception further towards the action handler.
            isys_application::instance()->container->get('notify')->error($e->getMessage());
        }
    }

    /**
     * Gets assigned objects.
     *
     * @param   integer $p_obj_id
     * @param   boolean $p_as_array
     *
     * @return  mixed
     */
    public function get_assigned_objects($p_obj_id, $p_as_array = false)
    {
        $l_sql = 'SELECT *
			FROM isys_catg_location_list
			INNER JOIN isys_obj ON isys_obj__id = isys_catg_location_list__isys_obj__id
			WHERE isys_catg_location_list__parentid = ' . $this->convert_sql_id($p_obj_id) . ';';

        $l_res = $this->retrieve($l_sql);

        if ($p_as_array === false) {
            return $l_res;
        } else {
            $l_arr = [];

            while ($l_row = $l_res->get_row()) {
                $l_arr[] = $l_row['isys_obj__id'];
            }

            return $l_arr;
        }
    }

    /**
     * @param int   $objectId
     * @param array $selectedObjects
     * @param bool  $withOutput
     *
     * @return array|bool
     * @throws isys_exception_dao
     * @throws isys_exception_dao_cmdb
     * @throws isys_exception_database
     */
    public function attachObjects($objectId, array $selectedObjects, $withOutput = false)
    {
        $l_location_dao = new isys_cmdb_dao_category_g_location($this->m_db);

        // Bugfix: Sometimes "get_assigned_objects" returns no array.
        $l_assigned_objects_plain = (array)$this->get_assigned_objects($objectId, true);
        $l_assigned_objects = array_flip($l_assigned_objects_plain);

        $l_auth_obj = isys_auth_cmdb::instance();
        $l_not_allowed_objects = [];

        $validationIssues = [];

        // logging newly assigned servers
        $newChildObjects = array_diff($selectedObjects, $l_assigned_objects_plain);

        foreach ($newChildObjects as $newChildObjectID) {
            // @see ID-10358 Check for recursion, by running the validation of 'Location' category.
            $validation = isys_cmdb_dao_category_g_location::instance($this->m_db)->validate([
                'object' => [
                    C__DATA__VALUE => $newChildObjectID
                ],
                'parent' => [
                    C__DATA__VALUE => $objectId
                ]]);

            if (is_array($validation)) {
                // Remove the affected object from the list of objects.
                $selectedObjects = array_filter($selectedObjects, fn ($object) => $object != $newChildObjectID);
                $validationIssues[] = $this->get_obj_name_by_id_as_string($newChildObjectID);
            }
        }

        if (count($validationIssues)) {
            $language = isys_application::instance()->container->get('language');

            if (count($validationIssues) === 1) {
                $validationMessage = $language->get('LC__CATG__LOCATION__VALIDATION__RECURSION_ONE', implode('', $validationIssues));
            } else {
                $validationMessage = $language->get('LC__CATG__LOCATION__VALIDATION__RECURSION_MANY', '"' . implode('", "', $validationIssues) . '"');
            }

            isys_notify::error($language->get('LC__VALIDATION_ERROR') . ' ' . $validationMessage, ['sticky' => true]);
        }

        $return = [];

        foreach ($selectedObjects as $l_popup_object) {
            $l_allowed_to_assign = $l_auth_obj->is_allowed_to(isys_auth::EDIT, 'OBJ_ID/' . $l_popup_object);

            // @see ID-5440 / Zendesk #3176 Prevent object relations from beeing deleted, when no right can be found.
            if (isset($l_assigned_objects[$l_popup_object])) {
                unset($l_assigned_objects[$l_popup_object]);
            }

            if ($l_popup_object != $objectId && $l_allowed_to_assign) {
                $l_sql = "SELECT isys_catg_location_list__id, isys_catg_location_list__parentid
					FROM isys_catg_location_list
					WHERE isys_catg_location_list__isys_obj__id = " . $this->convert_sql_id($l_popup_object) . ";";

                $l_row = $this->retrieve($l_sql)
                    ->get_row();

                if (!$l_row) {
                    $return[] = $l_location_dao->create($l_popup_object, $objectId);
                } else {
                    // We only need to save the data, if the object is beeing assigned newly.
                    if ($l_row['isys_catg_location_list__parentid'] != $objectId) {
                        if ($l_location_dao->save($l_row["isys_catg_location_list__id"], $l_popup_object, $objectId)) {
                            $return[] = $l_row["isys_catg_location_list__id"];
                        }
                    }
                }
            } else {
                if (!$l_allowed_to_assign) {
                    $l_not_allowed_objects[$l_location_dao->get_obj_type_name_by_obj_id($l_popup_object)] = true;
                } else {
                    isys_component_template_infobox::instance()
                        ->set_message('Error: Attaching own object is prohibitted.', 1, defined_or_default('C__LOGBOOK__ALERT_LEVEL__3'));
                }
            }
        }

        if (count($l_not_allowed_objects) > 0) {
            $l_message = 'Error: No rights to assign objects from object type (';
            foreach ($l_not_allowed_objects as $l_objtype_name => $l_bool) {
                $l_message .= isys_application::instance()->container->get('language')
                        ->get($l_objtype_name) . ', ';
            }
            $l_message = rtrim($l_message, ', ') . ').';
            isys_component_template_infobox::instance()
                ->set_message($l_message, 1, defined_or_default('C__LOGBOOK__ALERT_LEVEL__3'));
        }

        if (count($l_assigned_objects) > 0) {
            $handleLocationChanges = isys_tenantsettings::get('cmdb.chassis.handle-location-changes', false);

            // Now we save the objects, which were newly assigned to this object.
            foreach ($l_assigned_objects as $l_delObj => $l_val) {
                $l_sql = "SELECT isys_catg_location_list__id
					FROM isys_catg_location_list
					WHERE isys_catg_location_list__isys_obj__id = " . $this->convert_sql_id($l_delObj) . ";";

                $l_res = $this->retrieve($l_sql);
                if ($l_res->num_rows() > 0) {
                    $l_loc_id = $l_res->get_row_value('isys_catg_location_list__id');

                    $l_location_dao->save($l_loc_id, $l_delObj, null);

                    // @see  ID-4974 When saving the category, we check if the location changes need to be handled.
                    if ($handleLocationChanges) {
                        $chassisDao = isys_cmdb_dao_category_s_chassis::instance($this->m_db);
                        $result = $chassisDao->get_slots_by_assiged_object($l_delObj);

                        while ($row = $result->get_row()) {
                            $chassisDao->relations_remove($row['isys_cats_chassis_list__id'], null, $l_delObj);
                        }
                    }
                }
                // logging removed servers from a rack
                $this->logbookEntryForServer($l_delObj, self::IS_OBJECT_REMOVED);
            }
        }

        return ($withOutput === true ? $return: true);
    }

    /**
     * @param int $objectID
     * @param int $type
     *
     * @throws isys_exception_dao
     * @throws isys_exception_database
     * @deprecated Generic logbook is now handling it
     */
    public function logbookEntryForServer(int $objectID, int $type)
    {
        return;
    }

    /**
     * @param int|int[] $id
     * @param string    $tag
     * @param false     $asId
     *
     * @return CollectionInterface
     * @throws Exception
     */
    public function getAttachedEntries($id, $tag = '', $asId = false): CollectionInterface
    {
        $collection = new ObjectCollection();

        if (is_numeric($id)) {
            $id = [$id];
        }

        if (empty($id)) {
            return $collection;
        }

        $query = 'SELECT isys_catg_location_list__id as id, isys_obj__id as objId, isys_obj__title as title, isys_obj__id as sysid, isys_obj_type__title as objType
			FROM isys_catg_location_list
			INNER JOIN isys_obj ON isys_obj__id = isys_catg_location_list__isys_obj__id
            INNER JOIN isys_obj_type ON isys_obj_type__id = isys_obj__isys_obj_type__id
			WHERE isys_catg_location_list__parentid IN (' . implode(', ', array_map([$this, 'convert_sql_int'], $id)) . ');';

        $result = $this->retrieve($query);

        while ($row = $result->get_row()) {
            $collection->addEntry(ObjectEntry::factory(
                $row['objId'],
                $row['title'],
                $row['sysid'],
                isys_application::instance()->container->get('language')->get($row['objType']),
                $row
            ));
        }

        return $collection;
    }

    /**
     * @param isys_request $request
     *
     * @return array|mixed
     */
    public function getEntriesByRequestObject(isys_request $request)
    {
        return $this->get_assigned_objects($request->get_object_id(), true);
    }
}
