<?php

namespace idoit\Module\Pro\Algorithm\Algorithm;

use idoit\Module\Pro\Algorithm\Filter;
use idoit\Module\Pro\Algorithm\Formatter\FormatterInterface;
use isys_cmdb_dao;
use isys_tree_node_explorer;

/**
 * Class TreeAlgorithm
 *
 * @package idoit\Module\Pro\Algorithm\Algorithm
 */
class TreeAlgorithm
{
    const RELATION_TYPE_DIRECT  = 'direct';
    const RELATION_TYPE_COMPLEX = 'complex';

    /**
     * @var isys_tree_node_explorer[]
     */
    private $nodes = [];

    /**
     * @var FormatterInterface
     */
    private $formatter;

    /**
     * @var Filter
     */
    private $filter;

    /**
     * @var isys_cmdb_dao
     */
    private $dao;

    /**
     * @var string
     */
    private $authCondition;

    /**
     * @var bool
     */
    private $skipAlreadyIteratedConnections = false;

    /**
     * TreeAlgorithm constructor.
     *
     * @param FormatterInterface $formatter
     * @param Filter             $filter
     * @param isys_cmdb_dao      $dao
     */
    public function __construct(FormatterInterface $formatter, Filter $filter, isys_cmdb_dao $dao)
    {
        $this->formatter = $formatter;
        $this->filter = $filter;
        $this->dao = $dao;
    }

    /**
     * @param bool $skip
     *
     * @return $this
     */
    public function skipAlreadyIteratedConnections(bool $skip): self
    {
        $this->skipAlreadyIteratedConnections = $skip;

        return $this;
    }

    /**
     * @param isys_tree_node_explorer $node
     * @param int                     $level
     * @param bool                    $byMaster
     *
     * @return void
     */
    public function iterateNode(isys_tree_node_explorer $node, int $level, bool $byMaster)
    {
        if ($level > 0) {
            $level--;
        } else {
            return;
        }

        $parent = $node->get_parent();
        $parentId = 0;

        if (isset($parent) && $parent instanceof isys_tree_node_explorer) {
            $parentId = $parent->get_id();
        }

        $nodeId = $node->get_id();

        $relations = $this->getRelations($nodeId, $byMaster);

        foreach ($relations as $row) {
            $source = (int)($byMaster ? $row['masterObjectId'] : $row['slaveObjectId']);
            $target = (int)($byMaster ? $row['slaveObjectId'] : $row['masterObjectId']);
            $relationObject = (int)$row['relationObjectId'];
            $relationTypeId = (int)$row['relationTypeId'];

            $key =  ':' . $level . ':' . $parentId . '>' . $source . '>' . $relationTypeId . '>' . $target;
            $doubling = isset($this->nodes[$key]);

            // @see  ID-7370  New logic to optionally skip already iterated relations.
            if ($this->skipAlreadyIteratedConnections && isset($this->nodes[$key])) {
                continue;
            }

            // render the node according to its relation type
            $this->nodes[$key] = $row['relationType'] === self::RELATION_TYPE_DIRECT ? $this->formatter->formatNode(
                $target,
                $row,
                $doubling
            ) : $this->formatter->formatNode($relationObject, [], $doubling);

            $this->nodes[$key]->set_parent($node);

            if (!$doubling) {
                $this->iterateNode($this->nodes[$key], $level, $byMaster);
            }

            // Add the node.
            $node->add($this->nodes[$key]);
        }
    }

    /**
     * @param string $authCondition
     *
     * @return $this
     */
    public function setAuthCondition(string $authCondition): self
    {
        $this->authCondition = $authCondition;

        return $this;
    }

    /**
     * @param int  $objectId
     * @param bool $byMaster
     *
     * @return array
     * @throws \isys_exception_database
     */
    private function getRelations(int $objectId, bool $byMaster): array
    {
        $directionField = 'isys_catg_relation_list__isys_obj__id__' . ($byMaster ? 'master' : 'slave');
        $statusNormal = $this->dao->convert_sql_int(C__RECORD_STATUS__NORMAL);

        // Query to select all relations of the given object. This query has been snagged modified from `isys_cmdb_dao_list_catg_relation->get_result()`.
        $query = "SELECT 
            isys_catg_relation_list__id AS id, 
            isys_catg_relation_list__isys_obj__id AS objectId,
            EXISTS(
                SELECT 1
                    FROM isys_catg_relation_list sub
                    WHERE {$directionField} = main.isys_catg_relation_list__isys_obj__id
                LIMIT 1
            ) as complex
        FROM isys_catg_relation_list main
        WHERE {$directionField} = :objectId
            AND isys_catg_relation_list__status = {$statusNormal}
            AND EXISTS(SELECT 1 FROM isys_obj WHERE isys_obj__id = isys_catg_relation_list__isys_obj__id__slave AND isys_obj__status = {$statusNormal})
            AND EXISTS(SELECT 1 FROM isys_obj WHERE isys_obj__id = isys_catg_relation_list__isys_obj__id__master AND isys_obj__status = {$statusNormal})";

        // 1. Get relations from the current object
        $result = $this->dao->retrieve(str_replace(':objectId', $objectId, $query));

        // 2. Find out, if relation itself has the relations
        $direct = [];
        $complex = [];

        while ($row = $result->get_row()) {
            if ($row['complex'] === '0') {
                // Direct connection because no other relations depend on this relation.
                $direct[] = $row['id'];
            } else {
                // Complex connection because other relations depend on this relation.
                $complex[] = $row['id'];
            }
        }

        // 3. Get the relations with all needed data and types
        $relations = $this->fetchRelations($direct, $complex, $byMaster);

        // 4. if current object is a relation object - add into the relations also the "virtual" relations from master to relation object and from relation object to slave
        $catsRelation = $this->dao->retrieve('SELECT isys_catg_relation_list__id FROM isys_catg_relation_list WHERE isys_catg_relation_list__isys_obj__id = ' . $objectId)
            ->get_row();

        if (!empty($catsRelation)) {
            $ownRelations = $this->fetchRelations([$catsRelation['isys_catg_relation_list__id']], [], $byMaster);

            foreach ($ownRelations as $relation) {
                // create a virtual relations between master/slave and relation object
                $relation[$byMaster ? 'masterObjectId' : 'slaveObjectId'] = $objectId;
                $relations[] = $relation;
            }
        }

        return $relations;
    }

    /**
     * Load all the needed relation data
     *
     * @param array $direct
     * @param array $complex
     * @param bool  $byMaster
     *
     * @return array
     *
     * @throws \isys_exception_database
     */
    protected function fetchRelations(array $direct, array $complex, bool $byMaster): array
    {
        $cmdbStatusFilter = $this->filter->getCmdbStatus();

        if (\defined('C__CMDB_STATUS__IDOIT_STATUS')) {
            $cmdbStatusFilter[] = \constant('C__CMDB_STATUS__IDOIT_STATUS');
        }
        if (\defined('C__CMDB_STATUS__IDOIT_STATUS_TEMPLATE')) {
            $cmdbStatusFilter[] = \constant('C__CMDB_STATUS__IDOIT_STATUS_TEMPLATE');
        }

        $statusNormal = $this->dao->convert_sql_int(C__RECORD_STATUS__NORMAL);
        $notInCmdbStatus = $this->dao->prepare_in_condition($cmdbStatusFilter, true);
        $relationColumn = 'isys_catg_relation_list__isys_obj__id__' . ($byMaster ? 'slave' : 'master');

        if (!empty($direct) && !empty($complex)) {
            $relationTypeSelect = "CASE
                WHEN isys_catg_relation_list__id IN (" . implode(',', $direct) . ") THEN '" . self::RELATION_TYPE_DIRECT . "' 
                WHEN isys_catg_relation_list__id IN (" . implode(',', $complex) . ") THEN '" . self::RELATION_TYPE_COMPLEX . "'
                END AS relationType ";
        } elseif (empty($direct)) {
            $relationTypeSelect = "'" . self::RELATION_TYPE_COMPLEX . "' AS relationType";
        } elseif (empty($complex)) {
            $relationTypeSelect = "'" . self::RELATION_TYPE_DIRECT . "' AS relationType";
        }

        $combined = array_merge($direct, $complex);

        // If we get no relation entries, select nothing.
        if (empty($combined)) {
            return [];
        }

        $combined = implode(',', $combined);

        // Query to fetch all "connected" objects of a given object by its relations.
        $query = "SELECT
            ms.isys_obj__id AS objectId,
            ms.isys_obj__title AS objectTitle,
            isys_obj_type__id AS objectTypeId,
            isys_obj_type__color AS objectTypeColor,
            CASE isys_obj_type__const
                WHEN 'C__OBJTYPE__RELATION' THEN isys_relation_type__title
                ELSE isys_obj_type__title
            END AS objectTypeTitle,
            isys_catg_relation_list__isys_obj__id AS relationObjectId,
            isys_catg_relation_list__isys_relation_type__id AS relationTypeId,
            isys_catg_relation_list__isys_obj__id__slave AS slaveObjectId,
            isys_catg_relation_list__isys_obj__id__master AS masterObjectId,
            {$relationTypeSelect}
            FROM isys_catg_relation_list
            INNER JOIN isys_obj AS relation ON relation.isys_obj__id = isys_catg_relation_list__isys_obj__id
            INNER JOIN isys_obj AS ms ON ms.isys_obj__id = {$relationColumn}
            INNER JOIN isys_obj_type ON ms.isys_obj__isys_obj_type__id = isys_obj_type__id
            INNER JOIN isys_relation_type ON isys_relation_type__id = isys_catg_relation_list__isys_relation_type__id
            WHERE TRUE
            {$this->authCondition}
            AND isys_catg_relation_list__status = {$statusNormal}
            AND relation.isys_obj__status = {$statusNormal}
            AND relation.isys_obj__isys_cmdb_status__id {$notInCmdbStatus}
            AND isys_catg_relation_list__isys_obj__id__slave != isys_catg_relation_list__isys_obj__id__master
            AND ms.isys_obj__status = {$statusNormal}
            AND ms.isys_obj__isys_cmdb_status__id {$notInCmdbStatus}
            AND isys_catg_relation_list__id IN ({$combined})";

        if ($this->filter->getPriority() > 0) {
            $query .= ' AND isys_catg_relation_list__isys_weighting__id < ' . $this->dao->convert_sql_id($this->filter->getPriority());
        }

        if (\count($this->filter->getRelationTypes())) {
            $query .= ' AND isys_catg_relation_list__isys_relation_type__id ' . $this->dao->prepare_in_condition($this->filter->getRelationTypes(), true);
        }

        if (\count($this->filter->getObjectTypes())) {
            $query .= ' AND ms.isys_obj__isys_obj_type__id ' . $this->dao->prepare_in_condition($this->filter->getObjectTypes(), true);
        }

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

        $relations = [];

        while ($row = $result->get_row()) {
            $relations[] = $row;
        }

        return $relations;
    }
}
