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

/**
 * Module page class.
 */
class Module_tickets
{
    /**
     * 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'] = 7;
        $info['update_require_upgrade'] = true;
        $info['locked'] = false;
        $info['min_cms_version'] = 11.0;
        $info['addon'] = 'tickets';
        return $info;
    }

    /**
     * Uninstall the module.
     */
    public function uninstall()
    {
        $tables = [
            'ticket_types',
            'tickets',
            'ticket_known_emailers',
            'ticket_extra_access',
        ];
        $GLOBALS['SITE_DB']->drop_table_if_exists($tables);

        $privileges = [
            'view_others_tickets',
            'support_operator',
        ];
        delete_privilege($privileges);

        $GLOBALS['SITE_DB']->query_delete('group_category_access', ['module_the_name' => 'tickets']);

        if (get_forum_type() == 'cns') {
            require_lang('tickets');
            $forum_id = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_forums', 'id', ['f_parent_forum_id' => db_get_first_id(), 'f_name' => do_lang('TICKET_FORUM_NAME', null, null, null, get_site_default_lang())]);
            if ($forum_id !== null) {
                require_code('cns_forums_action');
                require_code('cns_forums_action2');
                cns_delete_forum($forum_id);
            }
        }
    }

    /**
     * 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)
    {
        require_lang('tickets');

        if ($upgrade_from === null) {
            $GLOBALS['SITE_DB']->create_table('tickets', [
                'ticket_id' => '*SHORT_TEXT',
                'topic_id' => 'AUTO_LINK',
                'forum_id' => 'AUTO_LINK',
                'ticket_type' => 'AUTO_LINK',
            ], false, false, true);

            $GLOBALS['SITE_DB']->create_table('ticket_types', [
                'id' => '*AUTO',
                'ticket_type_name' => 'SHORT_TRANS',
                'guest_emails_mandatory' => 'BINARY',
                'search_faq' => 'BINARY',
                'cache_lead_time' => '?TIME',
            ]);

            $default_types = [
                'TT_OTHER',
                'TT_COMPLAINT',
            ];
            foreach ($default_types as $ticket_type_name) {
                $map = [
                    'guest_emails_mandatory' => 0,
                    'search_faq' => 0,
                    'cache_lead_time' => null,
                ];
                $map += insert_lang('ticket_type_name', do_lang($ticket_type_name), 1);
                $ticket_type_id = $GLOBALS['SITE_DB']->query_insert('ticket_types', $map, true);

                require_code('permissions2');
                set_global_category_access('tickets', $ticket_type_id);
            }

            add_privilege('SUPPORT_TICKETS', 'view_others_tickets', false);
            add_privilege('SUPPORT_TICKETS', 'support_operator', false);

            if (get_forum_type() == 'cns') {
                $moderator_groups = $GLOBALS['FORUM_DRIVER']->get_moderator_groups();
                $staff_access = [];
                foreach ($moderator_groups as $id) {
                    $staff_access[$id] = 5;
                }
                cns_require_all_forum_stuff();
                require_code('cns_forums_action');
                require_code('cns_forums_action2');
                $GLOBALS['CNS_DRIVER'] = $GLOBALS['FORUM_DRIVER'];
                cns_make_forum(do_lang('TICKET_FORUM_NAME', null, null, null, get_site_default_lang()), '', db_get_first_id() + 1, $staff_access, db_get_first_id());
            }
        }

        if (($upgrade_from === null) || ($upgrade_from < 6)) {
            $GLOBALS['SITE_DB']->create_table('ticket_known_emailers', [
                'email_address' => '*SHORT_TEXT',
                'member_id' => 'MEMBER',
            ], false, false, true);

            $GLOBALS['SITE_DB']->create_table('ticket_extra_access', [
                'ticket_id' => '*SHORT_TEXT',
                'member_id' => '*MEMBER',
            ], false, false, true);
        }

        if (($upgrade_from !== null) && ($upgrade_from < 6)) { // LEGACY
            $GLOBALS['SITE_DB']->delete_index_if_exists('ticket_types', '#ticket_type');
            $GLOBALS['SITE_DB']->alter_table_field('ticket_types', 'ticket_type', '*AUTO', 'id'); // Note that this is not valid code for cross-database support. For future code do it differently, using add_auto_key.
            $GLOBALS['SITE_DB']->add_table_field('ticket_types', 'ticket_type_name', 'SHORT_TRANS', 0);
            $GLOBALS['SITE_DB']->query('UPDATE ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'ticket_types SET ticket_type_name=id');
        }

        if (($upgrade_from !== null) && ($upgrade_from < 5)) { // LEGACY
            $GLOBALS['SITE_DB']->delete_table_field('ticket_types', 'send_sms_to');
        }

        if (($upgrade_from === null) || ($upgrade_from < 7)) {
            $GLOBALS['SITE_DB']->create_index('ticket_known_emailers', 'member_id', ['member_id']);

            // Add ticket type for report_content
            require_lang('tickets');
            $map = [
                'guest_emails_mandatory' => 0,
                'search_faq' => 0,
                'cache_lead_time' => null,
            ];
            $map += insert_lang('ticket_type_name', do_lang('TT_REPORTED_CONTENT'), 1);
            $ticket_type_id = $GLOBALS['SITE_DB']->query_insert('ticket_types', $map, true);
            require_code('permissions2');
            set_global_category_access('tickets', $ticket_type_id);
        }

        if (($upgrade_from !== null) && ($upgrade_from < 7)) { // LEGACY
            rename_config_option('ticket_email_from', 'ticket_mail_email_address');
            rename_config_option('ticket_mail_server', 'ticket_mail_server_host');

            // Changes to support ticket forums
            if ((addon_installed('tickets')) && (get_forum_type() == 'cns')) {
                require_code('tickets');
                require_code('cns_forums_action2');
                $target_forum_id = get_ticket_forum_id(null, false, true);
                if ($target_forum_id !== null) {
                    $forum_id = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_forums', 'id', ['f_name' => 'Reported posts forum']);
                    if ($forum_id !== null) {
                        cns_delete_forum($forum_id, $target_forum_id);
                    }
                    $forum_id = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_forums', 'id', ['f_name' => 'Website "Contact Us" messages']);
                    if ($forum_id !== null) {
                        cns_delete_forum($forum_id, $target_forum_id);
                    }
                }
            }
        }
    }

    /**
     * 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('tickets')) {
            return null;
        }

        if (get_forum_type() == 'none') {
            return null;
        }

        return [
            'browse' => ['SUPPORT_TICKETS', 'menu/site_meta/tickets'],
        ];
    }

    public $title;
    public $ticket_type_id;

    /**
     * Module pre-run function. Allows us to know metadata for <head> before we start streaming output.
     *
     * @return ?Tempcode Tempcode indicating some kind of exceptional output (null: none)
     */
    public function pre_run() : ?object
    {
        $error_msg = new Tempcode();
        if (!addon_installed__messaged('tickets', $error_msg)) {
            return $error_msg;
        }

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

        require_lang('tickets');

        inject_feed_url('?mode=tickets&select=', do_lang('SUPPORT_TICKETS'));

        if ($type == 'browse') {
            if (!is_guest()) {
                // Our tickets
                $default_ticket_type_id = $this->get_ticket_type_id();
                if ($default_ticket_type_id !== null) {
                    inject_feed_url('?mode=tickets&select=' . strval($default_ticket_type_id), do_lang('SUPPORT_TICKETS'));
                }
                $this->ticket_type_id = $default_ticket_type_id;
            }

            $this->title = get_screen_title('SUPPORT_TICKETS');
        }

        if ($type == 'ticket') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('SUPPORT_TICKETS')]]);
        }

        if ($type == 'post') {
            $this->title = get_screen_title('SUPPORT_TICKETS');
        }

        if ($type == 'set_ticket_extra_access' || $type == '_set_ticket_extra_access') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('SUPPORT_TICKETS')], ['_SELF:_SELF:ticket:' . get_param_string('id'), do_lang_tempcode('VIEW_SUPPORT_TICKET')]]);

            $this->title = get_screen_title('SET_TICKET_EXTRA_ACCESS');
        }

        if ($type == 'merge' || $type == '_merge') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('SUPPORT_TICKETS')], ['_SELF:_SELF:ticket:' . get_param_string('from'), do_lang_tempcode('VIEW_SUPPORT_TICKET')]]);

            $this->title = get_screen_title('MERGE_SUPPORT_TICKETS');
        }

        if ($type == 'assign') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('SUPPORT_TICKETS')], ['_SELF:_SELF:ticket:' . get_param_string('ticket_id'), do_lang_tempcode('VIEW_SUPPORT_TICKET')]]);

            $this->title = get_screen_title('TICKET_ASSIGN');
        }

        if ($type == 'unassign') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('SUPPORT_TICKETS')], ['_SELF:_SELF:ticket:' . get_param_string('ticket_id'), do_lang_tempcode('VIEW_SUPPORT_TICKET')]]);

            $this->title = get_screen_title('TICKET_UNASSIGN');
        }

        if ($type == 'edit' || $type == '_edit') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('SUPPORT_TICKETS')], ['_SELF:_SELF:ticket:' . get_param_string('id'), do_lang_tempcode('VIEW_SUPPORT_TICKET')]]);

            $this->title = get_screen_title('EDIT_TICKET');
        }

        return null;
    }

    /**
     * Execute the module.
     *
     * @return Tempcode The result of execution
     */
    public function run() : object
    {
        if (has_no_forum()) {
            warn_exit(do_lang_tempcode('NO_FORUM_INSTALLED'));
        }

        require_javascript('checking');
        require_css('tickets');
        require_code('tickets');
        require_code('tickets2');

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

        if ($type == 'browse') {
            return $this->list_tickets();
        }
        if ($type == 'ticket') {
            return $this->display_ticket();
        }
        if ($type == 'post') {
            return $this->update_ticket();
        }
        if ($type == 'toggle_ticket_closed') {
            return $this->toggle_ticket_closed();
        }
        if ($type == 'set_ticket_extra_access') {
            return $this->set_ticket_extra_access();
        }
        if ($type == '_set_ticket_extra_access') {
            return $this->_set_ticket_extra_access();
        }
        if ($type == 'merge') {
            return $this->merge();
        }
        if ($type == '_merge') {
            return $this->_merge();
        }
        if ($type == 'assign') {
            return $this->assign();
        }
        if ($type == 'unassign') {
            return $this->unassign();
        }
        if ($type == 'edit') {
            return $this->edit();
        }
        if ($type == '_edit') {
            return $this->_edit();
        }

        return new Tempcode();
    }

    /**
     * Find the selected ticket type ID.
     *
     * @return ?AUTO_LINK The ticket type ID (null: none specified)
     */
    protected function get_ticket_type_id() : ?int
    {
        $default_ticket_type_id = either_param_integer('ticket_type_id', null);
        if ($default_ticket_type_id === null) {
            $_default_ticket_type = either_param_string('ticket_type', null);
            if ($_default_ticket_type !== null) {
                $default_ticket_type_id = $GLOBALS['SITE_DB']->query_select_value_if_there('ticket_types', 'id', [$GLOBALS['SITE_DB']->translate_field_ref('ticket_type_name') => $_default_ticket_type]);
                if ($default_ticket_type_id === null) {
                    warn_exit(do_lang_tempcode('CAT_NOT_FOUND', escape_html($_default_ticket_type), 'ticket_type'));
                }
            }
        }
        return $default_ticket_type_id;
    }

    /**
     * The UI to show support tickets we may view.
     *
     * @return Tempcode The UI
     */
    public function list_tickets() : object
    {
        require_code('feedback');

        $only_open = (get_param_integer('open', 1) == 1);

        $message = new Tempcode();
        $links = new Tempcode();
        $existing_ticket_types = [];

        if (is_guest()) {
            $_login_url = build_url(['page' => 'login']);
            $login_url = $_login_url->evaluate();
            $message = do_lang_tempcode('NO_TICKETS_GUESTS', escape_html($login_url));
            $tickets = [];
        } else {
            $ticket_type_id = $this->ticket_type_id;
            $tickets = get_tickets(['ticket_type_id' => $ticket_type_id, 'only_open' => $only_open]);

            // Find all ticket types used
            if ($ticket_type_id === null) {
                $all_tickets = $tickets;
            } else {
                $all_tickets = get_tickets(['only_open' => $only_open]);
            }
            foreach ($all_tickets as $topic) {
                $ticket_id = extract_topic_identifier($topic['description']);
                $ticket_type_id = $GLOBALS['SITE_DB']->query_select_value_if_there('tickets', 'ticket_type', ['ticket_id' => $ticket_id]);
                if ($ticket_type_id !== null) {
                    $existing_ticket_types[] = $ticket_type_id;
                }
            }

            // List tickets
            if ($tickets !== null) {
                if (has_privilege(get_member(), 'support_operator')) {
                    $message = do_lang_tempcode('TICKETS_STAFF');
                } else {
                    $message = do_lang_tempcode('TICKETS_MEMBER');
                }

                foreach ($tickets as $topic) {
                    list($ticket_type_tpl) = $this->render_ticket_row($topic);

                    $links->attach($ticket_type_tpl);
                }
            }
        }

        $map = ['page' => '_SELF', 'type' => 'ticket'];
        $default_ticket_type_id = $this->get_ticket_type_id();
        if ($default_ticket_type_id !== null) {
            $map['ticket_type_id'] = $default_ticket_type_id;
        }
        $add_ticket_url = build_url($map, '_SELF');

        $tpl = do_template('SUPPORT_TICKETS_SCREEN', [
            '_GUID' => 'b208a9f1504d6b8a76400d89a8265d91',
            'TITLE' => $this->title,
            'MESSAGE' => $message,
            'LINKS' => $links,
            'ADD_TICKET_URL' => $add_ticket_url,
            'TYPES' => build_types_list($default_ticket_type_id),
            'OPEN' => $only_open,
        ]);

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl, 30, $tickets);
    }

    /**
     * Render a ticket link row.
     *
     * @param  array $topic Ticket details (from forum API)
     * @return array A tuple: Ticket row (Tempcode), Ticket type (ID), Ticket type (String)
     */
    protected function render_ticket_row(array $topic) : array
    {
        require_code('feedback');

        $ticket_id = extract_topic_identifier($topic['description']);

        $url = build_url(['page' => '_SELF', 'type' => 'ticket', 'id' => $ticket_id], '_SELF');

        $title = $topic['firsttitle'];

        $first_date = get_timezoned_date_time($topic['firsttime']);
        $first_poster_id = isset($topic['firstmemberid']) ? $topic['firstmemberid'] : $GLOBALS['FORUM_DRIVER']->get_member_from_username($topic['firstusername']);
        $first_poster_profile_url = '';
        $first_poster = do_lang('UNKNOWN');
        if ($first_poster_id !== null) {
            $first_poster_profile_url = $GLOBALS['FORUM_DRIVER']->member_profile_url($first_poster_id, true);
            $first_poster = $topic['firstusername'];
        }

        $last_date = get_timezoned_date_time($topic['lasttime']);
        $last_poster_id = isset($topic['lastmemberid']) ? $topic['lastmemberid'] : $GLOBALS['FORUM_DRIVER']->get_member_from_username($topic['lastusername']);
        $last_poster = do_lang('UNKNOWN');
        $last_poster_profile_url = '';
        if ($last_poster_id !== null) {
            $last_poster_profile_url = $GLOBALS['FORUM_DRIVER']->member_profile_url($last_poster_id, true);
            $last_poster = $topic['lastusername'];
        }

        $ticket_type_id = $GLOBALS['SITE_DB']->query_select_value_if_there('tickets', 'ticket_type', ['ticket_id' => $ticket_id]);
        if ($ticket_type_id === null) {
            $ticket_type_name = do_lang('UNKNOWN');
        } else {
            $ticket_type_details = get_ticket_type($ticket_type_id);
            $ticket_type_name = $ticket_type_details['ticket_type_name_trans'];
        }

        $assigned = find_ticket_assigned_to($ticket_id);

        if (function_exists('get_homesite_support_timings_wrap')) { // FUDGE. Extra code may be added in for the homesite's ticket system
            $extra_details = get_homesite_support_timings_wrap($topic['closed'] == 0, $topic['id'], $ticket_type_name);
        } else {
            $extra_details = new Tempcode();
        }

        $num_posts = $topic['num'] - 1;

        $tpl = do_template('SUPPORT_TICKET_LINK', [
            '_GUID' => '4a39a6b5a7d56ead2d9c20b8a7a71398',
            '_NUM_POSTS' => strval($num_posts),
            'NUM_POSTS' => integer_format($num_posts, 0),
            'CLOSED' => strval($topic['closed']),
            'URL' => $url,
            'ID' => $ticket_id,
            'TITLE' => $title,
            'EXTRA_DETAILS' => $extra_details,
            'TICKET_TYPE_NAME' => $ticket_type_name,
            'TICKET_TYPE_ID' => ($ticket_type_id === null) ? '' : strval($ticket_type_id),
            'FIRST_DATE' => $first_date,
            'FIRST_DATE_RAW' => strval($topic['firsttime']),
            'FIRST_POSTER_PROFILE_URL' => $first_poster_profile_url,
            'FIRST_POSTER' => $first_poster,
            'FIRST_POSTER_ID' => strval($first_poster_id),
            'LAST_DATE' => $last_date,
            'LAST_DATE_RAW' => strval($topic['lasttime']),
            'LAST_POSTER_PROFILE_URL' => $last_poster_profile_url,
            'LAST_POSTER' => $last_poster,
            'LAST_POSTER_ID' => strval($last_poster_id),
            'ASSIGNED' => $assigned,
        ]);

        return [$tpl, $ticket_type_id, $ticket_type_name];
    }

    /**
     * The UI to either show an existing ticket and allow a reply, or to start a new ticket.
     *
     * @return Tempcode The UI
     */
    public function display_ticket() : object
    {
        require_lang('comcode');
        require_code('form_templates');
        require_code('feedback');

        list($warning_details, $ping_url) = handle_conflict_resolution(null, true);

        // Help text
        $ticket_page_text = comcode_to_tempcode(get_option('ticket_text'), null, true);

        // Selection of ticket type for a new ticket and link to add another
        $default_ticket_type_id = $this->get_ticket_type_id();
        $types = build_types_list($default_ticket_type_id);
        $add_ticket_url = build_url(['page' => '_SELF', 'type' => 'ticket', 'ticket_type_id' => $default_ticket_type_id], '_SELF');

        // Read in ticket ID
        $ticket_id = get_param_string('id', null);
        if ($ticket_id == '') {
            $ticket_id = null;
        }
        $starting_new_ticket = ($ticket_id === null);

        $whos_read = [];

        if ($starting_new_ticket) {
            // New ticket, generate an ID...

            if (has_privilege(get_member(), 'support_operator')) {
                $ticket_owner = get_param_integer('post_as', get_member());
            } else {
                $ticket_owner = get_member();
            }

            $ticket_id = ticket_generate_new_id($ticket_owner);

            // Screen title...

            if ($ticket_owner != get_member()) {
                $this->title = get_screen_title('ADD_TICKET_AS', true, [escape_html($GLOBALS['FORUM_DRIVER']->get_username($ticket_owner))]);
            } else {
                $this->title = get_screen_title('ADD_TICKET');
            }

            // Defaults for all...

            $ticket_posts = [];
            $ticket_type_id = null;
            $rendered_ticket_posts = new Tempcode();
            $has_staff_only = false;
            $ticket_type_details = get_ticket_type($default_ticket_type_id);
            $serialized_options = null;
            $hash = null;
            $staff_details = new Tempcode();
            $pagination = null;
        } else {
            // Existing ticket, check access...

            if (is_guest()) {
                // Guest has posted ticket successfully. Actually, this code problem never runs (as they in fact see a separate screen from update_ticket), but it's here as a fail safe.
                return inform_screen(get_screen_title('ADD_TICKET'), do_lang_tempcode('SUCCESS'));
            }

            $_temp = explode('_', $ticket_id, 2);
            if (!isset($_temp[1])) {
                warn_exit(do_lang_tempcode('INTERNAL_ERROR')); // Normal topic, not a ticket!
            }

            $ticket_owner = intval($_temp[0]);

            check_ticket_access($ticket_id);

            // Find existing posts/info...

            $num_to_show_limit = get_param_integer('max_comments', intval(get_option('comments_to_show_in_thread')));
            $start = get_param_integer('start_comments', 0);

            $ticket_type_id = $GLOBALS['SITE_DB']->query_select_value_if_there('tickets', 'ticket_type', ['ticket_id' => $ticket_id]);
            $ticket_type_details = get_ticket_type($ticket_type_id);
            $ticket_type_name = $ticket_type_details['ticket_type_name_trans'];

            $forum = 1; // Returned by reference
            $topic_id = 1; // Returned by reference
            $total_ticket_posts = 1; // Returned by reference
            $ticket_posts = get_ticket_posts($ticket_id, $forum, $topic_id, $total_ticket_posts, $start, $num_to_show_limit);
            if (empty($ticket_posts)) {
                warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'ticket'));
            }

            $ticket_title = $ticket_posts[0]['title'];

            // Screen title...

            $this->title = get_screen_title('_VIEW_SUPPORT_TICKET', true, [escape_html($ticket_title), escape_html($ticket_type_name)]);

            breadcrumb_set_self($ticket_title);

            // Render posts...

            require_code('topics');
            $renderer = new CMS_Topic();
            $renderer->set_rendering_context('tickets');
            $renderer->inject_posts_for_scoring_algorithm($ticket_posts);
            $renderer->topic_id = $topic_id;

            $topic_info = null;
            if (get_forum_type() == 'cns') {
                $_topic_info = $GLOBALS['FORUM_DB']->query_select('f_topics', ['*'], ['id' => $topic_id], '', 1);
                if (!array_key_exists(0, $_topic_info)) {
                    warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
                }
                $topic_info = $_topic_info[0];
            }

            $max_thread_depth = get_param_integer('max_thread_depth', intval(get_option('max_thread_depth')));
            list($rendered_ticket_posts, $serialized_options, $hash) = $renderer->render_posts($num_to_show_limit, $max_thread_depth, true, $ticket_owner, [], $forum, $topic_info);

            // Pagination...

            $pagination = null;
            if ((!$renderer->is_threaded) && ($total_ticket_posts > $num_to_show_limit)) {
                require_code('templates_pagination');
                $pagination = pagination(do_lang_tempcode('COMMENTS'), $start, 'start_comments', $num_to_show_limit, 'max_comments', $total_ticket_posts);
            }

            // Special staff links...

            if (has_privilege(get_member(), 'support_operator')) {
                $topic_url = $GLOBALS['FORUM_DRIVER']->topic_url($topic_id, get_option('ticket_forum_name'), true);
                $staff_details = is_object($topic_url) ? $topic_url : make_string_tempcode($topic_url);
            } else {
                $staff_details = new Tempcode();
            }

            $has_staff_only = ((get_forum_type() == 'cns') && ($GLOBALS['FORUM_DRIVER']->is_staff(get_member())));

            // Find who's read this...

            if (get_forum_type() == 'cns') {
                $rows = $GLOBALS['FORUM_DB']->query_select('f_read_logs', ['l_member_id', 'l_time'], ['l_topic_id' => $topic_id]);
                foreach ($rows as $row) {
                    if (is_guest($row['l_member_id'])) {
                        continue;
                    }

                    $username = $GLOBALS['FORUM_DRIVER']->get_username($row['l_member_id'], false, USERNAME_DEFAULT_NULL);
                    if ($username === null) {
                        continue;
                    }

                    $member_url = $GLOBALS['FORUM_DRIVER']->member_profile_url($row['l_member_id'], true);

                    $date = get_timezoned_date_time($row['l_time']);

                    $whos_read[] = [
                        'USERNAME' => $username,
                        'MEMBER_ID' => strval($row['l_member_id']),
                        'MEMBER_URL' => $member_url,
                        'DATE' => $date,
                    ];
                }
            }

            // Metadata...

            set_extra_request_metadata([
                'created' => date('Y-m-d', $ticket_posts[0]['date']),
                'creator' => $GLOBALS['FORUM_DRIVER']->get_username($ticket_posts[0]['member']),
                'type' => 'Support ticket',
                'title' => $ticket_posts[0]['title'],
                'identifier' => '_SEARCH:tickets:ticket:' . $ticket_id,
            ]);
        }

        // Posting form...

        $emoticons = $GLOBALS['FORUM_DRIVER']->get_emoticon_chooser();
        require_code('form_templates');
        list($attachments, $attach_size_field) = (get_forum_type() == 'cns') ? get_attachments('post') : [null, null];
        if (addon_installed('captcha')) {
            require_code('captcha');
            $use_captcha = ((get_option('captcha_on_feedback') == '1') && (use_captcha()));
            if ($use_captcha) {
                generate_captcha();
            }
        } else {
            $use_captcha = false;
        }

        $comment_form = do_template('COMMENTS_POSTING_FORM', [
            '_GUID' => 'aaa32620f3eb68d9cc820b18265792d7',
            'TITLE' => do_lang_tempcode($starting_new_ticket ? 'CREATE_TICKET_MAKE_POST' : 'REPLY'),
            'USE_CAPTCHA' => $use_captcha,
            'GET_EMAIL' => is_guest(),
            'EMAIL_OPTIONAL' => ((is_guest()) && ($ticket_type_details['guest_emails_mandatory'] == 0)),
            'GET_TITLE' => $starting_new_ticket,
            'TITLE_OPTIONAL' => !$starting_new_ticket,
            'DEFAULT_TITLE' => '',
            'DEFAULT_POST' => either_param_string('post', null),
            'POST_WARNING' => '',
            'RULES_TEXT' => '',
            'ATTACHMENTS' => $attachments,
            'ATTACH_SIZE_FIELD' => $attach_size_field,
            'TRUE_ATTACHMENT_UI' => true,
            'EMOTICONS' => $emoticons,
            'DISPLAY' => 'block',
            'FIRST_POST_URL' => '',
            'FIRST_POST' => '',
            'COMMENT_URL' => '',
            'SUBMIT_NAME' => do_lang_tempcode($starting_new_ticket ? 'CREATE_SUPPORT_TICKET' : 'MAKE_POST'),
            'ANALYTIC_EVENT_CATEGORY' => do_lang('CREATE_SUPPORT_TICKET'),
            'REVIEWS' => false,
        ]);

        // Show other tickets, plus grab topic row for our ticket...

        $other_tickets = new Tempcode();
        $our_topic = null;
        if (!is_guest($ticket_owner)) {
            $tickets_of_member = get_tickets(['only_owner_id' => $ticket_owner]);
            if ($tickets_of_member !== null) {
                foreach ($tickets_of_member as $topic) {
                    $_id = extract_topic_identifier($topic['description']);

                    list($other_ticket_tpl, $ticket_type_id, $ticket_type_name) = $this->render_ticket_row($topic);

                    if ($ticket_id != $_id) {
                        $other_tickets->attach($other_ticket_tpl);
                    } else {
                        $our_topic = $topic;
                    }
                }
            }
        }

        // Links...

        // Close/Open
        $closed = ($our_topic === null) ? false : ($our_topic['closed'] == 1);
        $toggle_ticket_closed_url = null;
        if ((get_forum_type() == 'cns') && (!$starting_new_ticket)) {
            $toggle_ticket_closed_url = build_url(['page' => '_SELF', 'type' => 'toggle_ticket_closed', 'id' => $ticket_id], '_SELF');
        }
        if ($closed) {
            $new_ticket_url = build_url(['page' => '_SELF', 'type' => 'ticket', 'ticket_type_id' => $ticket_type_id], '_SELF');
            attach_message(do_lang_tempcode('TICKET_IS_CLOSED', escape_html($new_ticket_url->evaluate())), 'notice');
        }

        // Link to edit ticket subject/type
        $edit_url = null;
        if (!$starting_new_ticket) {
            $edit_url = build_url(['page' => '_SELF', 'type' => 'edit', 'id' => $ticket_id], '_SELF');
        }

        // Link to set ticket extra access
        $set_ticket_extra_access_url = null;
        if (!$starting_new_ticket) {
            $set_ticket_extra_access_url = build_url(['page' => '_SELF', 'type' => 'set_ticket_extra_access', 'id' => $ticket_id], '_SELF');
        }

        // Post Templates...

        $post_templates = new Tempcode();
        if (($has_staff_only) && (addon_installed('cns_post_templates')) && (get_forum_type() == 'cns')) {
            require_code('cns_posts_action');
            require_lang('cns_post_templates');

            $forum_id = get_ticket_forum_id($ticket_type_id);

            $templates = cns_get_post_templates($forum_id);
            $_post_templates = new Tempcode();
            foreach ($templates as $template) {
                list($pt_title, $pt_text,) = $template;
                $_post_templates->attach(form_input_list_entry(str_replace("\n", '\n', $pt_text), false, $pt_title));
            }
            if (!$_post_templates->is_empty()) {
                $post_templates2 = form_input_list_entry('', false, do_lang_tempcode('NA_EM'));
                $post_templates2->attach($_post_templates);

                $post_templates = do_template('CNS_POST_TEMPLATE_SELECT', ['_GUID' => 'b670b322b96041db458057432e33cdca', 'SKIP_LABEL' => true, 'LIST' => $post_templates2, 'RESETS' => true]);
            }
        }

        // Assigned to whom...

        $assigned = find_ticket_assigned_to($ticket_id);

        $extra_details = new Tempcode();
        if (function_exists('get_homesite_support_timings_wrap')) { // FUDGE. Extra code may be added in for the homesite's ticket system
            if (!$starting_new_ticket && ($our_topic !== null)) {
                $last_poster_id = isset($our_topic['lastmemberid']) ? $our_topic['lastmemberid'] : $GLOBALS['FORUM_DRIVER']->get_member_from_username($our_topic['lastusername']);
                $extra_details = get_homesite_support_timings_wrap($our_topic['closed'] == 0, $our_topic['id'], $ticket_type_name, true);
            }
        }

        // Render screen...

        $post_url = build_url(['page' => '_SELF', 'type' => 'post', 'id' => $ticket_id, 'redirect' => protect_url_parameter(get_param_string('redirect', null, INPUT_FILTER_URL_INTERNAL)), 'start_comments' => get_param_string('start_comments', null), 'max_comments' => get_param_string('max_comments', null)], '_SELF');

        $tpl = do_template('SUPPORT_TICKET_SCREEN', [
            '_GUID' => 'd21a9d161008c6c44fe7309a14be2c5b',
            'ID' => ($ticket_id === null) ? '' : $ticket_id,
            'SERIALIZED_OPTIONS' => $serialized_options,
            'HASH' => $hash,
            'TOGGLE_TICKET_CLOSED_URL' => $toggle_ticket_closed_url,
            'CLOSED' => $closed,
            'OTHER_TICKETS' => $other_tickets,
            'USERNAME' => $GLOBALS['FORUM_DRIVER']->get_username($ticket_owner),
            'TICKET_TYPE_ID' => ($ticket_type_id === null) ? null : strval($ticket_type_id),
            'PING_URL' => $ping_url,
            'WARNING_DETAILS' => $warning_details,
            'NEW' => $starting_new_ticket,
            'TICKET_PAGE_TEXT' => $ticket_page_text,
            'POST_TEMPLATES' => $post_templates,
            'TYPES' => $types,
            'STAFF_ONLY' => $has_staff_only,
            'POSTER' => $GLOBALS['FORUM_DRIVER']->get_username(get_member()),
            'TITLE' => $this->title,
            'COMMENTS' => $rendered_ticket_posts,
            'COMMENT_FORM' => $comment_form,
            'STAFF_DETAILS' => $staff_details,
            'URL' => $post_url,
            'ADD_TICKET_URL' => $add_ticket_url,
            'PAGINATION' => $pagination,
            'SET_TICKET_EXTRA_ACCESS_URL' => $set_ticket_extra_access_url,
            'EDIT_URL' => $edit_url,
            'ASSIGNED' => $assigned,
            'EXTRA_DETAILS' => $extra_details,
            'WHOS_READ' => $whos_read,
        ]);

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl, 30, is_array($ticket_posts) ? count($ticket_posts) : 0);
    }

    /**
     * Actualise ticket creation/reply, then show the ticket again.
     *
     * @return Tempcode The UI
     */
    public function update_ticket() : object
    {
        @ignore_user_abort(true); // Must keep going till completion

        $ticket_id = get_param_string('id', ticket_generate_new_id());

        $ticket_type_id = $this->get_ticket_type_id();

        // Read in...

        $_title = post_param_string('title');

        $post = post_param_string('post', '');
        if ($post == '') {
            // Maybe we are relaying data into the ticket from elsewhere?
            require_code('mail_forms');
            $details = _form_to_email(
                ['title', 'ticket_type_id', 'ticket_type', 'staff_only', 'faq_searched', 'email', 'close'],
                null,
                '',
                '',
                '',
                '',
                null,
                null,
                true,
                'uploads/tickets'
            );
            list(, $post, , , , , $attachments) = $details;
            if ($post == '') {
                warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', 'post'));
            }
            if (!empty($attachments)) {
                $post .= '[semihtml]';
                foreach ($attachments as $attachment) {
                    list($url) = $attachment;
                    $post .= '[media framed="1" thumb="1"]' . $url . '[/media]';
                }
                $post .= '[/semihtml]';
            }
        }

        $staff_only = post_param_integer('staff_only', 0) == 1;

        $ticket_type_details = get_ticket_type($ticket_type_id);

        // Run checks...

        if ($ticket_type_id === null) {
            // If existing ticket...

            // Check ticket access
            check_ticket_access($ticket_id);
        } else {
            // If new ticket...

            // Spam check
            require_code('antispam');
            inject_action_spamcheck(null, post_param_string('email', null, INPUT_FILTER_POST_IDENTIFIER));

            // Check ticket type access
            if (!has_category_access(get_member(), 'tickets', strval($ticket_type_id))) {
                access_denied('I_ERROR');
            }

            // Check FAQ search results first
            if (($ticket_type_details['search_faq']) && (post_param_integer('faq_searched', 0) == 0)) {
                $results = $this->do_search($this->title, $ticket_id, $post);
                if ($results !== null) {
                    return $results;
                }
            }
        }

        if (addon_installed('captcha')) {
            if (get_option('captcha_on_feedback') == '1') {
                require_code('captcha');
                enforce_captcha();
            }
        }

        $email = post_param_string('email', '', INPUT_FILTER_POST_IDENTIFIER | INPUT_FILTER_EMAIL_ADDRESS);

        // Wrap around e-mail address if needed...
        if ($ticket_type_id !== null) {
            $post = ticket_wrap_with_email_address($post, $email, ($ticket_type_details['guest_emails_mandatory'] == 1));
        }

        // Stats logging...

        if ($ticket_type_id === null) {
            if (addon_installed('stats')) {
                require_code('stats');
                if (is_integer($ticket_type_details['ticket_type_name'])) {
                    $ticket_type_name_default_lang = get_translated_text($ticket_type_details['ticket_type_name'], null, get_site_default_lang());
                } else {
                    $ticket_type_name_default_lang = $ticket_type_details['ticket_type_name'];
                }
                log_stats_event(do_lang('FORM', null, null, null, get_site_default_lang()) . '-' . $ticket_type_name_default_lang);
            }
        }

        // Add post to ticket...

        $ticket_url = ticket_add_post($ticket_id, $ticket_type_id, $_title, $post, $staff_only);

        // Auto-monitor...

        if ((has_privilege(get_member(), 'support_operator')) && (get_option('ticket_auto_assign') == '1')) {
            require_code('notifications');
            set_notifications('ticket_assigned_staff', $ticket_id);
        }

        // Send e-mail...

        // Find true ticket title
        list($ticket_title, $topic_id) = get_ticket_meta_details($ticket_id);

        if (!$staff_only) {
            if ($email == '') {
                $email = $GLOBALS['FORUM_DRIVER']->get_member_email_address(get_member());
            }
            send_ticket_email($ticket_id, $ticket_title, $post, $ticket_url, ($ticket_type_id !== null) ? $email : '', $ticket_type_id, null);
        }

        // Close ticket, if requested...

        if (post_param_integer('close', 0) == 1) {
            if (get_forum_type() == 'cns') {
                $GLOBALS['FORUM_DB']->query_update('f_topics', ['t_is_open' => 0], ['id' => $topic_id], '', 1);
            }
        }

        // Redirect...

        $url = build_url(['page' => '_SELF', 'type' => 'ticket', 'id' => $ticket_id], '_SELF');
        if (is_guest()) {
            $url = build_url(['page' => '_SELF'], '_SELF');
        }
        if (get_param_string('redirect', '', INPUT_FILTER_URL_INTERNAL) != '') {
            $url = make_string_tempcode(get_param_string('redirect', false, INPUT_FILTER_URL_INTERNAL));
        }
        return redirect_screen($this->title, $url, do_lang_tempcode('TICKET_STARTED'));
    }

    /**
     * Actualise to toggle the closed state of a ticket.
     *
     * @return Tempcode The UI
     */
    public function toggle_ticket_closed() : object
    {
        $ticket_id = get_param_string('id');

        check_ticket_access($ticket_id);

        require_code('feedback');

        $details = get_ticket_meta_details($ticket_id);
        if ($details === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'ticket'));
        }
        list(, $topic_id) = $details;

        $is_open = $GLOBALS['FORUM_DB']->query_select_value('f_topics', 't_is_open', ['id' => $topic_id]);
        $GLOBALS['FORUM_DB']->query_update('f_topics', ['t_is_open' => 1 - $is_open], ['id' => $topic_id], '', 1);

        $action = ($is_open == 1) ? 'CLOSE_TICKET' : 'OPEN_TICKET';
        $this->title = get_screen_title($action);

        $url = build_url(['page' => '_SELF', 'type' => 'ticket', 'id' => $ticket_id], '_SELF');
        if (is_guest()) {
            $url = build_url(['page' => '_SELF'], '_SELF');
        }
        return redirect_screen($this->title, $url, do_lang_tempcode('SUCCESS'));
    }

    /**
     * Check for existing FAQs matching a ticket to be submitted, via searching.
     *
     * @param  Tempcode $title Page title
     * @param  string $ticket_id Ticket ID we'd be creating
     * @param  string $search_query Search query
     * @return ?Tempcode The search results (null: could not search)
     */
    public function do_search(object $title, string $ticket_id, string $search_query) : ?object
    {
        if (!addon_installed('catalogues')) {
            return null;
        }
        if (!addon_installed('search')) {
            return null;
        }

        require_code('database_search');
        require_code('search');

        // We don't want to display too many --- just enough to show the top results
        $max = 10;

        // Search under all hooks we've asked to search under
        $results = [];
        require_code('hooks/modules/search/catalogue_entries');
        $object = object_factory('Hook_search_catalogue_entries');
        $info = $object->info();
        if ($info === null) {
            return null;
        }

        // Get the ID of the default FAQ catalogue
        $catalogue_id = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'id', ['c_name' => 'faqs'], '');
        if ($catalogue_id === null) {
            return null;
        }

        // Category filter
        $where_clause = 'r.' . $info['category'] . '=' . strval($catalogue_id);
        list($content_where) = build_content_where($search_query);
        $hook_results = $object->run($search_query, $content_where, $where_clause, '-1', false, false, 100, 0, 'relevance', 'ASC', '', null, null);
        if (($hook_results === null) || (empty($hook_results))) {
            return null;
        }

        foreach ($hook_results as $i => $result) {
            $result['type'] = 'catalogue_entries';
            $hook_results[$i] = $result;
        }

        $results = sort_search_results($hook_results, [], 'ASC');
        $out = build_search_results_interface($results, 0, $max, 'ASC');

        return do_template('SUPPORT_TICKETS_SEARCH_SCREEN', [
            '_GUID' => '427e28208e15494a8f126eb4fb2aa60c',
            'TITLE' => $title,
            'URL' => build_url(['page' => '_SELF', 'type' => 'post', 'id' => $ticket_id], '_SELF'),
            'POST_FIELDS' => build_keep_post_fields(),
            'RESULTS' => $out,
        ]);
    }

    /**
     * UI for setting ticket access.
     *
     * @return Tempcode The UI
     */
    public function set_ticket_extra_access() : object
    {
        require_code('form_templates');

        $ticket_id = get_param_string('id');

        $ticket_owner = check_ticket_access($ticket_id);
        $ticket_owner_username = $GLOBALS['FORUM_DRIVER']->get_username($ticket_owner);

        $post_url = build_url(['page' => '_SELF', 'type' => '_set_ticket_extra_access', 'id' => $ticket_id], '_SELF');

        $submit_name = do_lang_tempcode('SET_TICKET_EXTRA_ACCESS');

        $text = do_lang_tempcode('DESCRIPTION_SET_TICKET_EXTRA_ACCESS', escape_html($ticket_owner_username));

        $fields = new Tempcode();

        $access = [];
        $_access = $GLOBALS['SITE_DB']->query_select('ticket_extra_access', ['member_id'], ['ticket_id' => $ticket_id]);
        foreach ($_access as $a) {
            $access[] = $GLOBALS['FORUM_DRIVER']->get_username($a['member_id'], false, USERNAME_DEFAULT_BLANK);
        }
        $fields->attach(form_input_username_multi(do_lang_tempcode('USERNAME'), '', 'access', $access, 0, true));

        return do_template('FORM_SCREEN', [
            '_GUID' => 'a94d516fc14662908628ff19eb7305b9',
            'TITLE' => $this->title,
            'HIDDEN' => '',
            'TEXT' => $text,
            'FIELDS' => $fields,
            'SUBMIT_ICON' => 'menu/adminzone/security/permissions/privileges',
            'SUBMIT_NAME' => $submit_name,
            'URL' => $post_url,
        ]);
    }

    /**
     * Actualiser for setting ticket access.
     *
     * @return Tempcode The UI
     */
    public function _set_ticket_extra_access() : object
    {
        $ticket_id = get_param_string('id');

        check_ticket_access($ticket_id);

        $GLOBALS['SITE_DB']->query_delete('ticket_extra_access', [
            'ticket_id' => $ticket_id,
        ]);

        foreach ($_POST as $key => $username) {
            if (is_integer($key)) {
                $key = strval($key);
            }

            if (substr($key, 0, strlen('access_')) != 'access_') {
                continue;
            }
            if ($username == '') {
                continue;
            }

            $member_id = $GLOBALS['FORUM_DRIVER']->get_member_from_username($username);
            if ($member_id === null) {
                continue;
            }

            $GLOBALS['SITE_DB']->query_insert('ticket_extra_access', [
                'ticket_id' => $ticket_id,
                'member_id' => $member_id,
            ]);
        }

        $url = build_url(['page' => '_SELF', 'type' => 'ticket', 'id' => $ticket_id], '_SELF');
        if (is_guest()) {
            $url = build_url(['page' => '_SELF'], '_SELF');
        }
        return redirect_screen($this->title, $url, do_lang_tempcode('SUCCESS'));
    }

    /**
     * UI for editing a ticket.
     *
     * @return Tempcode The UI
     */
    public function edit() : object
    {
        require_code('form_templates');

        $ticket_id = get_param_string('id');

        check_ticket_access($ticket_id);

        $post_url = build_url(['page' => '_SELF', 'type' => '_edit', 'id' => $ticket_id], '_SELF');

        list($title) = get_ticket_meta_details($ticket_id);

        $submit_name = do_lang_tempcode('EDIT');

        $text = '';

        $fields = new Tempcode();

        $selected_ticket_type_id = $GLOBALS['SITE_DB']->query_select_value('tickets', 'ticket_type', ['ticket_id' => $ticket_id]);

        if (get_forum_type() == 'cns') {
            $fields->attach(form_input_line(do_lang_tempcode('SUBJECT'), '', 'title', $title, true));
        }

        $ticket_types = build_types_list($selected_ticket_type_id, [$selected_ticket_type_id]);
        $_ticket_types = new Tempcode();
        foreach ($ticket_types as $ticket_type) {
            $_ticket_types->attach(form_input_list_entry($ticket_type['TICKET_TYPE_ID'], $ticket_type['SELECTED'], $ticket_type['NAME']));
        }
        $fields->attach(form_input_list(do_lang_tempcode('TICKET_TYPE'), '', 'ticket_type', $_ticket_types));

        return do_template('FORM_SCREEN', [
            '_GUID' => '1fd8f7eaf5346776ae2362450d81f4c2',
            'TITLE' => $this->title,
            'HIDDEN' => '',
            'TEXT' => $text,
            'FIELDS' => $fields,
            'SUBMIT_ICON' => 'buttons/save',
            'SUBMIT_NAME' => $submit_name,
            'URL' => $post_url,
        ]);
    }

    /**
     * Actualiser for editing a ticket.
     *
     * @return Tempcode The UI
     */
    public function _edit() : object
    {
        $ticket_id = get_param_string('id');

        check_ticket_access($ticket_id);

        $old_ticket_type = $GLOBALS['SITE_DB']->query_select_value('tickets', 'ticket_type', ['ticket_id' => $ticket_id]);
        $ticket_type = post_param_integer('ticket_type');

        list($title, , , $uid) = get_ticket_meta_details($ticket_id);

        if ($old_ticket_type != $ticket_type) {
            // Tell the staff that the ticket has been re-routed

            require_code('notifications');
            $username = $GLOBALS['FORUM_DRIVER']->get_username($uid);
            $ticket_type_details = get_ticket_type($ticket_type);
            $ticket_type_name_new = $ticket_type_details['ticket_type_name_trans'];
            $ticket_type_details = get_ticket_type($old_ticket_type);
            $ticket_type_name_old = $ticket_type_details['ticket_type_name_trans'];

            $subject = do_lang('SUBJECT_TICKET_REROUTED', $title, $username, [$ticket_type_name_new, $ticket_type_name_old]);
            $message = do_notification_lang('BODY_TICKET_REROUTED', comcode_escape($title), comcode_escape($username), [comcode_escape($ticket_type_name_new), comcode_escape($ticket_type_name_old)]);

            dispatch_notification(
                'ticket_new_staff',
                strval($ticket_type),
                $subject,
                $message
            );
        }

        $forum_id = get_ticket_forum_id($ticket_type, true);

        $GLOBALS['SITE_DB']->query_update('tickets', ['ticket_type' => $ticket_type, 'forum_id' => $forum_id], ['ticket_id' => $ticket_id], '', 1);

        if (get_forum_type() == 'cns') {
            $title = post_param_string('title');

            $topic_id = $GLOBALS['SITE_DB']->query_select_value('tickets', 'topic_id', ['ticket_id' => $ticket_id]);
            $post_id = $GLOBALS['FORUM_DB']->query_select_value('f_topics', 't_cache_first_post_id', ['id' => $topic_id]);
            $post_id = $GLOBALS['FORUM_DB']->query_select_value('f_posts', 'MIN(id)', ['p_topic_id' => $topic_id], 'AND id<>' . strval($post_id));
            $GLOBALS['FORUM_DB']->query_update('f_topics', ['t_forum_id' => $forum_id, 't_cache_first_title' => $title], ['id' => $topic_id], '', 1);
            $GLOBALS['FORUM_DB']->query_update('f_posts', ['p_cache_forum_id' => $forum_id], ['p_topic_id' => $topic_id], '', 1);
            $GLOBALS['FORUM_DB']->query_update('f_posts', ['p_title' => $title], ['id' => $post_id], '', 1);
        }

        $url = build_url(['page' => '_SELF', 'type' => 'ticket', 'id' => $ticket_id], '_SELF');
        if (is_guest()) {
            $url = build_url(['page' => '_SELF'], '_SELF');
        }
        return redirect_screen($this->title, $url, do_lang_tempcode('SUCCESS'));
    }

    /**
     * UI for merging one ticket into another.
     *
     * @return Tempcode The UI
     */
    public function merge() : object
    {
        if (!has_privilege(get_member(), 'support_operator')) {
            access_denied('I_ERROR');
        }

        $from = get_param_string('from');
        $to = get_param_string('to');

        list($from_title) = get_ticket_meta_details($from);
        list($to_title) = get_ticket_meta_details($to);

        require_code('templates_confirm_screen');
        $preview = do_lang_tempcode('CONFIRM_MERGE_TICKETS', escape_html($from_title), escape_html($to_title));
        $action_url = build_url(['page' => '_SELF', 'type' => '_merge', 'from' => $from, 'to' => $to], '_SELF');
        return confirm_screen($this->title, $preview, $action_url);
    }

    /**
     * Actualiser for merging one ticket into another.
     *
     * @return Tempcode The UI
     */
    public function _merge() : object
    {
        if (!has_privilege(get_member(), 'support_operator')) {
            access_denied('I_ERROR');
        }

        $from = get_param_string('from');
        $to = get_param_string('to');

        list($from_title, $from_topic_id) = get_ticket_meta_details($from);
        list($to_title) = get_ticket_meta_details($to);

        $ticket_type_id_from = $GLOBALS['SITE_DB']->query_select_value_if_there('tickets', 'ticket_type', ['ticket_id' => $from]);
        $ticket_type_id_to = $GLOBALS['SITE_DB']->query_select_value_if_there('tickets', 'ticket_type', ['ticket_id' => $to]);

        // Add into new ticket
        $forum = 0; // Returned by reference
        $topic_id = 0; // Returned by reference
        $ticket_posts_all = get_ticket_posts($from, $forum, $topic_id);
        if (empty($ticket_posts_all)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'ticket'));
        }
        foreach ($ticket_posts_all as $comment) {
            $ticket_url = ticket_add_post($to, $ticket_type_id_to, $comment['title'], array_key_exists('message_comcode', $comment) ? $comment['message_comcode'] : $comment['message'], isset($comment['staff_only']) && $comment['staff_only'], $comment['member'], $comment['date']);
        }

        // Notification to support operator
        require_code('notifications');
        $subject = do_lang(
            'TICKETS_MERGED_INTO_SUBJECT',
            $from_title,
            $to_title,
            [
                $GLOBALS['FORUM_DRIVER']->get_username(get_member(), true),
                $GLOBALS['FORUM_DRIVER']->get_username(get_member()),
            ],
            get_site_default_lang()
        );
        $message = do_notification_lang(
            'TICKETS_MERGED_INTO_BODY',
            comcode_escape($from_title),
            comcode_escape($to_title),
            [
                comcode_escape($GLOBALS['FORUM_DRIVER']->get_username(get_member(), true)),
                $ticket_url,
                comcode_escape($GLOBALS['FORUM_DRIVER']->get_username(get_member())),
            ],
            get_site_default_lang()
        );
        dispatch_notification(
            'ticket_assigned_staff',
            $to,
            $subject,
            $message
        );

        // Add move notification post into from (old) ticket
        $merge_title = do_lang('TICKETS_MERGED_TITLE');
        $merge_post = do_lang('TICKETS_MERGED_POST', $to_title);
        $ticket_url = ticket_add_post($from, $ticket_type_id_from, $merge_title, $merge_post, false);
        $email = $GLOBALS['FORUM_DRIVER']->get_member_email_address($ticket_posts_all[0]['member']);
        send_ticket_email($from, $merge_title, $merge_post, $ticket_url, $email, null, null);

        // Closed old ticket
        if (get_forum_type() == 'cns') {
            $GLOBALS['FORUM_DB']->query_update('f_topics', ['t_is_open' => 0], ['id' => $from_topic_id], '', 1);
        }

        // Redirect
        require_code('templates_redirect_screen');
        $redirect_url = build_url(['page' => '_SELF', 'type' => 'ticket', 'id' => $to], '_SELF');
        return redirect_screen($this->title, $redirect_url, do_lang_tempcode('SUCCESS'));
    }

    /**
     * Assign a ticket.
     *
     * @return Tempcode The UI
     */
    public function assign() : object
    {
        if (!has_privilege(get_member(), 'support_operator')) {
            access_denied('I_ERROR');
        }

        $ticket_id = get_param_string('ticket_id');

        list($ticket_title) = get_ticket_meta_details($ticket_id);

        $username = post_param_string('username', false, INPUT_FILTER_POST_IDENTIFIER);
        $member_id = $GLOBALS['FORUM_DRIVER']->get_member_from_username($username);
        if ($member_id === null) {
            warn_exit(do_lang_tempcode('_MEMBER_NO_EXIST', escape_html($username)), false, false, 404);
        }

        require_code('notifications');
        set_notifications('ticket_assigned_staff', $ticket_id, $member_id);

        // Notification to support operator that they are assigned
        $_ticket_url = build_url(['page' => '_SELF', 'type' => 'ticket', 'id' => $ticket_id, 'redirect' => null], '_SELF', [], false, true, true);
        $ticket_url = $_ticket_url->evaluate();
        $subject = do_lang(
            'TICKET_ASSIGNED_SUBJECT',
            $ticket_title,
            $GLOBALS['FORUM_DRIVER']->get_username(get_member(), true),
            [
                $GLOBALS['FORUM_DRIVER']->get_username(get_member()),
            ],
            get_site_default_lang()
        );
        $message = do_notification_lang(
            'TICKET_ASSIGNED_BODY',
            comcode_escape($ticket_title),
            comcode_escape($GLOBALS['FORUM_DRIVER']->get_username(get_member(), true)),
            [
                $ticket_url,
                $GLOBALS['FORUM_DRIVER']->get_username(get_member()),
            ],
            get_site_default_lang()
        );
        dispatch_notification(
            'ticket_assigned_staff',
            $ticket_id,
            $subject,
            $message,
            [$member_id]
        );

        // Redirect
        require_code('templates_redirect_screen');
        $redirect_url = build_url(['page' => '_SELF', 'type' => 'ticket', 'id' => $ticket_id], '_SELF');
        return redirect_screen($this->title, $redirect_url, do_lang_tempcode('SUCCESS'));
    }

    /**
     * Unassign a ticket.
     *
     * @return Tempcode The UI
     */
    public function unassign() : object
    {
        if (!has_privilege(get_member(), 'support_operator')) {
            access_denied('I_ERROR');
        }

        $ticket_id = get_param_string('ticket_id');

        list($ticket_title) = get_ticket_meta_details($ticket_id);

        $member_id = get_param_integer('member_id');

        require_code('notifications');

        // Notification to support operator that they are assigned
        $_ticket_url = build_url(['page' => '_SELF', 'type' => 'ticket', 'id' => $ticket_id, 'redirect' => null], '_SELF', [], false, true, true);
        $ticket_url = $_ticket_url->evaluate();
        $subject = do_lang(
            'TICKET_UNASSIGNED_SUBJECT',
            $ticket_title,
            $GLOBALS['FORUM_DRIVER']->get_username(get_member(), true),
            [
                $GLOBALS['FORUM_DRIVER']->get_username(get_member()),
            ],
            get_site_default_lang()
        );
        $message = do_notification_lang(
            'TICKET_UNASSIGNED_BODY',
            comcode_escape($ticket_title),
            comcode_escape($GLOBALS['FORUM_DRIVER']->get_username(get_member(), true)),
            [
                $ticket_url,
                $GLOBALS['FORUM_DRIVER']->get_username(get_member()),
            ],
            get_site_default_lang()
        );
        dispatch_notification(
            'ticket_assigned_staff',
            $ticket_id,
            $subject,
            $message,
            [$member_id],
            null,
            [
                'use_real_from' => true,
            ]
        );

        reset_notifications('ticket_assigned_staff', $ticket_id, $member_id);

        // Redirect
        require_code('templates_redirect_screen');
        $redirect_url = build_url(['page' => '_SELF', 'type' => 'ticket', 'id' => $ticket_id], '_SELF');
        return redirect_screen($this->title, $redirect_url, do_lang_tempcode('SUCCESS'));
    }
}
