<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */

namespace Piwik\Plugins\Insights;

use Piwik\API\Request as ApiRequest;
use Piwik\DataTable;
use Piwik\Piwik;

/**
 * API for plugin Insights
 *
 * @method static \Piwik\Plugins\Insights\API getInstance()
 */
class API extends \Piwik\Plugin\API
{
    /**
     * Include only 'movers' which are existing in the current and past report.
     */
    public const FILTER_BY_MOVERS = 'movers';

    /**
     * Include only 'new' rows which were not existing in the past report.
     */
    public const FILTER_BY_NEW = 'new';

    /**
     * Include only 'disappeared' rows which were existing in the past report but no longer in the current report.
     */
    public const FILTER_BY_DISAPPEARED = 'disappeared';

    /**
     * @var Model
     */
    private $model;

    public function __construct(Model $model)
    {
        $this->model = $model;
    }

    private function getOverviewReports()
    {
        $reports = array();

        /**
         * Triggered to gather all reports to be displayed in the "Insight" and "Movers And Shakers" overview reports.
         * Plugins that want to add new reports to the overview should subscribe to this event and add reports to the
         * incoming array. API parameters can be configured as an array optionally.
         *
         * **Example**
         *
         *     public function addReportToInsightsOverview(&$reports)
         *     {
         *         $reports['Actions_getPageUrls']  = array();
         *         $reports['Actions_getDownloads'] = array('flat' => 1, 'minGrowthPercent' => 60);
         *     }
         *
         * @param array &$reports An array containing a report unique id as key and an array of API parameters as
         *                        values.
         */
        Piwik::postEvent('Insights.addReportToOverview', array(&$reports));

        return $reports;
    }

    /**
     * Detects whether insights can be generated for this date/period combination or not.
     * @param string $date     eg 'today', '2012-12-12'
     * @param string $period   eg 'day' or 'week'
     *
     * @return bool
     */
    public function canGenerateInsights($date, $period)
    {
        Piwik::checkUserHasSomeViewAccess();

        try {
            $lastDate = $this->model->getLastDate($date, $period, 1);
        } catch (\Exception $e) {
            return false;
        }

        if (empty($lastDate)) {
            return false;
        }

        return true;
    }

    /**
     * Generates insights for a set of reports. Plugins can add their own reports to be included in the insights
     * overview by listening to the {@hook Insights.addReportToOverview} event.
     *
     * @param int $idSite
     * @param string $period
     * @param string $date
     * @param bool|string $segment
     *
     * @return DataTable\Map   A map containing a dataTable for each insight report. See {@link getInsights()} for more
     *                         information
     */
    public function getInsightsOverview($idSite, $period, $date, $segment = false)
    {
        Piwik::checkUserHasViewAccess($idSite);

        $defaultParams = array(
            'limitIncreaser' => 3,
            'limitDecreaser' => 3,
            'minImpactPercent' => 1,
            'minGrowthPercent' => 25,
        );

        $map = $this->generateOverviewReport('getInsights', $idSite, $period, $date, $segment, $defaultParams);

        return $map;
    }

    /**
     * Detects the movers and shakers for a set of reports. Plugins can add their own reports to be included in this
     * overview by listening to the {@hook Insights.addReportToOverview} event.
     *
     * @param int $idSite
     * @param string $period
     * @param string $date
     * @param bool|string $segment
     *
     * @return DataTable\Map   A map containing a dataTable for each movers and shakers report. See
     *                         {@link getMoversAndShakers()} for more information
     */
    public function getMoversAndShakersOverview($idSite, $period, $date, $segment = false)
    {
        Piwik::checkUserHasViewAccess($idSite);

        $defaultParams = array(
            'limitIncreaser' => 4,
            'limitDecreaser' => 4
        );

        $map = $this->generateOverviewReport('getMoversAndShakers', $idSite, $period, $date, $segment, $defaultParams);

        return $map;
    }

    private function generateOverviewReport($method, $idSite, $period, $date, $segment, array $defaultParams)
    {
        $tableManager = DataTable\Manager::getInstance();

        /** @var DataTable[] $tables */
        $tables = array();
        foreach ($this->getOverviewReports() as $reportId => $reportParams) {
            if (!empty($reportParams)) {
                foreach ($defaultParams as $key => $defaultParam) {
                    if (!array_key_exists($key, $reportParams)) {
                        $reportParams[$key] = $defaultParam;
                    }
                }
            }

            $firstTableId     = $tableManager->getMostRecentTableId();
            $table            = $this->requestApiMethod($method, $idSite, $period, $date, $reportId, $segment, $reportParams);
            $reportTableIds[] = $table->getId();
            $tableManager->deleteTablesExceptIgnored($reportTableIds, $firstTableId);

            $tables[] = $table;
        }

        $map = new DataTable\Map();

        foreach ($tables as $table) {
            $map->addTable($table, $table->getMetadata('reportName'));
        }

        return $map;
    }

    /**
     * Detects the movers and shakers of a given date / report combination. A mover and shakers has an higher impact
     * than other rows on average. For instance if a sites pageviews increase by 10% a page that increased by 40% at the
     * same time contributed significantly more to the success than the average of 10%.
     *
     * @param int $idSite
     * @param string $period
     * @param string $date
     * @param string $reportUniqueId   eg 'Actions_getPageUrls'. An id like 'Goals_getVisitsUntilConversion_idGoal--4' works as well.
     * @param bool|string $segment
     * @param int $comparedToXPeriods
     * @param int $limitIncreaser      Value '0' ignores all increasers
     * @param int $limitDecreaser      Value '0' ignores all decreasers
     *
     * @return DataTable
     *
     * @throws \Exception In case a report having the given ID does not exist
     * @throws \Exception In case the report exists but does not return a dataTable
     */
    public function getMoversAndShakers(
        $idSite,
        $period,
        $date,
        $reportUniqueId,
        $segment = false,
        $comparedToXPeriods = 1,
        $limitIncreaser = 4,
        $limitDecreaser = 4
    ) {
        Piwik::checkUserHasViewAccess(array($idSite));

        $metric  = 'nb_visits';
        $orderBy = InsightReport::ORDER_BY_ABSOLUTE;

        $reportMetadata = $this->model->getReportByUniqueId($idSite, $reportUniqueId);

        if (empty($reportMetadata)) {
            throw new \Exception('A report having the ID ' . $reportUniqueId .  ' does not exist');
        }

        $totalValue     = $this->model->getTotalValue($idSite, $period, $date, $metric, $segment);
        $currentReport  = $this->model->requestReport($idSite, $period, $date, $reportUniqueId, $metric, $segment);
        $this->checkReportIsValid($currentReport);

        $lastDate       = $this->model->getLastDate($date, $period, $comparedToXPeriods);
        $lastTotalValue = $this->model->getTotalValue($idSite, $period, $lastDate, $metric, $segment);
        $lastReport     = $this->model->requestReport($idSite, $period, $lastDate, $reportUniqueId, $metric, $segment);
        $this->checkReportIsValid($lastReport);

        $insight = new InsightReport();
        return $insight->generateMoverAndShaker($reportMetadata, $period, $date, $lastDate, $metric, $currentReport, $lastReport, $totalValue, $lastTotalValue, $orderBy, $limitIncreaser, $limitDecreaser);
    }

    /**
     * Generates insights by comparing the report for a given date/period with a different date and calculating the
     * difference. The API can exclude rows which growth is not good enough or did not have enough impact.
     *
     * @param int $idSite
     * @param string $period
     * @param string $date
     * @param string $reportUniqueId   eg 'Actions_getPageUrls'. An id like 'Goals_getVisitsUntilConversion_idGoal--4' works as well.
     * @param bool|string $segment
     * @param int $limitIncreaser      Value '0' ignores all increasers
     * @param int $limitDecreaser      Value '0' ignores all decreasers
     * @param string $filterBy         By default all rows will be ignored. If given only 'movers', 'new' or 'disappeared' will be returned.
     * @param int $minImpactPercent    The minimum impact in percent. Eg '2%' of 1000 visits means the change /
     *                                 increase / decrease has to be at least 20 visits. Usually the '2%' are based on the total
     *                                 amount of visits but for reports having way less visits the metric total is used. Eg A page
     *                                 has 1000 visits but only 100 visits having keywords. In this case a minimum impact of '2%' evaluates to 2 and not 20.
     * @param int $minGrowthPercent    The amount of percent a row has to increase or decrease at least compared to the previous period.
     *                                 If value is '20' the growth has to be either at least '+20%' or '-20%' and lower.
     * @param int $comparedToXPeriods  The report will be compared to X periods before.
     * @param string $orderBy          Orders the rows by 'absolute', 'relative' or 'importance'.
     *
     * @return DataTable
     *
     * @throws \Exception In case a report having the given ID does not exist
     * @throws \Exception In case the report exists but does not return a dataTable
     */
    public function getInsights(
        $idSite,
        $period,
        $date,
        $reportUniqueId,
        $segment = false,
        $limitIncreaser = 5,
        $limitDecreaser = 5,
        $filterBy = '',
        $minImpactPercent = 2,
        $minGrowthPercent = 20,
        $comparedToXPeriods = 1,
        $orderBy = 'absolute'
    ) {
        Piwik::checkUserHasViewAccess(array($idSite));

        $metric = 'nb_visits';

        $reportMetadata = $this->model->getReportByUniqueId($idSite, $reportUniqueId);

        if (empty($reportMetadata)) {
            throw new \Exception('A report having the ID ' . $reportUniqueId .  ' does not exist');
        }

        $totalValue     = $this->model->getTotalValue($idSite, $period, $date, $metric, $segment);
        $currentReport  = $this->model->requestReport($idSite, $period, $date, $reportUniqueId, $metric, $segment);
        $this->checkReportIsValid($currentReport);

        $lastDate       = $this->model->getLastDate($date, $period, $comparedToXPeriods);
        $lastTotalValue = $this->model->getTotalValue($idSite, $period, $lastDate, $metric, $segment);
        $lastReport     = $this->model->requestReport($idSite, $period, $lastDate, $reportUniqueId, $metric, $segment);
        $this->checkReportIsValid($lastReport);

        $minGrowthPercentPositive = abs($minGrowthPercent);
        $minGrowthPercentNegative = -1 * $minGrowthPercentPositive;

        $relevantTotal = $this->model->getRelevantTotalValue($currentReport, $metric, $totalValue);

        $minMoversPercent      = -1;
        $minNewPercent         = -1;
        $minDisappearedPercent = -1;

        switch ($filterBy) {
            case self::FILTER_BY_MOVERS:
                $minMoversPercent = $minImpactPercent;
                break;
            case self::FILTER_BY_NEW:
                $minNewPercent = $minImpactPercent;
                break;
            case self::FILTER_BY_DISAPPEARED:
                $minDisappearedPercent = $minImpactPercent;
                break;
            default:
                $minMoversPercent      = $minImpactPercent;
                $minNewPercent         = $minImpactPercent;
                $minDisappearedPercent = $minImpactPercent;
        }

        $insight = new InsightReport();
        $table   = $insight->generateInsight($reportMetadata, $period, $date, $lastDate, $metric, $currentReport, $lastReport, $relevantTotal, $minMoversPercent, $minNewPercent, $minDisappearedPercent, $minGrowthPercentPositive, $minGrowthPercentNegative, $orderBy, $limitIncreaser, $limitDecreaser);
        $insight->markMoversAndShakers($table, $currentReport, $lastReport, $totalValue, $lastTotalValue);

        return $table;
    }

    private function checkReportIsValid($report)
    {
        if (!($report instanceof DataTable)) {
            throw new \Exception('Insight can be only generated for reports returning a dataTable');
        }
    }

    private function requestApiMethod($method, $idSite, $period, $date, $reportId, $segment, $additionalParams)
    {
        $params = array(
            'idSite' => $idSite,
            'date'   => $date,
            'period' => $period,
            'format' => 'original',
            'reportUniqueId' => $reportId,
            'totals' => 0,
        );

        if (!empty($segment)) {
            $params['segment'] = $segment;
        }

        if (!empty($additionalParams)) {
            foreach ($additionalParams as $key => $value) {
                $params[$key] = $value;
            }
        }

        return ApiRequest::processRequest('Insights.' . $method, $params, $default = []);
    }
}
