<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
/**
 * Privacy class for requesting user data.
 *
 * @package    core_calendar
 * @copyright  2018 Zig Tan <zig@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace core_calendar\privacy;

defined('MOODLE_INTERNAL') || die();

use \core_privacy\local\metadata\collection;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\context;
use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\transform;
use \core_privacy\local\request\writer;
use \core_privacy\local\request\userlist;
use \core_privacy\local\request\approved_userlist;

/**
 * Privacy Subsystem for core_calendar implementing metadata, plugin, and user_preference providers.
 *
 * @package    core_calendar
 * @copyright  2018 Zig Tan <zig@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class provider implements
        \core_privacy\local\metadata\provider,
        \core_privacy\local\request\plugin\provider,
        \core_privacy\local\request\core_userlist_provider,
        \core_privacy\local\request\user_preference_provider
{

    /**
     * Provides meta data that is stored about a user with core_calendar.
     *
     * @param  collection $collection A collection of meta data items to be added to.
     * @return  collection Returns the collection of metadata.
     */
    public static function get_metadata(collection $collection) : collection {
        // The calendar 'event' table contains user data.
        $collection->add_database_table(
            'event',
            [
                'name' => 'privacy:metadata:calendar:event:name',
                'description' => 'privacy:metadata:calendar:event:description',
                'eventtype' => 'privacy:metadata:calendar:event:eventtype',
                'timestart' => 'privacy:metadata:calendar:event:timestart',
                'timeduration' => 'privacy:metadata:calendar:event:timeduration',
            ],
            'privacy:metadata:calendar:event'
        );

        // The calendar 'event_subscriptions' table contains user data.
        $collection->add_database_table(
            'event_subscriptions',
            [
                'name' => 'privacy:metadata:calendar:event_subscriptions:name',
                'url' => 'privacy:metadata:calendar:event_subscriptions:url',
                'eventtype' => 'privacy:metadata:calendar:event_subscriptions:eventtype',
            ],
            'privacy:metadata:calendar:event_subscriptions'
        );

        // The calendar user preference setting 'calendar_savedflt'.
        $collection->add_user_preference(
            'calendar_savedflt',
            'privacy:metadata:calendar:preferences:calendar_savedflt'
        );

        return $collection;
    }

    /**
     * Get the list of contexts that contain calendar user information for the specified user.
     *
     * @param   int $userid The user to search.
     * @return  contextlist   $contextlist  The contextlist containing the list of contexts used in this plugin.
     */
    public static function get_contexts_for_userid(int $userid) : contextlist {
        $contextlist = new contextlist();

        // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
        $params = [
            'sitecontext'        => CONTEXT_SYSTEM,
            'categorycontext'    => CONTEXT_COURSECAT,
            'coursecontext'      => CONTEXT_COURSE,
            'groupcontext'       => CONTEXT_COURSE,
            'usercontext'        => CONTEXT_USER,
            'cuserid'            => $userid,
            'modulecontext'      => CONTEXT_MODULE,
            'muserid'            => $userid
        ];

        // Get contexts of Calendar Events for the owner.
        $sql = "SELECT ctx.id
                  FROM {context} ctx
                  JOIN {event} e ON
                       (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
                       (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
                       (e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
                       (e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
                       (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
                 WHERE e.userid = :cuserid
                 UNION
                SELECT ctx.id
                  FROM {context} ctx
                  JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
                  JOIN {modules} m ON m.id = cm.module
                  JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
                 WHERE e.userid = :muserid";
        $contextlist->add_from_sql($sql, $params);

        // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
        $params = [
            'sitecontext'       => CONTEXT_SYSTEM,
            'categorycontext'   => CONTEXT_COURSECAT,
            'coursecontext'     => CONTEXT_COURSE,
            'groupcontext'      => CONTEXT_COURSE,
            'usercontext'       => CONTEXT_USER,
            'userid'            => $userid
        ];

        // Get contexts for Calendar Subscriptions for the owner.
        $sql = "SELECT ctx.id
                  FROM {context} ctx
                  JOIN {event_subscriptions} s ON
                       (s.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
                       (s.categoryid = ctx.instanceid AND s.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
                       (s.courseid = ctx.instanceid AND s.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
                       (s.courseid = ctx.instanceid AND s.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
                       (s.userid = ctx.instanceid AND s.eventtype = 'user' AND ctx.contextlevel = :usercontext)
                 WHERE s.userid = :userid";
        $contextlist->add_from_sql($sql, $params);

        // Return combined contextlist for Calendar Events & Calendar Subscriptions.
        return $contextlist;
    }

    /**
     * Get the list of users within a specific context.
     *
     * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
     */
    public static function get_users_in_context(userlist $userlist) {
        global $DB;

        $context = $userlist->get_context();

        // Calendar Events can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT),
        // Course and Course Group (CONTEXT_COURSE), User (CONTEXT_USER), or Course Modules (CONTEXT_MODULE) contexts.
        if ($context->contextlevel == CONTEXT_MODULE) {
            $params = ['cmid' => $context->instanceid];

            $sql = "SELECT e.userid
                      FROM {course_modules} cm
                      JOIN {modules} m ON m.id = cm.module
                      JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
                     WHERE cm.id = :cmid";

            $userlist->add_from_sql('userid', $sql, $params);
        } else if ($context->contextlevel == CONTEXT_SYSTEM) {
            // Get contexts of Calendar Events for the owner.
            $sql = "SELECT userid FROM {event} WHERE eventtype = 'site'";
            $userlist->add_from_sql('userid', $sql, []);

            // Get contexts for Calendar Subscriptions for the owner.
            $sql = "SELECT userid FROM {event_subscriptions} WHERE eventtype = 'site'";
            $userlist->add_from_sql('userid', $sql, []);
        } else if (in_array($context->contextlevel, [CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER])) {
            $eventfields = [
                CONTEXT_COURSECAT   => 'categoryid',
                CONTEXT_COURSE      => 'courseid',
                CONTEXT_USER        => 'userid'
            ];
            $eventfield = $eventfields[$context->contextlevel];

            $eventtypes = [
                CONTEXT_COURSECAT   => 'category',
                CONTEXT_COURSE      => ['course' , 'group'],
                CONTEXT_USER        => 'user'
            ];
            list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED);

            $params = $eventtypeparams + ['instanceid' => $context->instanceid];

            // Get contexts of Calendar Events for the owner.
            $sql = "SELECT userid
                      FROM {event}
                     WHERE eventtype $eventtypesql
                           AND $eventfield = :instanceid";
            $userlist->add_from_sql('userid', $sql, $params);

            // Get contexts for Calendar Subscriptions for the owner.
            $sql = "SELECT userid
                      FROM {event_subscriptions}
                     WHERE eventtype $eventtypesql
                           AND $eventfield = :instanceid";
            $userlist->add_from_sql('userid', $sql, $params);
        }
    }

    /**
     * Export all user data for the specified user, in the specified contexts.
     *
     * @param   approved_contextlist $contextlist The approved contexts to export information for.
     */
    public static function export_user_data(approved_contextlist $contextlist) {
        if (empty($contextlist)) {
            return;
        }

        self::export_user_calendar_event_data($contextlist);
        self::export_user_calendar_subscription_data($contextlist);
    }

    /**
     * Export all user preferences for the plugin.
     *
     * @param   int $userid The userid of the user whose data is to be exported.
     */
    public static function export_user_preferences(int $userid) {
        $calendarsavedflt = get_user_preferences('calendar_savedflt', null, $userid);

        if (null !== $calendarsavedflt) {
            writer::export_user_preference(
                'core_calendar',
                'calendarsavedflt',
                $calendarsavedflt,
                get_string('privacy:metadata:calendar:preferences:calendar_savedflt', 'core_calendar')
            );
        }
    }

    /**
     * Delete all Calendar Event and Calendar Subscription data for all users in the specified context.
     *
     * @param   context $context Transform the specific context to delete data for.
     */
    public static function delete_data_for_all_users_in_context(\context $context) {
        // Delete all Calendar Events in the specified context in batches.
        if ($eventids = array_keys(self::get_calendar_event_ids_by_context($context))) {
            self::delete_batch_records('event', 'id', $eventids);
        }

        // Delete all Calendar Subscriptions in the specified context in batches.
        if ($subscriptionids = array_keys(self::get_calendar_subscription_ids_by_context($context))) {
            self::delete_batch_records('event_subscriptions', 'id', $subscriptionids);
        }
    }

    /**
     * Delete multiple users within a single context.
     *
     * @param approved_userlist $userlist The approved context and user information to delete information for.
     */
    public static function delete_data_for_users(approved_userlist $userlist) {
        $context = $userlist->get_context();
        $userids = $userlist->get_userids();

        $allowedcontexts = [
            CONTEXT_SYSTEM,
            CONTEXT_COURSECAT,
            CONTEXT_COURSE,
            CONTEXT_MODULE,
            CONTEXT_USER
        ];

        if (!in_array($context->contextlevel, $allowedcontexts)) {
            return;
        }

        if (empty($userids)) {
            return;
        }

        // Delete all Calendar Events in the specified context in batches.
        if ($eventids = array_keys(self::get_calendar_event_ids_by_context($context, $userids))) {
            self::delete_batch_records('event', 'id', $eventids);
        }

        // Delete all Calendar Subscriptions in the specified context in batches.
        if ($subscriptionids = array_keys(self::get_calendar_subscription_ids_by_context($context, $userids))) {
            self::delete_batch_records('event_subscriptions', 'id', $subscriptionids);
        }
    }

    /**
     * Delete all user data for the specified user, in the specified contexts.
     *
     * @param   approved_contextlist $contextlist The approved contexts and user information to delete information for.
     */
    public static function delete_data_for_user(approved_contextlist $contextlist) {
        if (empty($contextlist)) {
            return;
        }

        // Delete all Calendar Events for the owner and specified contexts in batches.
        $eventdetails = self::get_calendar_event_details_by_contextlist($contextlist);
        $eventids = [];
        foreach ($eventdetails as $eventdetail) {
            $eventids[] = $eventdetail->eventid;
        }
        $eventdetails->close();
        self::delete_batch_records('event', 'id', $eventids);

        // Delete all Calendar Subscriptions for the owner and specified contexts in batches.
        $subscriptiondetails = self::get_calendar_subscription_details_by_contextlist($contextlist);
        $subscriptionids = [];
        foreach ($subscriptiondetails as $subscriptiondetail) {
            $subscriptionids[] = $subscriptiondetail->subscriptionid;
        }
        $subscriptiondetails->close();
        self::delete_batch_records('event_subscriptions', 'id', $subscriptionids);
    }

    /**
     * Helper function to export Calendar Events data by a User's contextlist.
     *
     * @param approved_contextlist $contextlist
     * @throws \coding_exception
     */
    protected static function export_user_calendar_event_data(approved_contextlist $contextlist) {
        // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
        $eventdetails = self::get_calendar_event_details_by_contextlist($contextlist);

        // Multiple Calendar Events of the same eventtype and time can exist for a context, so collate them for export.
        $eventrecords = [];
        foreach ($eventdetails as $eventdetail) {
            // Create an array key based on the contextid, eventtype, and time.
            $key = $eventdetail->contextid . $eventdetail->eventtype . $eventdetail->timestart;

            if (array_key_exists($key, $eventrecords) === false) {
                $eventrecords[$key] = [ $eventdetail ];
            } else {
                $eventrecords[$key] = array_merge($eventrecords[$key], [$eventdetail]);
            }
        }
        $eventdetails->close();

        // Export Calendar Event data.
        foreach ($eventrecords as $eventrecord) {
            $index = (count($eventrecord) > 1) ? 1 : 0;

            foreach ($eventrecord as $event) {
                // Export the events using the structure Calendar/Events/{datetime}/{eventtype}-event.json.
                $subcontexts = [
                    get_string('calendar', 'calendar'),
                    get_string('events', 'calendar'),
                    date('c', $event->timestart)
                ];
                $name = $event->eventtype . '-event';

                // Use name {eventtype}-event-{index}.json if multiple eventtypes and time exists at the same context.
                if ($index != 0) {
                    $name .= '-' . $index;
                    $index++;
                }

                $eventdetails = (object) [
                    'name' => $event->name,
                    'description' => $event->description,
                    'location' => $event->location,
                    'eventtype' => $event->eventtype,
                    'timestart' => transform::datetime($event->timestart),
                    'timeduration' => $event->timeduration
                ];

                $context = \context::instance_by_id($event->contextid);
                writer::with_context($context)->export_related_data($subcontexts, $name, $eventdetails);
            }
        }
    }

    /**
     * Helper function to export Calendar Subscriptions data by a User's contextlist.
     *
     * @param approved_contextlist $contextlist
     * @throws \coding_exception
     */
    protected static function export_user_calendar_subscription_data(approved_contextlist $contextlist) {
        // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
        $subscriptiondetails = self::get_calendar_subscription_details_by_contextlist($contextlist);

        // Multiple Calendar Subscriptions of the same eventtype can exist for a context, so collate them for export.
        $subscriptionrecords = [];
        foreach ($subscriptiondetails as $subscriptiondetail) {
            // Create an array key based on the contextid and eventtype.
            $key = $subscriptiondetail->contextid . $subscriptiondetail->eventtype;

            if (array_key_exists($key, $subscriptionrecords) === false) {
                $subscriptionrecords[$key] = [ $subscriptiondetail ];
            } else {
                $subscriptionrecords[$key] = array_merge($subscriptionrecords[$key], [$subscriptiondetail]);
            }
        }
        $subscriptiondetails->close();

        // Export Calendar Subscription data.
        foreach ($subscriptionrecords as $subscriptionrecord) {
            $index = (count($subscriptionrecord) > 1) ? 1 : 0;

            foreach ($subscriptionrecord as $subscription) {
                // Export the events using the structure Calendar/Subscriptions/{eventtype}-subscription.json.
                $subcontexts = [
                    get_string('calendar', 'calendar'),
                    get_string('subscriptions', 'calendar')
                ];
                $name = $subscription->eventtype . '-subscription';

                // Use name {eventtype}-subscription-{index}.json if multiple eventtypes exists at the same context.
                if ($index != 0) {
                    $name .= '-' . $index;
                    $index++;
                }

                $context = \context::instance_by_id($subscription->contextid);
                writer::with_context($context)->export_related_data($subcontexts, $name, $subscription);
            }
        }
    }

    /**
     * Helper function to return all Calendar Event id results for a specified context and optionally
     * included user list.
     *
     * @param \context $context
     * @param array $userids
     * @return array|null
     * @throws \dml_exception
     */
    protected static function get_calendar_event_ids_by_context(\context $context, $userids = array()) {
        global $DB;

        // Calendar Events can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT),
        // Course and Course Group (CONTEXT_COURSE), User (CONTEXT_USER), or Course Modules (CONTEXT_MODULE) contexts.
        if (!in_array($context->contextlevel, [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER, CONTEXT_MODULE])) {
            return [];
        }

        $whereusersql = '';
        $userparams = array();
        if (!empty($userids)) {
            list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
            $whereusersql = "AND e.userid {$usersql}";
        }

        if ($context->contextlevel == CONTEXT_MODULE) { // Course Module events.
            $params = ['cmid' => $context->instanceid];

            // Get Calendar Events for the specified Course Module context.
            $sql = "SELECT DISTINCT e.id AS eventid
                      FROM {course_modules} cm
                      JOIN {modules} m ON m.id = cm.module
                      JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
                     WHERE cm.id = :cmid
                           $whereusersql";
        } else if ($context->contextlevel == CONTEXT_SYSTEM) { // Site events.
            $params = [];
            $sql = "SELECT DISTINCT e.id AS eventid
                      FROM {event} e
                     WHERE e.eventtype = 'site'
                           $whereusersql";
        } else { // The rest.
            $eventfields = [
                CONTEXT_COURSECAT   => 'categoryid',
                CONTEXT_COURSE      => 'courseid',
                CONTEXT_USER        => 'userid'
            ];
            $eventfield = $eventfields[$context->contextlevel];

            $eventtypes = [
                CONTEXT_COURSECAT   => 'category',
                CONTEXT_COURSE      => ['course' , 'group'],
                CONTEXT_USER        => 'user'
            ];
            list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED);

            $params = $eventtypeparams + ['instanceid' => $context->instanceid];

            // Get Calendar Events for the specified Moodle context.
            $sql = "SELECT DISTINCT e.id AS eventid
                      FROM {event} e
                     WHERE e.eventtype $eventtypesql
                           AND e.{$eventfield} = :instanceid
                           $whereusersql";
        }

        $params += $userparams;

        return $DB->get_records_sql($sql, $params);
    }

    /**
     * Helper function to return all Calendar Subscription id results for a specified context and optionally
     * included user list.
     *
     * @param \context $context
     * @param array $userids
     * @return array
     * @throws \dml_exception
     */
    protected static function get_calendar_subscription_ids_by_context(\context $context, $userids = array()) {
        global $DB;

        // Calendar Subscriptions can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT),
        // Course and Course Group (CONTEXT_COURSE), or User (CONTEXT_USER) contexts.
        if (!in_array($context->contextlevel, [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER])) {
            return [];
        }

        $whereusersql = '';
        $userparams = array();
        if (!empty($userids)) {
            list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
            $whereusersql = "AND s.userid {$usersql}";
        }

        if ($context->contextlevel == CONTEXT_SYSTEM) {
            $params = [];

            // Get Calendar Subscriptions for the system context.
            $sql = "SELECT DISTINCT s.id AS subscriptionid
                      FROM {event_subscriptions} s
                     WHERE s.eventtype = 'site'
                           $whereusersql";
        } else {
            $eventfields = [
                CONTEXT_COURSECAT   => 'categoryid',
                CONTEXT_COURSE      => 'courseid',
                CONTEXT_USER        => 'userid'
            ];
            $eventfield = $eventfields[$context->contextlevel];

            $eventtypes = [
                CONTEXT_COURSECAT   => 'category',
                CONTEXT_COURSE      => ['course' , 'group'],
                CONTEXT_USER        => 'user'
            ];
            list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED);

            $params = $eventtypeparams + ['instanceid' => $context->instanceid];

            // Get Calendar Subscriptions for the specified context.
            $sql = "SELECT DISTINCT s.id AS subscriptionid
                      FROM {event_subscriptions} s
                     WHERE s.eventtype $eventtypesql
                           AND s.{$eventfield} = :instanceid
                           $whereusersql";
        }

        $params += $userparams;

        return $DB->get_records_sql($sql, $params);
    }

    /**
     * Helper function to return the Calendar Events for a given user and context list.
     *
     * @param approved_contextlist $contextlist
     * @return array
     * @throws \coding_exception
     * @throws \dml_exception
     */
    protected static function get_calendar_event_details_by_contextlist(approved_contextlist $contextlist) {
        global $DB;

        $userid = $contextlist->get_user()->id;

        list($contextsql1, $contextparams1) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
        list($contextsql2, $contextparams2) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);

        // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
        $params = [
            'sitecontext'       => CONTEXT_SYSTEM,
            'categorycontext'   => CONTEXT_COURSECAT,
            'coursecontext'     => CONTEXT_COURSE,
            'groupcontext'      => CONTEXT_COURSE,
            'usercontext'       => CONTEXT_USER,
            'cuserid'           => $userid,
            'modulecontext'     => CONTEXT_MODULE,
            'muserid'           => $userid
        ];
        $params += $contextparams1;
        $params += $contextparams2;

        // Get Calendar Events details for the approved contexts and the owner.
        $sql = "SELECT ctxid as contextid,
                       details.id as eventid,
                       details.name as name,
                       details.description as description,
                       details.location as location,
                       details.eventtype as eventtype,
                       details.timestart as timestart,
                       details.timeduration as timeduration
                  FROM (
                          SELECT e.id AS id,
                                 ctx.id AS ctxid
                            FROM {context} ctx
                      INNER JOIN {event} e ON
                                 (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
                                 (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
                                 (e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
                                 (e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
                                 (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
                           WHERE e.userid = :cuserid
                             AND ctx.id {$contextsql1}
                           UNION
                          SELECT e.id AS id,
                                 ctx.id AS ctxid
                            FROM {context} ctx
                      INNER JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
                      INNER JOIN {modules} m ON m.id = cm.module
                      INNER JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
                           WHERE e.userid = :muserid
                             AND ctx.id {$contextsql2}
                  ) ids
                  JOIN {event} details ON details.id = ids.id
              ORDER BY ids.id";

        return $DB->get_recordset_sql($sql, $params);
    }

    /**
     * Helper function to return the Calendar Subscriptions for a given user and context list.
     *
     * @param approved_contextlist $contextlist
     * @return array
     * @throws \coding_exception
     * @throws \dml_exception
     */
    protected static function get_calendar_subscription_details_by_contextlist(approved_contextlist $contextlist) {
        global $DB;

        $user = $contextlist->get_user();

        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);

        $params = [
            'sitecontext' => CONTEXT_SYSTEM,
            'categorycontext' => CONTEXT_COURSECAT,
            'coursecontext' => CONTEXT_COURSE,
            'groupcontext' => CONTEXT_COURSE,
            'usercontext' => CONTEXT_USER,
            'userid' => $user->id
        ];
        $params += $contextparams;

        // Get Calendar Subscriptions for the approved contexts and the owner.
        $sql = "SELECT DISTINCT
                       c.id as contextid,
                       s.id as subscriptionid,
                       s.name as name,
                       s.url as url,
                       s.eventtype as eventtype
                  FROM {context} c
            INNER JOIN {event_subscriptions} s ON
                       (s.eventtype = 'site' AND c.contextlevel = :sitecontext) OR
                       (s.categoryid = c.instanceid AND s.eventtype = 'category' AND c.contextlevel = :categorycontext) OR
                       (s.courseid = c.instanceid AND s.eventtype = 'course' AND c.contextlevel = :coursecontext) OR
                       (s.courseid = c.instanceid AND s.eventtype = 'group' AND c.contextlevel = :groupcontext) OR
                       (s.userid = c.instanceid AND s.eventtype = 'user' AND c.contextlevel = :usercontext)
                 WHERE s.userid = :userid
                   AND c.id {$contextsql}";

        return $DB->get_recordset_sql($sql, $params);
    }

    /**
     * Helper function to delete records in batches in order to minimise amount of deletion queries.
     *
     * @param string    $tablename  The table name to delete from.
     * @param string    $field      The table column field name to delete records by.
     * @param array     $values     The table column field values to delete records by.
     * @throws \dml_exception
     */
    protected static function delete_batch_records($tablename, $field, $values) {
        global $DB;

        // Batch deletion with an upper limit of 2000 records to minimise the number of deletion queries.
        $batchrecords = array_chunk($values, 2000);

        foreach ($batchrecords as $batchrecord) {
            $DB->delete_records_list($tablename, $field, $batchrecord);
        }
    }
}
