<?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/>.

/**
 * Data provider.
 *
 * @package    mod_feedback
 * @copyright  2018 Frédéric Massart
 * @author     Frédéric Massart <fred@branchup.tech>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace mod_feedback\privacy;
defined('MOODLE_INTERNAL') || die();

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

require_once($CFG->dirroot . '/mod/feedback/lib.php');

/**
 * Data provider class.
 *
 * @package    mod_feedback
 * @copyright  2018 Frédéric Massart
 * @author     Frédéric Massart <fred@branchup.tech>
 * @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\core_userlist_provider,
    \core_privacy\local\request\plugin\provider {

    /**
     * Returns metadata.
     *
     * @param collection $collection The initialised collection to add items to.
     * @return collection A listing of user data stored through this system.
     */
    public static function get_metadata(collection $collection): collection {
        $completedfields = [
            'userid' => 'privacy:metadata:completed:userid',
            'timemodified' => 'privacy:metadata:completed:timemodified',
            'anonymous_response' => 'privacy:metadata:completed:anonymousresponse',
        ];

        $collection->add_database_table('feedback_completed', $completedfields, 'privacy:metadata:completed');
        $collection->add_database_table('feedback_completedtmp', $completedfields, 'privacy:metadata:completedtmp');

        $valuefields = [
            'value' => 'privacy:metadata:value:value'
        ];

        $collection->add_database_table('feedback_value', $valuefields, 'privacy:metadata:value');
        $collection->add_database_table('feedback_valuetmp', $valuefields, 'privacy:metadata:valuetmp');

        return $collection;
    }

    /**
     * Get the list of contexts that contain 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 {
        $sql = "
            SELECT DISTINCT ctx.id
              FROM {%s} fc
              JOIN {modules} m
                ON m.name = :feedback
              JOIN {course_modules} cm
                ON cm.instance = fc.feedback
               AND cm.module = m.id
              JOIN {context} ctx
                ON ctx.instanceid = cm.id
               AND ctx.contextlevel = :modlevel
             WHERE fc.userid = :userid";
        $params = ['feedback' => 'feedback', 'modlevel' => CONTEXT_MODULE, 'userid' => $userid];
        $contextlist = new contextlist();
        $contextlist->add_from_sql(sprintf($sql, 'feedback_completed'), $params);
        $contextlist->add_from_sql(sprintf($sql, 'feedback_completedtmp'), $params);
        return $contextlist;
    }

    /**
     * Get the list of users who have data within a 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) {
        $context = $userlist->get_context();

        if (!is_a($context, \context_module::class)) {
            return;
        }

        // Find users with feedback entries.
        $sql = "
            SELECT fc.userid
              FROM {%s} fc
              JOIN {modules} m
                ON m.name = :feedback
              JOIN {course_modules} cm
                ON cm.instance = fc.feedback
               AND cm.module = m.id
              JOIN {context} ctx
                ON ctx.instanceid = cm.id
               AND ctx.contextlevel = :modlevel
             WHERE ctx.id = :contextid";
        $params = ['feedback' => 'feedback', 'modlevel' => CONTEXT_MODULE, 'contextid' => $context->id];

        $userlist->add_from_sql('userid', sprintf($sql, 'feedback_completed'), $params);
        $userlist->add_from_sql('userid', sprintf($sql, 'feedback_completedtmp'), $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) {
        global $DB;

        $user = $contextlist->get_user();
        $userid = $user->id;
        $contextids = array_map(function($context) {
            return $context->id;
        }, array_filter($contextlist->get_contexts(), function($context) {
            return $context->contextlevel == CONTEXT_MODULE;
        }));

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

        $flushdata = function($context, $data) use ($user) {
            $contextdata = helper::get_context_data($context, $user);
            helper::export_context_files($context, $user);
            $mergeddata = array_merge((array) $contextdata, (array) $data);

            // Drop the temporary keys.
            if (array_key_exists('submissions', $mergeddata)) {
                $mergeddata['submissions'] = array_values($mergeddata['submissions']);
            }

            writer::with_context($context)->export_data([], (object) $mergeddata);
        };

        $lastctxid = null;
        $data = (object) [];
        list($sql, $params) = static::prepare_export_query($contextids, $userid);
        $recordset = $DB->get_recordset_sql($sql, $params);
        foreach ($recordset as $record) {
            if ($lastctxid && $lastctxid != $record->contextid) {
                $flushdata(context::instance_by_id($lastctxid), $data);
                $data = (object) [];
            }

            context_helper::preload_from_record($record);
            $id = ($record->istmp ? 'tmp' : 'notmp') . $record->submissionid;

            if (!isset($data->submissions)) {
                $data->submissions = [];
            }

            if (!isset($data->submissions[$id])) {
                $data->submissions[$id] = [
                    'inprogress' => transform::yesno($record->istmp),
                    'anonymousresponse' => transform::yesno($record->anonymousresponse == FEEDBACK_ANONYMOUS_YES),
                    'timemodified' => transform::datetime($record->timemodified),
                    'answers' => []
                ];
            }
            $item = static::extract_item_record_from_record($record);
            $value = static::extract_value_record_from_record($record);
            $itemobj = feedback_get_item_class($record->itemtyp);
            $data->submissions[$id]['answers'][] = [
                'question' => format_text($record->itemname, FORMAT_HTML, [
                    'context' => context::instance_by_id($record->contextid),
                    'para' => false,
                    'noclean' => true,
                ]),
                'answer' => $itemobj->get_printval($item, $value)
            ];

            $lastctxid = $record->contextid;
        }

        if (!empty($lastctxid)) {
            $flushdata(context::instance_by_id($lastctxid), $data);
        }

        $recordset->close();
    }

    /**
     * Delete all data for all users in the specified context.
     *
     * @param context $context The specific context to delete data for.
     */
    public static function delete_data_for_all_users_in_context(\context $context) {
        global $DB;

        // This should not happen, but just in case.
        if ($context->contextlevel != CONTEXT_MODULE) {
            return;
        }

        // Prepare SQL to gather all completed IDs.

        $completedsql = "
            SELECT fc.id
              FROM {%s} fc
              JOIN {modules} m
                ON m.name = :feedback
              JOIN {course_modules} cm
                ON cm.instance = fc.feedback
               AND cm.module = m.id
             WHERE cm.id = :cmid";
        $completedparams = ['cmid' => $context->instanceid, 'feedback' => 'feedback'];

        // Delete temp answers and submissions.
        $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams);
        if (!empty($completedtmpids)) {
            list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED);
            $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams);
            $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams);
        }

        // Delete answers and submissions.
        $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams);
        if (!empty($completedids)) {
            list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED);
            $DB->delete_records_select('feedback_value', "completed $insql", $inparams);
            $DB->delete_records_select('feedback_completed', "id $insql", $inparams);
        }
    }

    /**
     * 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) {
        global $DB;
        $userid = $contextlist->get_user()->id;

        // Ensure that we only act on module contexts.
        $contextids = array_map(function($context) {
            return $context->instanceid;
        }, array_filter($contextlist->get_contexts(), function($context) {
            return $context->contextlevel == CONTEXT_MODULE;
        }));

        // Prepare SQL to gather all completed IDs.
        list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
        $completedsql = "
            SELECT fc.id
              FROM {%s} fc
              JOIN {modules} m
                ON m.name = :feedback
              JOIN {course_modules} cm
                ON cm.instance = fc.feedback
               AND cm.module = m.id
             WHERE fc.userid = :userid
               AND cm.id $insql";
        $completedparams = array_merge($inparams, ['userid' => $userid, 'feedback' => 'feedback']);

        // Delete all submissions in progress.
        $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams);
        if (!empty($completedtmpids)) {
            list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED);
            $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams);
            $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams);
        }

        // Delete all final submissions.
        $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams);
        if (!empty($completedids)) {
            list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED);
            $DB->delete_records_select('feedback_value', "completed $insql", $inparams);
            $DB->delete_records_select('feedback_completed', "id $insql", $inparams);
        }
    }

    /**
     * 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) {
        global $DB;

        $context = $userlist->get_context();
        $userids = $userlist->get_userids();

        // Prepare SQL to gather all completed IDs.
        list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
        $completedsql = "
            SELECT fc.id
              FROM {%s} fc
              JOIN {modules} m
                ON m.name = :feedback
              JOIN {course_modules} cm
                ON cm.instance = fc.feedback
               AND cm.module = m.id
             WHERE cm.id = :instanceid
               AND fc.userid $insql";
        $completedparams = array_merge($inparams, ['instanceid' => $context->instanceid, 'feedback' => 'feedback']);

        // Delete all submissions in progress.
        $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams);
        if (!empty($completedtmpids)) {
            list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED);
            $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams);
            $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams);
        }

        // Delete all final submissions.
        $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams);
        if (!empty($completedids)) {
            list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED);
            $DB->delete_records_select('feedback_value', "completed $insql", $inparams);
            $DB->delete_records_select('feedback_completed', "id $insql", $inparams);
        }
    }

    /**
     * Extract an item record from a database record.
     *
     * @param stdClass $record The record.
     * @return The item record.
     */
    protected static function extract_item_record_from_record(stdClass $record) {
        $newrec = new stdClass();
        foreach ($record as $key => $value) {
            if (strpos($key, 'item') !== 0) {
                continue;
            }
            $key = substr($key, 4);
            $newrec->{$key} = $value;
        }
        return $newrec;
    }

    /**
     * Extract a value record from a database record.
     *
     * @param stdClass $record The record.
     * @return The value record.
     */
    protected static function extract_value_record_from_record(stdClass $record) {
        $newrec = new stdClass();
        foreach ($record as $key => $value) {
            if (strpos($key, 'value') !== 0) {
                continue;
            }
            $key = substr($key, 5);
            $newrec->{$key} = $value;
        }
        return $newrec;
    }

    /**
     * Prepare the query to export all data.
     *
     * Doing it this way allows for merging all records from both the temporary and final tables
     * as most of their columns are shared. It is a lot easier to deal with the records when
     * exporting as we do not need to try to manually group the two types of submissions in the
     * same reported dataset.
     *
     * The ordering may affect performance on large datasets.
     *
     * @param array $contextids The context IDs.
     * @param int $userid The user ID.
     * @return array With SQL and params.
     */
    protected static function prepare_export_query(array $contextids, $userid) {
        global $DB;

        $makefetchsql = function($istmp) use ($DB, $contextids, $userid) {
            $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
            list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);

            $i = $istmp ? 0 : 1;
            $istmpsqlval = $istmp ? 1 : 0;
            $prefix = $istmp ? 'idtmp' : 'id';
            $uniqid = $DB->sql_concat("'$prefix'", 'fc.id');

            $sql = "
                SELECT $uniqid AS uniqid,
                       f.id AS feedbackid,
                       ctx.id AS contextid,

                       $istmpsqlval AS istmp,
                       fc.id AS submissionid,
                       fc.anonymous_response AS anonymousresponse,
                       fc.timemodified AS timemodified,

                       fv.id AS valueid,
                       fv.course_id AS valuecourse_id,
                       fv.item AS valueitem,
                       fv.completed AS valuecompleted,
                       fv.tmp_completed AS valuetmp_completed,

                       $ctxfields
                  FROM {context} ctx
                  JOIN {course_modules} cm
                    ON cm.id = ctx.instanceid
                  JOIN {feedback} f
                    ON f.id = cm.instance
                  JOIN {%s} fc
                    ON fc.feedback = f.id
                  JOIN {%s} fv
                    ON fv.completed = fc.id
                 WHERE ctx.id $insql
                   AND fc.userid = :userid{$i}";

            $params = array_merge($inparams, [
                'userid' . $i => $userid,
            ]);

            $completedtbl = $istmp ? 'feedback_completedtmp' : 'feedback_completed';
            $valuetbl = $istmp ? 'feedback_valuetmp' : 'feedback_value';
            return [sprintf($sql, $completedtbl, $valuetbl), $params];
        };

        list($nontmpsql, $nontmpparams) = $makefetchsql(false);
        list($tmpsql, $tmpparams) = $makefetchsql(true);

        // Oracle does not support UNION on text fields, therefore we must get the itemdescription
        // and valuevalue after doing the union by joining on the result.
        // TODO: Optimise the query, as Oracle-specific constraints no longer apply.
        $sql = "
            SELECT q.*,

                   COALESCE(fv.value, fvt.value) AS valuevalue,

                   fi.id AS itemid,
                   fi.feedback AS itemfeedback,
                   fi.template AS itemtemplate,
                   fi.name AS itemname,
                   fi.label AS itemlabel,
                   fi.presentation AS itempresentation,
                   fi.typ AS itemtyp,
                   fi.hasvalue AS itemhasvalue,
                   fi.position AS itemposition,
                   fi.required AS itemrequired,
                   fi.dependitem AS itemdependitem,
                   fi.dependvalue AS itemdependvalue,
                   fi.options AS itemoptions

              FROM ($nontmpsql UNION $tmpsql) q
         LEFT JOIN {feedback_value} fv
                ON fv.id = q.valueid AND q.istmp = 0
         LEFT JOIN {feedback_valuetmp} fvt
                ON fvt.id = q.valueid AND q.istmp = 1
              JOIN {feedback_item} fi
                ON (fi.id = fv.item OR fi.id = fvt.item)
          ORDER BY q.contextid, q.istmp, q.submissionid, q.valueid";
        $params = array_merge($nontmpparams, $tmpparams);

        return [$sql, $params];
    }
}
