<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    actionlog
 */

/**
 * Hook class.
 */
class Hook_admin_stats_actionlogs extends CMSStatsProvider
{
    /**
     * Find metadata about stats graphs that are provided by this stats hook.
     *
     * @param  boolean $for_kpi Whether this is for setting up a KPI
     * @return ?array Map of metadata (null: hook is disabled)
     */
    public function info(bool $for_kpi = false) : ?array
    {
        if (!addon_installed('actionlog')) {
            return null;
        }

        require_lang('actionlog');

        // Possible selections for action type filter
        require_all_lang();
        $_action_type_list_growth = [];
        $_action_type_list_activity = [];
        $rows1 = (get_forum_type() == 'cns') ? $GLOBALS['FORUM_DB']->query_select('f_moderator_logs', ['DISTINCT l_the_type']) : [];
        $rows2 = $GLOBALS['SITE_DB']->query_select('actionlogs', ['DISTINCT the_type']);
        foreach ($rows1 as $row) {
            if ($this->should_skip_type($row['l_the_type'], '')) {
                continue;
            }
            $lang = do_lang($row['l_the_type'], null, null, null, null, false);
            if ($lang !== null) {
                if ($this->should_skip_type($row['l_the_type'], 'actionlog_activity')) {
                    $_action_type_list_activity[$row['l_the_type']] = $lang;
                }
                if (!$this->should_skip_type($row['l_the_type'], 'actionlog_growth')) {
                    $_action_type_list_growth[$row['l_the_type']] = $lang;
                }
            }
        }
        foreach ($rows2 as $row) {
            if ($this->should_skip_type($row['the_type'], '')) {
                continue;
            }
            $lang = do_lang($row['the_type'], null, null, null, null, false);
            if ($lang !== null) {
                if ($this->should_skip_type($row['the_type'], 'actionlog_activity')) {
                    $_action_type_list_activity[$row['the_type']] = $lang;
                }
                if (!$this->should_skip_type($row['the_type'], 'actionlog_growth')) {
                    $_action_type_list_growth[$row['the_type']] = $lang;
                }
            }
        }
        asort($_action_type_list_activity, SORT_NATURAL | SORT_FLAG_CASE);
        asort($_action_type_list_growth, SORT_NATURAL | SORT_FLAG_CASE);

        return [
            'actionlog_growth' => [
                'label' => do_lang_tempcode('STATS_ACTIONLOG_GROWTH'),
                'category' => 'content_growth',
                'filters' => [
                    'actionlog_growth__month_range' => new CMSStatsDateMonthRangeFilter('actionlog_growth__month_range', do_lang_tempcode('DATE_RANGE'), null, $for_kpi),
                    'actionlog_growth__the_type' => new CMSStatsListFilter('actionlog_growth__the_type', do_lang_tempcode('ACTION'), $_action_type_list_growth),
                ],
                'pivot' => new CMSStatsDatePivot('actionlog_growth__pivot', $this->get_date_pivots(!$for_kpi)),
                'support_kpis' => self::KPI_HIGH_IS_GOOD,
            ],
            'actionlog_activity' => [
                'label' => do_lang_tempcode('STATS_ACTIONLOG_ACTIVITY'),
                'category' => 'feedback_and_engagement',
                'filters' => [
                    'actionlog_activity__month_range' => new CMSStatsDateMonthRangeFilter('actionlog_activity__month_range', do_lang_tempcode('DATE_RANGE'), null, $for_kpi),
                    'actionlog_activity__the_type' => new CMSStatsListFilter('actionlog_activity__the_type', do_lang_tempcode('ACTION'), $_action_type_list_activity),
                ],
                'pivot' => new CMSStatsDatePivot('actionlog_activity__pivot', $this->get_date_pivots(!$for_kpi)),
                'support_kpis' => self::KPI_HIGH_IS_GOOD,
            ],
            'actionlog_adminzone' => [
                'label' => do_lang_tempcode('STATS_ACTIONLOG_ADMINZONE'),
                'category' => 'security',
                'filters' => [
                    'actionlog_adminzone__month_range' => new CMSStatsDateMonthRangeFilter('actionlog_adminzone__month_range', do_lang_tempcode('DATE_RANGE'), null, $for_kpi),
                ],
                'pivot' => new CMSStatsDatePivot('actionlog_adminzone__pivot', $this->get_date_pivots(!$for_kpi)),
                'support_kpis' => self::KPI_LOW_IS_GOOD,
            ],
        ];
    }

    /**
     * Preprocess raw data in the database into something we can efficiently draw graphs/conclusions from.
     *
     * @param  TIME $start_time Start timestamp
     * @param  TIME $end_time End timestamp
     * @param  array $data_buckets Map of data buckets; a map of bucket name to nested maps with the following maps in sequence: 'month', 'pivot', 'value' (then further map data) ; extended and returned by reference
     */
    public function preprocess_raw_data(int $start_time, int $end_time, array &$data_buckets)
    {
        require_code('temporal');

        $server_timezone = get_server_timezone();

        $max = 1000;
        $start = 0;

        $date_pivots = $this->get_date_pivots();

        $query = 'SELECT * FROM ' . get_table_prefix() . 'actionlogs WHERE ';
        $query .= 'date_and_time>=' . strval($start_time) . ' AND ';
        $query .= 'date_and_time<=' . strval($end_time);
        $query .= ' ORDER BY date_and_time';
        do {
            $rows = $GLOBALS['SITE_DB']->query($query, $max, $start);
            foreach ($rows as $row) {
                $timestamp = $row['date_and_time'];
                $timestamp = tz_time($timestamp, $server_timezone);

                $month = to_epoch_interval_index($timestamp, 'months');

                $type = $row['the_type'];

                if ($type == 'ACCESSED_ADMIN_ZONE') { // NB: special handling
                    foreach (array_keys($date_pivots) as $pivot) {
                        $pivot_value = $this->calculate_date_pivot_value($pivot, $timestamp);
                        if (!isset($data_buckets['actionlog_adminzone'][$month][$pivot][$pivot_value])) {
                            $data_buckets['actionlog_adminzone'][$month][$pivot][$pivot_value][$type] = 0;
                        }
                        $data_buckets['actionlog_adminzone'][$month][$pivot][$pivot_value][$type]++;
                    }
                    continue;
                }

                if ($this->should_skip_type($type, '')) {
                    continue;
                }

                foreach (array_keys($date_pivots) as $pivot) {
                    $pivot_value = $this->calculate_date_pivot_value($pivot, $timestamp);

                    if (!$this->should_skip_type($row['the_type'], 'actionlog_activity')) {
                        if (!isset($data_buckets['actionlog_activity'][$month][$pivot][$pivot_value][$type])) {
                            $data_buckets['actionlog_activity'][$month][$pivot][$pivot_value][$type] = 0;
                        }
                        $data_buckets['actionlog_activity'][$month][$pivot][$pivot_value][$type]++;
                    }
                    if (!$this->should_skip_type($row['the_type'], 'actionlog_growth')) {
                        if (!isset($data_buckets['actionlog_growth'][$month][$pivot][$pivot_value][$type])) {
                            $data_buckets['actionlog_growth'][$month][$pivot][$pivot_value][$type] = 0;
                        }
                        $data_buckets['actionlog_growth'][$month][$pivot][$pivot_value][$type]++;
                    }
                }
            }

            $start += $max;
        } while (!empty($rows));

        $query = 'SELECT * FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_moderator_logs WHERE ';
        $query .= 'l_date_and_time>=' . strval($start_time) . ' AND ';
        $query .= 'l_date_and_time<=' . strval($end_time);
        $query .= ' ORDER BY l_date_and_time';
        do {
            $rows = $GLOBALS['SITE_DB']->query($query, $max, $start);
            foreach ($rows as $row) {
                $timestamp = $row['l_date_and_time'];
                $timestamp = tz_time($timestamp, $server_timezone);

                $month = to_epoch_interval_index($timestamp, 'months');

                $type = $row['l_the_type'];
                if ($this->should_skip_type($type, '')) {
                    continue;
                }

                foreach (array_keys($date_pivots) as $pivot) {
                    $pivot_value = $this->calculate_date_pivot_value($pivot, $timestamp);

                    if (!$this->should_skip_type($row['l_the_type'], 'actionlog_activity')) {
                        if (!isset($data_buckets['actionlog_activity'][$month][$pivot][$pivot_value][$type])) {
                            $data_buckets['actionlog_activity'][$month][$pivot][$pivot_value][$type] = 0;
                        }
                        $data_buckets['actionlog_activity'][$month][$pivot][$pivot_value][$type]++;
                    }
                    if (!$this->should_skip_type($row['l_the_type'], 'actionlog_growth')) {
                        if (!isset($data_buckets['actionlog_growth'][$month][$pivot][$pivot_value][$type])) {
                            $data_buckets['actionlog_growth'][$month][$pivot][$pivot_value][$type] = 0;
                        }
                        $data_buckets['actionlog_growth'][$month][$pivot][$pivot_value][$type]++;
                    }
                }
            }

            $start += $max;
        } while (!empty($rows));
    }

    /**
     * Generate final data from preprocessed data.
     *
     * @param  string $bucket Data bucket we want data for
     * @param  string $pivot Pivot value
     * @param  array $filters Map of filters (including pivot if applicable)
     * @return ?array Final data in standardised map format (null: could not generate)
     */
    public function generate_final_data(string $bucket, string $pivot, array $filters) : ?array
    {
        $range = $this->convert_month_range_filter_to_pair($filters[$bucket . '__month_range']);

        $data = $this->fill_data_by_date_pivots($pivot, $range[0], $range[1]);

        $where = [
            'p_bucket' => $bucket,
            'p_pivot' => $pivot,
        ];
        $extra = '';
        $extra .= ' AND p_month>=' . strval($range[0]);
        $extra .= ' AND p_month<=' . strval($range[1]);
        $data_rows = $GLOBALS['SITE_DB']->query_select('stats_preprocessed', ['p_data'], $where, $extra);
        foreach ($data_rows as $data_row) {
            $_data = @unserialize($data_row['p_data']);
            foreach ($_data as $pivot_value => $__) {
                $pivot_value = $this->make_date_pivot_value_nice($pivot, $pivot_value);

                foreach ($__ as $the_type => $total) {
                    if ((!empty($filters[$bucket . '__the_type'])) && (!simulated_wildcard_match($filters[$bucket . '__the_type'], $the_type, true))) {
                        continue;
                    }

                    if (!isset($data[$pivot_value])) {
                        $data[$pivot_value] = 0;
                    }
                    $data[$pivot_value] += $total;
                }
            }
        }

        return [
            'type' => null,
            'data' => $data,
            'x_axis_label' => do_lang_tempcode('TIME_IN_TIMEZONE', escape_html(make_nice_timezone_name(get_site_timezone()))),
            'y_axis_label' => do_lang_tempcode('COUNT_NEW'),
        ];
    }

    /**
     * Determine whether we should skip the provided action log type in the stats module.
     *
     * @param  ID_TEXT $type The action log type
     * @param  ID_TEXT $stat The statistic we are fetching (blank: skip only if the type would have been skipped by all statistics)
     * @return boolean Whether to skip it
     */
    protected function should_skip_type(string $type, string $stat) : bool
    {
        require_code('actionlog');

        $flags = get_handler_flags($type);

        // Do not count GDPR towards the statistics regardless of stat
        if (($flags & ACTIONLOG_FLAG__GDPR) != 0) {
            return true;
        }

        // Do not count user actions towards content growth
        if ($stat == 'actionlog_growth') {
            if (($flags & ACTIONLOG_FLAG__USER_ACTION) != 0) {
                return true;
            }
        }

        return false;
    }
}
