<?php
/**
 * SugarCRM Community Edition is a customer relationship management program developed by
 * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
 *
 * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd.
 * Copyright (C) 2011 - 2021 SalesAgility Ltd.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by the
 * Free Software Foundation with the addition of the following permission added
 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * 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 Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along with
 * this program; if not, see http://www.gnu.org/licenses or write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 *
 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
 * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
 * these Appropriate Legal Notices must retain the display of the "Powered by
 * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not
 * reasonably feasible for technical reasons, the Appropriate Legal Notices must
 * display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM".
 */

namespace SuiteCRM\Search;

use BeanFactory;
use LoggerManager;
use SugarBean;
use SuiteCRM\Exception\Exception;
use SuiteCRM\Exception\InvalidArgumentException;

if (!defined('sugarEntry') || !sugarEntry) {
    die('Not A Valid Entry Point');
}

/**
 * Class SearchResults holds results and metadata of a search.
 *
 * @author Vittorio Iocolano
 */
class SearchResults
{
    /** @var array Contains the results ids */
    private $hits;
    /** @var array Contains the scores of each hit. This should match in structure with $hits */
    private $scores;
    /** @var array Similar to scores, but customisable by the search engine */
    private $options;
    /** @var float The number of seconds it took to perform the search */
    private $searchTime;
    /** @var int The total number of hits (without pagination) */
    private $total;
    /** @var bool Flag specifying if the (nested) hits are grouped by the modules */
    private $groupedByModule;

    /**
     * SearchResults constructor.
     *
     * @param array $hits Contains the results ids
     * @param bool $groupedByModule Flag specifying if the (nested) hits are grouped by the modules
     * @param float|null $searchTime The number of seconds it took to perform the search
     * @param int|null $total The number of total hits (without pagination)
     * @param array|null $scores Contains the scores of each hit. This should match in structure with $hits
     * @param array|null $options Similar to scores, but customisable by the search engine
     * @throws InvalidArgumentException
     */
    public function __construct(
        array $hits,
        $groupedByModule = true,
        float $searchTime = null,
        int $total = null,
        array $scores = null,
        array $options = null
    ) {
        $this->hits = $hits;
        $this->scores = $scores;
        $this->options = $options;
        $this->groupedByModule = $groupedByModule;
        $this->searchTime = $searchTime;
        $this->total = $total;

        if ($this->scores !== null && count($hits) !== count($scores)) {
            throw new InvalidArgumentException('The sizes of $hits and $scores must match.');
        }
    }

    /**
     * Returns the results.
     *
     * Note that the structure of the results depends whether of not they have been nested by module or not.
     *
     * @return array
     */
    public function getHits(): array
    {
        return $this->hits;
    }

    /**
     * Fetches the results (originally just module->id) as Beans.
     *
     * @return array
     * @throws Exception
     * @see getHits()
     */
    public function getHitsAsBeans(): array
    {
        $searchHits = $this->hits;
        $parsed = [];

        foreach ($searchHits as $module => $beans) {
            foreach ((array)$beans as $bean) {
                $obj = BeanFactory::getBean($module, $bean);

                // if a search found a bean but suitecrm does not, it could happens
                // maybe the bean is deleted but elsasticsearch is not re-indexing yet.
                // so at this point we trying to rebuild the index and try again to get bean:
                if (!$obj) {
                    ElasticSearch\ElasticSearchIndexer::repairElasticsearchIndex();
                    $obj = BeanFactory::getBean($module, $bean);
                }

                if (!$obj) {
                    throw new Exception('Error retrieving bean: ' . $module . ' [' . $bean . ']');
                }

                $obj->load_relationships();
                $fieldDefs = $obj->getFieldDefinitions();
                $parsed[$module][] = $this->updateFieldDefLinks($obj, $fieldDefs);
            }
        }

        return $parsed;
    }

    /**
     *
     * @param SugarBean $obj
     * @param array $fieldDefs
     * @return SugarBean
     */
    protected function updateFieldDefLinks(SugarBean $obj, array $fieldDefs): SugarBean
    {
        foreach ($fieldDefs as $fieldDef) {
            if (isset($obj->{$fieldDef['name']})) {
                $obj = $this->updateObjLinks($obj, $fieldDef);
            }
        }

        return $obj;
    }

    /**
     * Update related links in a bean to show it on results page
     *
     * @param SugarBean $obj
     * @param array $fieldDef
     * @return SugarBean
     */
    protected function updateObjLinks(SugarBean $obj, array $fieldDef): SugarBean
    {
        if (isset($fieldDef['link']) && !empty($fieldDef['id_name']) && $fieldDef['type'] === 'relate') {
            $relId = $this->getRelatedId($obj, $fieldDef['id_name'], $fieldDef['link']);
            if (!empty($relId)) {
                $obj->{$fieldDef['name']} = $this->getLink(
                    $obj->{$fieldDef['name']},
                    $fieldDef['module'],
                    $relId,
                    'DetailView'
                );
            }
        } elseif ($fieldDef['name'] === 'name') {
            $obj->{$fieldDef['name']} = $this->getLink(
                $obj->{$fieldDef['name']},
                $obj->module_name,
                $obj->id,
                'DetailView'
            );
        }

        return $obj;
    }

    /**
     * resolve related record ID
     *
     * @param SugarBean $obj
     * @param string $idName
     * @param string $link
     * @return null|string
     */
    protected function getRelatedId(SugarBean $obj, string $idName, string $link): string
    {
        $relField = $idName;
        if (isset($obj->$link)) {
            $relId = $obj->$link->getFocus()->$relField;
            if (is_object($relId)) {
                if (method_exists($relId, "getFocus")) {
                    $relId = $relId->getFocus()->id;
                } else {
                    $relId = null;
                }
            }
        } elseif (isset($obj->$relField)) {
            $relId = $obj->$relField;
        } else {
            $relId = null;
            LoggerManager::getLogger()->warn('Unresolved related ID for field: ' . $relField);
        }

        return $relId;
    }

    /**
     *
     * @param string $label
     * @param string $module
     * @param string $record
     * @param string $action
     * @return string
     * @global array $sugar_config
     */
    protected function getLink(string $label, string $module, string $record, string $action): string
    {
        global $sugar_config;

        return "<a href=\"{$sugar_config['site_url']}/index.php?action={$action}&module={$module}&record={$record}&offset=1\"><span>{$label}</span></a>";
    }

    /**
     * Returns an arbitrary scores defining how much related a hit is to the query.
     *
     * @return array
     */
    public function getScores(): ?array
    {
        return $this->scores;
    }

    /**
     * Returns the total number of hits (without pagination).
     *
     * @return int
     */
    public function getTotal(): ?int
    {
        return $this->total;
    }

    /**
     * @return array
     */
    public function getOptions(): ?array
    {
        return $this->options;
    }

    /**
     * @param string $key
     *
     * @return array
     */
    public function getOption(string $key): array
    {
        return $this->options[$key];
    }

    /**
     * Time in seconds it took to perform the search.
     *
     * @return float
     */
    public function getSearchTime(): ?float
    {
        return $this->searchTime;
    }

    /**
     * @return bool
     */
    public function isGroupedByModule(): bool
    {
        return $this->groupedByModule;
    }
}
