<?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    stats
 */

require_code('crud_module');

/**
 * Module page class.
 */
class Module_admin_stats extends Standard_crud_module
{
    protected $lang_type = 'KPI';
    protected $select_name = 'KPI';
    protected $select_name_description = 'DESCRIPTION_KPI';
    protected $menu_label = 'MODULE_TRANS_NAME_admin_stats';
    protected $table = 'stats_kpis';
    protected $do_preview = false;
    protected $orderer = 'k_title';

    public $title;

    /**
     * Find details of the module.
     *
     * @return ?array Map of module info (null: module is disabled)
     */
    public function info() : ?array
    {
        $info = [];
        $info['author'] = 'Chris Graham';
        $info['organisation'] = 'Composr';
        $info['hacked_by'] = null;
        $info['hack_version'] = null;
        $info['version'] = 11;
        $info['locked'] = true;
        $info['update_require_upgrade'] = true;
        $info['min_cms_version'] = 11.0;
        $info['addon'] = 'stats';
        return $info;
    }

    /**
     * Uninstall the module.
     */
    public function uninstall()
    {
        $tables = [
            'stats',
            'stats_preprocessed',
            'stats_preprocessed_flat',
            'stats_kpis',
            'stats_events',
            'stats_link_tracker',
            'stats_known_events',
            'stats_known_tracking',
            'stats_known_links',
            'usersonline_track',
            'ip_country',
        ];
        $GLOBALS['SITE_DB']->drop_table_if_exists($tables);
    }

    /**
     * Install the module.
     *
     * @param  ?integer $upgrade_from What version we're upgrading from (null: new install)
     * @param  ?integer $upgrade_from_hack What hack version we're upgrading from (null: new-install/not-upgrading-from-a-hacked-version)
     */
    public function install(?int $upgrade_from = null, ?int $upgrade_from_hack = null)
    {
        if ($upgrade_from === null) {
            $GLOBALS['SITE_DB']->create_table('stats', [
                'id' => '*AUTO',
                'date_and_time' => 'TIME',
                'page_link' => 'SHORT_TEXT',
                'post' => 'LONG_TEXT',
                'referer_url' => 'URLPATH',
                'ip' => 'IP',
                'member_id' => 'MEMBER',
                'session_id' => 'ID_TEXT', // May not be a literal session, as we don't always assign sessions to users (when operating from static caching) - might be an IP address for example
                'browser' => 'SHORT_TEXT',
                'operating_system' => 'SHORT_TEXT',
                'requested_language' => 'ID_TEXT',
                'milliseconds' => 'INTEGER',
                'tracking_code' => 'ID_TEXT',
            ]);
            $GLOBALS['SITE_DB']->create_index('stats', 'date_and_time', ['date_and_time']);

            $GLOBALS['SITE_DB']->create_table('usersonline_track', [
                'date_and_time' => '*TIME',
                'peak' => 'INTEGER',
            ]);
            $GLOBALS['SITE_DB']->create_index('usersonline_track', 'peak_track', ['peak']);

            $GLOBALS['SITE_DB']->create_table('ip_country', [
                'id' => '*AUTO',
                'begin_num' => 'UINTEGER',
                'end_num' => 'UINTEGER',
                'country' => 'SHORT_TEXT',
            ]);

            require_lang('stats');
            require_code('crypt');
            $secure_ref = get_secure_random_string();
            $id = $GLOBALS['SITE_DB']->query_insert('task_queue', [
                't_title' => do_lang('INSTALL_GEOLOCATION_DATA'),
                't_hook' => 'install_geolocation_data',
                't_args' => serialize([]),
                't_member_id' => $GLOBALS['FORUM_DRIVER']->get_guest_id(),
                't_secure_ref' => $secure_ref, // Used like a temporary password to initiate the task
                't_send_notification' => 0,
                't_locked' => 0,
                't_add_time' => time(),
            ], true);
        }

        if (($upgrade_from !== null) && ($upgrade_from < 8)) { // LEGACY
            $GLOBALS['SITE_DB']->alter_table_field('stats', 'get', 'URLPATH', 's_get');
        }

        if (($upgrade_from !== null) && ($upgrade_from < 9)) { // LEGACY
            $GLOBALS['SITE_DB']->alter_table_field('stats', 'the_user', 'MEMBER', 'member_id');
            $GLOBALS['SITE_DB']->add_table_field('stats', 'session_id', 'ID_TEXT');
            $GLOBALS['SITE_DB']->query_update('db_meta_indices', ['i_fields' => 'member_id'], ['i_name' => 'member_track_1'], '', 1);
        }

        if (($upgrade_from !== null) && ($upgrade_from < 10)) { // LEGACY
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats', 'pages');
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats', 'milliseconds');
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats', 'referer');
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats', 'browser');
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats', 'operating_system');
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats', 'member_track_1');
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats', 'member_track_2');
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats', 'member_track_3');
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats', 'member_track_4');

            $GLOBALS['SITE_DB']->delete_table_field('stats', 'access_denied_counter');

            $GLOBALS['SITE_DB']->add_table_field('stats', 'page_link', 'SHORT_TEXT');
            $start = 0;
            do { // Do a rough upgrade to the new format
                $rows = $GLOBALS['SITE_DB']->query_select('stats', ['id', 'the_page', 's_get'], [], 'ORDER BY id', 1000, $start);
                foreach ($rows as $row) {
                    $matches = [];
                    if (preg_match('#^(([^/]*)/)?pages/.*/([^/]+)\.\w+$#', $row['the_page'], $matches) != 0) {
                        $page_link = $matches[2] . ':' . $matches[3];
                        $GLOBALS['SITE_DB']->query_update('stats', ['page_link' => $page_link], ['id' => $row['id']]);
                    } else {
                        $GLOBALS['SITE_DB']->query_delete('stats', ['id' => $row['id']]);
                    }
                }
                $start += 1000;
            } while (!empty($rows));
            $GLOBALS['SITE_DB']->delete_table_field('stats', 'the_page');
            $GLOBALS['SITE_DB']->delete_table_field('stats', 's_get');

            $GLOBALS['SITE_DB']->add_table_field('stats', 'requested_language', 'LANGUAGE_NAME');
            $GLOBALS['SITE_DB']->add_table_field('stats', 'tracking_code', 'ID_TEXT');
        }

        if (($upgrade_from === null) || ($upgrade_from < 10)) { // LEGACY
            $GLOBALS['SITE_DB']->create_table('stats_preprocessed', [
                'p_bucket' => '*ID_TEXT',
                'p_month' => '*INTEGER',
                'p_pivot' => '*ID_TEXT',
                'p_data' => 'LONG_TEXT',
            ]);

            $GLOBALS['SITE_DB']->create_table('stats_preprocessed_flat', [
                'p_bucket' => '*ID_TEXT',
                'p_data' => 'LONG_TEXT',
            ]);

            $GLOBALS['SITE_DB']->create_table('stats_events', [ // This table is not about tracking individual users, 'stats' does that - it's for trend analysis
                'id' => '*AUTO',
                'e_event' => 'ID_TEXT',
                'e_date_and_time' => 'TIME',
                'e_country_code' => 'ID_TEXT',
                'e_session_id' => 'ID_TEXT',
            ]);
            $GLOBALS['SITE_DB']->create_index('stats_events', 'e_event', ['e_event', 'e_date_and_time']);
            $GLOBALS['SITE_DB']->create_index('stats_events', 'e_date_and_time', ['e_date_and_time']);

            $GLOBALS['SITE_DB']->create_table('stats_kpis', [
                'id' => '*AUTO',
                'k_graph_name' => 'ID_TEXT',
                'k_pivot' => 'ID_TEXT',
                'k_filters' => 'LONG_TEXT',
                'k_target' => '?REAL',
                'k_title' => 'SHORT_TEXT',
                'k_added_time' => 'TIME',
                'k_submitter' => 'MEMBER',
                'k_notes' => 'LONG_TEXT',
            ]);
            $GLOBALS['SITE_DB']->create_index('stats_kpis', 'k_graph_name', ['k_graph_name']);

            $GLOBALS['SITE_DB']->create_table('stats_link_tracker', [
                'id' => '*AUTO',
                'c_url' => 'URLPATH',
                'c_date_and_time' => 'TIME',

                // Unlike events, it is not possible to guess a link happened by looking at the stats table, so we gather more
                'c_member_id' => 'MEMBER',
                'c_ip_address' => 'IP',
            ]);
            $GLOBALS['SITE_DB']->create_index('stats_link_tracker', 'c_url', ['c_url']);
            $GLOBALS['SITE_DB']->create_index('stats_link_tracker', 'c_date_and_time', ['c_date_and_time']);

            $GLOBALS['SITE_DB']->create_table('stats_known_events', [
                'e_event' => '*ID_TEXT',
                'e_count_logged' => 'INTEGER',
            ]);

            $GLOBALS['SITE_DB']->create_table('stats_known_tracking', [
                't_tracking_code' => '*ID_TEXT',
                't_count_logged' => 'INTEGER',
            ]);

            $GLOBALS['SITE_DB']->create_table('stats_known_links', [
                'id' => '*AUTO',
                'l_url' => 'URLPATH',
                'l_count_logged' => 'INTEGER',
            ]);
            $GLOBALS['SITE_DB']->create_index('stats_known_links', 'l_url', ['l_url']);

            $GLOBALS['SITE_DB']->create_index('stats', 'session_id', ['session_id']);

            $GLOBALS['SITE_DB']->create_index('ip_country', 'begin_num', ['begin_num']);
            $GLOBALS['SITE_DB']->create_index('ip_country', 'end_num', ['end_num']);
        }

        if (($upgrade_from !== null) && ($upgrade_from == 10)) { // LEGACY: 11.beta1
            // Database consistency fixes
            $GLOBALS['SITE_DB']->alter_table_field('stats', 'referer', 'URLPATH', 'referer_url');
            $GLOBALS['SITE_DB']->alter_table_field('stats_kpis', 'k_added', 'TIME', 'k_added_time');
            $GLOBALS['SITE_DB']->alter_table_field('stats_kpis', 'k_added_by', 'MEMBER', 'k_submitter');
            $GLOBALS['SITE_DB']->alter_table_field('stats_known_events', 'e_times_seen', 'INTEGER', 'e_count_logged');
            $GLOBALS['SITE_DB']->alter_table_field('stats_known_tracking', 't_times_seen', 'INTEGER', 't_count_logged');
            $GLOBALS['SITE_DB']->alter_table_field('stats_known_links', 'l_times_seen', 'INTEGER', 'l_count_logged');

            $GLOBALS['SITE_DB']->delete_index_if_exists('escrow', 'recipient_id');
        }

        if (($upgrade_from !== null) && ($upgrade_from < 11)) { // LEGACY
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats_known_events', 'e_times_seen');
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats_known_links', 'l_times_seen');
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats_known_tracking', 't_times_seen');
        }

        if (($upgrade_from === null) || ($upgrade_from < 11)) {
            $GLOBALS['SITE_DB']->create_index('stats_known_events', 'e_count_logged', ['e_count_logged']);
            $GLOBALS['SITE_DB']->create_index('stats_known_tracking', 't_count_logged', ['t_count_logged']);
            $GLOBALS['SITE_DB']->create_index('stats_known_links', 'l_count_logged', ['l_count_logged']);
        }
    }

    /**
     * Find entry-points available within this module.
     *
     * @param  boolean $check_perms Whether to check permissions
     * @param  ?MEMBER $member_id The member to check permissions as (null: current user)
     * @param  boolean $support_crosslinks Whether to allow cross links to other modules (identifiable via a full-page-link rather than a screen-name)
     * @param  boolean $be_deferential Whether to avoid any entry-point (or even return null to disable the page in the Sitemap) if we know another module, or page_group, is going to link to that entry-point. Note that "!" and "browse" entry points are automatically merged with container page nodes (likely called by page-groupings) as appropriate.
     * @return ?array A map of entry points (screen-name=>language-code/string or screen-name=>[language-code/string, icon-theme-image]) (null: disabled)
     */
    public function get_entry_points(bool $check_perms = true, ?int $member_id = null, bool $support_crosslinks = true, bool $be_deferential = false) : ?array
    {
        if (!addon_installed('stats')) {
            return null;
        }

        require_code('stats');

        $ret = [
            'browse' => ['MODULE_TRANS_NAME_admin_stats', 'menu/adminzone/audit/statistics/statistics'],
        ];

        static $has_geolocation_data = null;
        if ($has_geolocation_data === null) {
            $test = $GLOBALS['SITE_DB']->query_select_value_if_there('ip_country', 'id');
            $has_geolocation_data = ($test !== null);
        }
        if (!$has_geolocation_data) {
            $ret['install_data'] = ['INSTALL_GEOLOCATION_DATA', 'menu/adminzone/audit/statistics/geolocate'];
        }

        $ret['edit'] = ['EDIT_KPI', 'admin/edit'];

        $ret['kpis'] = ['KPIS', 'admin/view_archive'];

        return $ret;
    }

    /**
     * Module pre-run function. Allows us to know metadata for <head> before we start streaming output.
     *
     * @param  boolean $top_level Whether this is running at the top level, prior to having sub-objects called
     * @param  ?ID_TEXT $type The screen type to consider for metadata purposes (null: read from environment)
     * @return ?Tempcode Tempcode indicating some kind of exceptional output (null: none)
     */
    public function pre_run(bool $top_level = true, ?string $type = null) : ?object
    {
        $error_msg = new Tempcode();
        if (!addon_installed__messaged('stats', $error_msg)) {
            return $error_msg;
        }

        require_code('stats');
        require_lang('stats');

        if (!cron_installed()) {
            attach_message(do_lang_tempcode('CRON_NEEDED_TO_WORK', escape_html(get_tutorial_url('tut_configuration'))), 'warn');
        }

        $type = get_param_string('type', 'browse');

        if ($type == 'browse') {
            $this->title = get_screen_title('MODULE_TRANS_NAME_admin_stats');

            set_helper_panel_tutorial('tut_statistics');
        }

        if ($type == 'category') {
            $categories = stats_find_graph_categories();
            $category_name = get_param_string('id');
            if (!isset($categories[$category_name])) {
                warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
            }
            $this->title = get_screen_title($categories[$category_name]['label_lang_string']);
            $_graphs = stats_find_graphs_in_category($category_name);
            foreach ($_graphs as $graph_name => $_graph) {
                list($hook_ob, $graph_details) = $_graph;
                if ($hook_ob instanceof CMSStatsProvider) {
                    foreach ($graph_details['filters'] as $filter) {
                        if ($filter !== null) {
                            inform_non_canonical_parameter($filter->filter_name);
                        }
                    }
                }
                inform_non_canonical_parameter($graph_name . '_start');
                inform_non_canonical_parameter($graph_name . '_max');
                inform_non_canonical_parameter($graph_name . '_sort');
            }
        }

        if (($type == 'graph') || ($type == 'spreadsheet')) {
            $graph_name = get_param_string('id');
            list($hook_ob, $graph_details) = stats_find_graph_details($graph_name);

            if (!is_array($graph_details)) {
                warn_exit(do_lang_tempcode('GRAPH_NOT_FOUND', escape_html($graph_name)));
            }

            $title = get_screen_title($graph_details['label'], false);

            foreach ($graph_details['filters'] as $filter) {
                if ($filter !== null) {
                    inform_non_canonical_parameter($filter->filter_name);
                }
            }
            inform_non_canonical_parameter($graph_name . '_start');
            inform_non_canonical_parameter($graph_name . '_max');
            inform_non_canonical_parameter($graph_name . '_sort');
        }

        if ($type == 'redirect') {
            $id = get_param_string('id');

            $hooks = find_all_hook_obs('modules', 'admin_stats_redirects', 'Hook_admin_stats_redirects_');
            foreach ($hooks as $ob) {
                $info = $ob->info();
                if ($info !== null) {
                    foreach ($info as $_id => $details) {
                        if ($_id == $id) {
                            $title = get_screen_title($details['label_lang_string']);

                            break 2;
                        }
                    }
                }
            }
        }

        if ($type == 'install_data') {
            $this->title = get_screen_title('INSTALL_GEOLOCATION_DATA');
        }

        if ($type == 'kpis') {
            $this->title = get_screen_title('KPIS');
        }

        return parent::pre_run($top_level);
    }

    /**
     * Standard crud_module run_start.
     *
     * @param  ID_TEXT $type The type of module execution
     * @return Tempcode The output of the run
     */
    public function run_start(string $type) : object
    {
        require_css('stats');

        if ($type == 'browse') {
            return $this->browse();
        } elseif ($type == 'install_data') {
            return $this->install_geolocation_data();
        } elseif ($type == 'category') {
            cms_extend_time_limit(TIME_LIMIT_EXTEND__CRAWL);
            disable_php_memory_limit();

            return $this->category();
        } elseif ($type == 'graph') {
            cms_extend_time_limit(TIME_LIMIT_EXTEND__CRAWL);
            disable_php_memory_limit();

            return $this->graph();
        } elseif ($type == 'spreadsheet') {
            return $this->spreadsheet();
        } elseif ($type == 'redirect') {
            return $this->redirect();
        } elseif ($type == 'kpis') {
            return $this->kpis();
        }
        return new Tempcode();
    }

    /**
     * The do-next manager for choosing a stats category.
     *
     * @return Tempcode The UI
     */
    public function browse() : object
    {
        return $this->do_next_manager($this->title, comcode_lang_string('DOC_STATISTICS'));
    }

    /**
     * Install geolocation data.
     *
     * @return Tempcode The UI, showing the result of the installation
     */
    public function install_geolocation_data() : object
    {
        if (post_param_integer('confirm', 0) == 0) {
            $preview = do_lang_tempcode('INSTALL_GEOLOCATION_DATA');
            $url = get_self_url(false, false);
            return do_template('CONFIRM_SCREEN', ['_GUID' => '153e04d683281ead45497b424307aabf', 'TITLE' => $this->title, 'PREVIEW' => $preview, 'FIELDS' => form_input_hidden('confirm', '1'), 'URL' => $url]);
        }

        require_code('tasks');
        return call_user_func_array__long_task(do_lang('INSTALL_GEOLOCATION_DATA'), $this->title, 'install_geolocation_data');
    }

    /**
     * Viewing all the graphs in a stats category.
     *
     * @return Tempcode The UI
     */
    public function category() : object
    {
        send_http_output_ping();

        $category_name = get_param_string('id');

        $_graphs = stats_find_graphs_in_category($category_name);

        $graphs = [];
        foreach ($_graphs as $graph_name => $_graph) {
            list($hook_ob, $graph_details) = $_graph;
            $graph = $this->templatify_graph($graph_name, $hook_ob, $graph_details);
            $graphs[] = $graph;
        }

        return do_template('STATS_SCREEN', ['_GUID' => 'a0e4ac56baa17004ff93c6096ff0f038', 'TITLE' => $this->title, 'GRAPHS' => $graphs]);
    }

    /**
     * View a specific graph.
     *
     * @return Tempcode The UI
     */
    public function graph() : object
    {
        $graph_name = get_param_string('id');

        send_http_output_ping();

        list($hook_ob, $graph_details) = stats_find_graph_details($graph_name);
        $graph = $this->templatify_graph($graph_name, $hook_ob, $graph_details);
        $graphs = [$graph];

        return do_template('STATS_SCREEN', ['_GUID' => 'ad070e6b3f02f9a69e0161225300e2a3', 'TITLE' => $this->title, 'GRAPHS' => $graphs]);
    }

    /**
     * Make a specific graph template-ready.
     *
     * @param  string $graph_name The UI
     * @param  object $hook_ob Hook object
     * @param  array $graph_details Graph details
     * @param  ?array $graph_final_details Graph data (null: look it up using $graph_name etc)
     * @param  array $filters Filter settings to take precedence
     * @param  ?mixed $pivot Pivot value to take precedence (null: none)
     * @param  boolean $include_form Whether to include a form
     * @param  boolean $for_kpi Whether this is for setting up a KPI
     * @return array The UI
     */
    protected function templatify_graph(string $graph_name, object $hook_ob, array $graph_details, ?array $graph_final_details = null, array $filters = [], $pivot = null, bool $include_form = true, bool $for_kpi = false) : array
    {
        $graph_rendered = stats_generate_graph($graph_name, $filters, $pivot, $hook_ob, $graph_details, $graph_final_details, $for_kpi);

        if ($include_form) {
            $graph_form = stats_generate_graph_form($graph_name, $hook_ob, $graph_details, $filters, $pivot, $for_kpi);
        } else {
            $graph_form = new Tempcode();
        }

        $results_table = stats_generate_results_table($graph_name, [], null, $hook_ob, $graph_details, $graph_final_details, $for_kpi);

        list($filters_relative, $pivot_relative) = _stats_get_graph_context($graph_details, $filters, $pivot, true); // We have to do a conversion because a new KPI has to take initial parameters in a KPI-format
        list($filters_absolute, $pivot_absolute) = _stats_get_graph_context($graph_details, $filters, $pivot, false); // We have to do a conversion because stats_generate_spreadsheet assumes non-KPI parameters

        $supports_kpi = (!empty($graph_details['support_kpis'])) && (($pivot_absolute === null) || (strpos($pivot_absolute, '_series') !== false));

        if ($supports_kpi) {
            $existing_kpis = [];
            $kpi_rows = $GLOBALS['SITE_DB']->query_select('stats_kpis', ['id', 'k_title'], ['k_graph_name' => $graph_name]);
            foreach ($kpi_rows as $kpi_row) {
                $existing_kpis[] = [
                    'KPI_ID' => strval($kpi_row['id']),
                    'KPI_TITLE' => $kpi_row['k_title'],
                    'KPI_EDIT_URL' => build_url(['page' => '_SELF', 'type' => '_edit', 'id' => $kpi_row['id']], '_SELF'),
                ];
            }
        } else {
            $existing_kpis = null;
        }

        $kpi_add_url = null;
        if ((!$for_kpi) && ($supports_kpi)) {
            $kpi_add_url = build_url(['page' => '_SELF', 'type' => 'add', 'graph_name' => $graph_name, 'filters' => json_encode($filters_relative), 'pivot' => $pivot_relative], '_SELF');
        }

        if ($results_table->is_empty()) {
            $spreadsheet_url = null;
        } else {
            $spreadsheet_url = build_url(['page' => '_SELF', 'type' => 'spreadsheet', 'id' => $graph_name, 'filters' => json_encode($filters_absolute), 'pivot' => $pivot_absolute], '_SELF');
        }

        return [
            'GRAPH_NAME' => $graph_name,
            'GRAPH_LABEL' => $graph_details['label'],
            'GRAPH_RENDERED' => $graph_rendered,
            'GRAPH_FORM' => $graph_form,
            'RESULTS_TABLE' => $results_table,
            'EXISTING_KPIS' => $existing_kpis,
            'KPI_ADD_URL' => $kpi_add_url,
            'SPREADSHEET_URL' => $spreadsheet_url,
        ];
    }

    /**
     * Download the spreadsheet for a graph.
     *
     * @return Tempcode The UI
     */
    public function spreadsheet() : object
    {
        $spreadsheet_graph_name = get_param_string('id');

        $filters = json_decode(get_param_string('filters', '[]', INPUT_FILTER_GET_COMPLEX), true);
        $pivot = get_param_string('pivot', null);

        $filename = null;
        $sheet_writer = stats_generate_spreadsheet($spreadsheet_graph_name, $filename, $filters, $pivot);
        $sheet_writer->output_and_exit($filename, true);

        return new Tempcode();
    }

    /**
     * Redirect to an external stats system.
     *
     * @return Tempcode The UI
     */
    public function redirect() : object
    {
        $id = get_param_string('id');

        $hooks = find_all_hook_obs('modules', 'admin_stats_redirects', 'Hook_admin_stats_redirects_');
        foreach ($hooks as $ob) {
            $info = $ob->info();
            if ($info !== null) {
                foreach ($info as $_id => $details) {
                    if ($_id == $id) {
                        $url = $ob->get_redirect_url($id);
                        return redirect_screen($this->title, $url);
                    }
                }
            }
        }

        warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
    }

    /**
     * Show KPIs.
     *
     * @return Tempcode The UI
     */
    public function kpis() : object
    {
        $graphs = [];
        $kpis = [];
        $_kpis = gather_kpis();
        foreach ($_kpis as $_kpi) {
            list($kpi_row, $hook_ob, $graph_details, $graph_final_details, $filters, $pivot, $edit_url, , $target, $current, $hits_target) = $_kpi;

            $graphs[] = $this->templatify_graph($kpi_row['k_graph_name'], $hook_ob, $graph_details, $graph_final_details, $filters, $pivot, false, true);

            $username = $GLOBALS['FORUM_DRIVER']->get_username($kpi_row['k_submitter'], true);

            $kpi = [
                'TITLE' => $kpi_row['k_title'],
                'CURRENT' => ($current === null) ? null : (is_integer($current) ? integer_format($current, 0) : float_format($current, 4, true)),
                'HITS_TARGET' => $hits_target,
                'TARGET' => ($target === null) ? null : float_format($target, 4, true),
                'KPI_EDIT_URL' => $edit_url,
                'GRAPH_NAME' => $kpi_row['k_graph_name'],
                'USERNAME' => $username,
                'ADDED' => get_timezoned_date($kpi_row['k_added_time']),
                'NOTES' => $kpi_row['k_notes'],
            ];
            $kpis[] = $kpi;
        }

        if (empty($kpis)) {
            inform_exit(do_lang_tempcode('NO_ENTRIES'));
        }

        return do_template('KPI_SCREEN', [
            '_GUID' => 'edd8e8737647f494681d8b34fb67998c',
            'TITLE' => $this->title,
            'GRAPHS' => $graphs,
            'KPIS' => $kpis,
        ]);
    }

    /**
     * Standard crud_module table function.
     *
     * @param  array $url_map Details to go to build_url for link to the next screen
     * @return array A pair: The choose table, Whether re-ordering is supported from this screen
     */
    public function create_selection_list_choose_table(array $url_map) : array
    {
        require_code('templates_results_table');

        list($rows, $max_rows) = $this->get_entry_rows();

        $hr = [
            do_lang_tempcode('TITLE'),
            do_lang_tempcode('BLOCK_main_staff_stats_graph_PARAM_param_TITLE'),
            do_lang_tempcode('USERNAME'),
            do_lang_tempcode('ADDED'),
            do_lang_tempcode('TARGET'),
            do_lang_tempcode('ACTIONS'),
        ];
        $header_row = results_header_row($hr);

        $result_entries = new Tempcode();

        $_kpis = gather_kpis();
        foreach ($_kpis as $_kpi) {
            list($kpi_row, , , , , , $edit_url, , , , ) = $_kpi;

            $username = $GLOBALS['FORUM_DRIVER']->get_username($kpi_row['k_submitter'], true);

            $fr = [
                $kpi_row['k_title'],
                $kpi_row['k_graph_name'],
                $username,
                get_timezoned_date($kpi_row['k_added_time']),
                ($kpi_row['k_target'] === null) ? do_lang_tempcode('NONE_EM') : protect_from_escaping(escape_html(float_format($kpi_row['k_target'], 4, true))),
                protect_from_escaping(hyperlink($edit_url, do_lang_tempcode('EDIT'), false, true, $kpi_row['k_title'])),
            ];

            $result_entries->attach(results_entry($fr, true));
        }

        return [results_table(do_lang($this->menu_label), get_param_integer('start', 0), 'start', either_param_integer('max', 20), 'max', $max_rows, $header_row, $result_entries), false];
    }

    /**
     * Get Tempcode for an adding form.
     *
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function get_form_fields_for_add()
    {
        return $this->get_form_fields();
    }

    /**
     * Get Tempcode for adding/editing form.
     *
     * @param  ?integer $id ID (null: new)
     * @param  array $filters Filter settings
     * @param  ?ID_TEXT $pivot Pivot value (null: default)
     * @param  ?float $target Target (null: none)
     * @param  ?SHORT_TEXT $title Title (null: auto-generate)
     * @param  LONG_TEXT $notes Staff notes
     * @return array A tuple: The input fields, Hidden fields, ...
     */
    public function get_form_fields(?int $id = null, array $filters = [], ?string $pivot = null, ?float $target = null, ?string $title = null, string $notes = '') : array
    {
        $fields = new Tempcode();
        $hidden = new Tempcode();

        if ($id === null) {
            $graph_name = get_param_string('graph_name');

            $filters = json_decode(get_param_string('filters', '[]', INPUT_FILTER_GET_COMPLEX), true);
            $pivot = get_param_string('pivot', null);
        } else {
            $graph_name = $GLOBALS['SITE_DB']->query_select_value('stats_kpis', 'k_graph_name', ['id' => $id]);
        }
        $hook_ob = null;
        $graph_details = null;
        $_graph_fields = stats_generate_graph_form_fields($graph_name, $hook_ob, $graph_details, $filters, $pivot, true);

        $hidden->attach(form_input_hidden('graph_name', $graph_name));

        if ($title === null) {
            $title = $graph_details['label'];
        }

        $message = do_lang_tempcode('KPI_CONFIGURING_FOR', $graph_details['label']/*Tempcode*/);
        if ($id === null) {
            $this->add_text = $message;
        } else {
            $this->edit_text = $message;
        }

        if ($_graph_fields !== null) {
            list($graph_fields, $hidden_fields) = $_graph_fields;
            $fields->attach($graph_fields);
            $hidden->attach($hidden_fields);
        }

        $fields->attach(form_input_float(do_lang_tempcode('TARGET'), do_lang_tempcode('DESCRIPTION_KPI_TARGET'), 'target_to_reach', $target, false));

        $fields->attach(form_input_line(do_lang_tempcode('TITLE'), do_lang_tempcode('DESCRIPTION_TITLE'), 'title', is_object($title) ? $title->evaluate() : $title, true));

        $fields->attach(form_input_text(do_lang_tempcode('NOTES'), do_lang_tempcode('DESCRIPTION_NOTES'), 'notes', $notes, false, false));

        return [$fields, $hidden];
    }

    /**
     * Standard crud_module submitter getter.
     *
     * @param  ID_TEXT $id The entry for which the submitter is sought
     * @return array The submitter, and the time of submission (null submission time implies no known submission time)
     */
    public function get_submitter(string $id) : array
    {
        $rows = $GLOBALS['SITE_DB']->query_select('stats_kpis', ['k_submitter', 'k_added_time'], ['id' => intval($id)], '', 1);
        if (!array_key_exists(0, $rows)) {
            return [null, null];
        }
        return [$rows[0]['k_submitter'], $rows[0]['k_added_time']];
    }

    /**
     * Standard crud_module edit form filler.
     *
     * @param  ID_TEXT $id The entry being edited
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function fill_in_edit_form(string $id)
    {
        $rows = $GLOBALS['SITE_DB']->query_select('stats_kpis', ['*'], ['id' => intval($id)], '', 1);
        if (!array_key_exists(0, $rows)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'kpi'));
        }
        $myrow = $rows[0];

        $filters = json_decode($myrow['k_filters'], true);
        $pivot = ($myrow['k_pivot'] == '') ? null : $myrow['k_pivot'];
        return $this->get_form_fields(intval($id), $filters, $pivot, $myrow['k_target'], $myrow['k_title'], $myrow['k_notes']);
    }

    /**
     * Standard crud_module add actualiser.
     *
     * @return array A pair: The entry added, description about usage
     */
    public function add_actualisation() : array
    {
        $graph_name = post_param_string('graph_name');
        $_target = post_param_string('target_to_reach', '');
        $target = ($_target == '') ? null : floatval($_target);
        $title = post_param_string('title');
        $notes = post_param_string('notes');

        list(, $graph_details) = stats_find_graph_details($graph_name, true);
        $filters = [];
        $pivot = null;
        list($filters, $pivot) = _stats_get_graph_context($graph_details, $filters, $pivot, true);

        $id = $GLOBALS['SITE_DB']->query_insert('stats_kpis', [
            'k_graph_name' => $graph_name,
            'k_pivot' => ($pivot === null) ? '' : $pivot,
            'k_filters' => json_encode($filters),
            'k_target' => $target,
            'k_title' => $title,
            'k_added_time' => time(),
            'k_submitter' => get_member(),
            'k_notes' => $notes,
        ], true);

        log_it('ADD_KPI', strval($id), $title);

        return [strval($id), null];
    }

    /**
     * Standard crud_module edit actualiser.
     *
     * @param  ID_TEXT $id The entry being edited
     * @return ?Tempcode Description about usage (null: none)
     */
    public function edit_actualisation(string $id) : ?object
    {
        $graph_name = post_param_string('graph_name');
        $_target = post_param_string('target_to_reach', '');
        $target = ($_target == '') ? null : floatval($_target);
        $title = post_param_string('title');
        $notes = post_param_string('notes');

        list(, $graph_details) = stats_find_graph_details($graph_name, true);
        $filters = [];
        $pivot = null;
        list($filters, $pivot) = _stats_get_graph_context($graph_details, $filters, $pivot, true);

        $GLOBALS['SITE_DB']->query_update('stats_kpis', [
            'k_graph_name' => $graph_name,
            'k_pivot' => ($pivot === null) ? '' : $pivot,
            'k_filters' => json_encode($filters),
            'k_target' => $target,
            'k_title' => $title,
            'k_notes' => $notes,
        ], [
            'id' => intval($id),
        ], '', 1);

        log_it('EDIT_KPI', strval($id), $title);

        return null;
    }

    /**
     * Standard crud_module delete actualiser.
     *
     * @param  ID_TEXT $id The entry being deleted
     */
    public function delete_actualisation(string $id)
    {
        $title = $GLOBALS['SITE_DB']->query_select_value('stats_kpis', 'k_title', ['id' => intval($id)]);

        $GLOBALS['SITE_DB']->query_delete('stats_kpis', [
            'id' => intval($id),
        ], '', 1);

        log_it('DELETE_KPI', strval($id), $title);
    }

    /**
     * The do-next manager for after banner content management (banners only).
     *
     * @param  ?Tempcode $title The title (output of get_screen_title) (null: don't use full page)
     * @param  Tempcode $description Some description to show, saying what happened
     * @param  ?ID_TEXT $id The ID of whatever we are working with (null: deleted)
     * @return Tempcode The UI
     */
    public function do_next_manager(?object $title, object $description, ?string $id = null) : object
    {
        require_code('templates_donext');

        $install_actions = [];
        $category_actions = [];
        $kpi_actions = [];
        $redirect_actions = [];

        $test = $GLOBALS['SITE_DB']->query_select_value_if_there('ip_country', 'id');
        if ($test === null) {
            $install_actions[] = ['menu/adminzone/audit/statistics/geolocate', ['_SELF', ['type' => 'install_data'], '_SELF'], do_lang_tempcode('INSTALL_GEOLOCATION_DATA'), 'DOC_INSTALL_GEOLOCATION_DATA'];
        } else {
            $install_actions[] = ['menu/adminzone/audit/statistics/geolocate', ['_SELF', ['type' => 'install_data'], '_SELF'], do_lang_tempcode('REINSTALL_GEOLOCATION_DATA'), 'DOC_INSTALL_GEOLOCATION_DATA'];
        }

        $hooks = find_all_hook_obs('modules', 'admin_stats_redirects', 'Hook_admin_stats_redirects_');
        foreach ($hooks as $ob) {
            $info = $ob->info();
            if ($info !== null) {
                foreach ($info as $id => $graph_details) {
                    $label = do_lang_tempcode($graph_details['label_lang_string']);
                    $label->attach(' (' . escape_html(integer_format(1)) . ')');
                    $redirect_actions[] = [$graph_details['icon'], ['_SELF', ['type' => 'redirect', 'id' => $id], '_SELF'], protect_from_escaping($label)];
                }
            }
        }

        $hooks = find_all_hook_obs('modules', 'admin_stats', 'Hook_admin_stats_');
        foreach ($hooks as $ob) {
            $info = $ob->info();
            if ($info !== null) {
                foreach ($info as $graph_name => $graph_details) {
                    if ($graph_details['category'] === null) {
                        $label = $graph_details['label'];
                        $label->attach(' (' . escape_html(integer_format(1)) . ')');
                        $category_actions[] = [$graph_details['icon'], ['_SELF', ['type' => 'graph', 'id' => $graph_name], '_SELF'], protect_from_escaping($label)];
                    }
                }
            }
        }

        $categories = stats_find_graph_categories();
        foreach ($categories as $category_name => $category) {
            $graphs = stats_find_graphs_in_category($category_name);
            $label = do_lang_tempcode($category['label_lang_string']);
            $label->attach(' (' . escape_html(integer_format(count($graphs))) . ')');
            $category_actions[] = [$category['icon'], ['_SELF', ['type' => 'category', 'id' => $category_name], '_SELF'], protect_from_escaping($label)];
        }

        $kpi_actions[] = ['admin/view_archive', ['_SELF', ['type' => 'kpis'], '_SELF'], do_lang('VIEW')];

        $kpi_actions[] = ['admin/edit', ['_SELF', ['type' => 'edit'], '_SELF'], do_lang('EDIT_KPI')];

        return do_next_manager(
            get_screen_title('MODULE_TRANS_NAME_admin_stats'),
            $description,
            $install_actions,
            do_lang('SETUP'),
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            $kpi_actions,
            $category_actions,
            $redirect_actions,
            ((!empty($redirect_actions)) ? do_lang('REDIRECTS') : null),
            null,
            do_lang('KPIS'),
            do_lang('CATEGORIES')
        );
    }
}
