<?php

/**
 * The phpMyFAQ Search class.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
 * obtain one at http://mozilla.org/MPL/2.0/.
 *
 * @package   phpMyFAQ
 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
 * @author    Matteo Scaramuccia <matteo@scaramuccia.com>
 * @author    Adrianna Musiol <musiol@imageaccess.de>
 * @copyright 2008-2020 phpMyFAQ Team
 * @license   http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
 * @link      https://www.phpmyfaq.de
 * @since     2008-01-26
 */

namespace phpMyFAQ;

use DateTime;
use phpMyFAQ\Search\Elasticsearch;
use phpMyFAQ\Search\SearchFactory;

/**
 * Class Search
 *
 * @package phpMyFAQ
 */
class Search
{
    /**
     * @var Configuration
     */
    private $config;

    /**
     * Entity ID.
     *
     * @var int
     */
    private $categoryId = null;

    /**
     * Entity object.
     *
     * @var Category
     */
    private $category = null;

    /**
     * Search table.
     *
     * @var string
     */
    private $table = null;

    /**
     * Constructor.
     *
     * @param Configuration $config
     */
    public function __construct(Configuration $config)
    {
        $this->config = $config;
        $this->table = Database::getTablePrefix() . 'faqsearches';
    }

    /**
     * Setter for category.
     *
     * @param int $categoryId Entity ID
     */
    public function setCategoryId($categoryId)
    {
        $this->categoryId = (int)$categoryId;
    }

    /**
     * Getter for category.
     *
     * @return int
     */
    public function getCategoryId()
    {
        return $this->categoryId;
    }

    /**
     * The search function to handle the different search engines.
     *
     * @param string $searchTerm   Text/Number (solution id)
     * @param bool   $allLanguages true to search over all languages
     * @return array
     */
    public function search($searchTerm, $allLanguages = true)
    {
        if (is_numeric($searchTerm)) {
            return $this->searchDatabase($searchTerm, $allLanguages);
        }
        if ($this->config->get('search.enableElasticsearch')) {
            return $this->searchElasticsearch($searchTerm, $allLanguages);
        } else {
            return $this->searchDatabase($searchTerm, $allLanguages);
        }
    }

    /**
     * The auto complete function to handle the different search engines.
     *
     * @param string $searchTerm Text to auto complete
     * @return array
     */
    public function autoComplete($searchTerm)
    {
        if ($this->config->get('search.enableElasticsearch')) {
            $esSearch = new Elasticsearch($this->config);
            $allCategories = $this->getCategory()->getAllCategoryIds();

            $esSearch->setCategoryIds($allCategories);
            $esSearch->setLanguage($this->config->getLanguage()->getLanguage());

            return $esSearch->autoComplete($searchTerm);
        } else {
            return $this->searchDatabase($searchTerm, false);
        }
    }

    /**
     * The search function for the database powered full text search.
     *
     * @param string $searchTerm   Text/Number (solution id)
     * @param bool   $allLanguages true to search over all languages
     * @return array
     */
    public function searchDatabase($searchTerm, $allLanguages = true)
    {
        $fdTable = Database::getTablePrefix() . 'faqdata AS fd';
        $fcrTable = Database::getTablePrefix() . 'faqcategoryrelations';
        $condition = ['fd.active' => "'yes'"];
        $search = SearchFactory::create($this->config, ['database' => Database::getType()]);

        if (!is_null($this->getCategoryId()) && 0 < $this->getCategoryId()) {
            if ($this->getCategory() instanceof Category) {
                $children = $this->getCategory()->getChildNodes($this->getCategoryId());
                $selectedCategory = [
                    $fcrTable . '.category_id' => array_merge((array)$this->getCategoryId(), $children),
                ];
            } else {
                $selectedCategory = [
                    $fcrTable . '.category_id' => $this->getCategoryId(),
                ];
            }
            $condition = array_merge($selectedCategory, $condition);
        }

        if ((!$allLanguages) && (!is_numeric($searchTerm))) {
            $selectedLanguage = ['fd.lang' => "'" . $this->config->getLanguage()->getLanguage() . "'"];
            $condition        = array_merge($selectedLanguage, $condition);
        }

        $search->setTable($fdTable)
            ->setResultColumns(
                [
                'fd.id AS id',
                'fd.lang AS lang',
                'fd.solution_id AS solution_id',
                $fcrTable . '.category_id AS category_id',
                'fd.thema AS question',
                'fd.content AS answer'
                ]
            )
            ->setJoinedTable($fcrTable)
            ->setJoinedColumns(
                [
                'fd.id = ' . $fcrTable . '.record_id',
                'fd.lang = ' . $fcrTable . '.record_lang'
                ]
            )
            ->setConditions($condition);

        if (is_numeric($searchTerm)) {
            $search->setMatchingColumns(['fd.solution_id']);
        } else {
            $search->setMatchingColumns(['fd.thema', 'fd.content', 'fd.keywords']);
        }

        $result = $search->search($searchTerm);

        if (!$this->config->getDb()->numRows($result)) {
            return [];
        } else {
            return $this->config->getDb()->fetchAll($result);
        }
    }

    /**
     * The search function for the Elasticsearch powered full text search.
     *
     * @param  string $searchTerm   Text/Number (solution id)
     * @param  bool   $allLanguages true to search over all languages
     * @throws
     *
     * @return array
     */
    public function searchElasticsearch($searchTerm, $allLanguages = true)
    {
        $esSearch = new Elasticsearch($this->config);

        if (!is_null($this->getCategoryId()) && 0 < $this->getCategoryId()) {
            if ($this->getCategory() instanceof Category) {
                $children = $this->getCategory()->getChildNodes($this->getCategoryId());
                $esSearch->setCategoryIds(array_merge([$this->getCategoryId()], $children));
            }
        } else {
            $allCategories = $this->getCategory()->getAllCategoryIds();
            $esSearch->setCategoryIds($allCategories);
        }

        if (!$allLanguages) {
            $esSearch->setLanguage($this->config->getLanguage()->getLanguage());
        }

        return $esSearch->search($searchTerm);
    }

    /**
     * Logging of search terms for improvements.
     *
     * @param string $searchTerm Search term
     * @throws \Exception
     */
    public function logSearchTerm($searchTerm)
    {
        if (Strings::strlen($searchTerm) === 0) {
            return;
        }

        $date = new DateTime();
        $query = sprintf(
            "
            INSERT INTO
                %s
            (id, lang, searchterm, searchdate)
                VALUES
            (%d, '%s', '%s', '%s')",
            $this->table,
            $this->config->getDb()->nextId($this->table, 'id'),
            $this->config->getLanguage()->getLanguage(),
            $this->config->getDb()->escape($searchTerm),
            $date->format('Y-m-d H:i:s')
        );

        $this->config->getDb()->query($query);
    }

    /**
     * Deletes a search term.
     *
     * @param string $searchTerm
     *
     * @return bool
     */
    public function deleteSearchTerm(string $searchTerm): bool
    {
        $query = sprintf(
            "
            DELETE FROM
                %s
            WHERE
                searchterm = '%s'",
            $this->table,
            $searchTerm
        );

        return $this->config->getDb()->query($query);
    }

    /**
     * Deletes all search terms.
     *
     * @return bool
     */
    public function deleteAllSearchTerms(): bool
    {
        $query = sprintf('DELETE FROM %s', $this->table);

        return $this->config->getDb()->query($query);
    }

    /**
     * Returns the most popular searches.
     *
     * @param int  $numResults Number of Results, default: 7
     * @param bool $withLang   Should the language be included in the result?
     *
     * @return array
     */
    public function getMostPopularSearches(int $numResults = 7, bool $withLang = false): array
    {
        $searchResult = [];

        $byLang = $withLang ? ', lang' : '';
        $query = sprintf(
            '
            SELECT 
                MIN(id) as id, searchterm, COUNT(searchterm) AS number %s
            FROM
                %s
            GROUP BY
                searchterm %s
            ORDER BY
                number
            DESC',
            $byLang,
            $this->table,
            $byLang
        );

        $result = $this->config->getDb()->query($query);

        if (false !== $result) {
            $i = 0;
            while ($row = $this->config->getDb()->fetchObject($result)) {
                if ($i < $numResults) {
                    $searchResult[] = (array)$row;
                }
                ++$i;
            }
        }

        return $searchResult;
    }

    /**
     * Returns row count from the "faqsearches" table.
     *
     * @return int
     */
    public function getSearchesCount(): int
    {
        $sql = sprintf(
            'SELECT COUNT(1) AS count FROM %s',
            $this->table
        );

        $result = $this->config->getDb()->query($sql);

        return (int)$this->config->getDb()->fetchObject($result)->count;
    }

    /**
     * Sets the Entity object.
     *
     * @param Category $category
     */
    public function setCategory(Category $category)
    {
        $this->category = $category;
    }

    /**
     * @return Category
     */
    public function getCategory(): Category
    {
        return $this->category;
    }
}
