<?php
// This file is part of the Studyplan plugin for Moodle
//
// 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 <https://www.gnu.org/licenses/>.

/**
 * Synchronize enrolled cohorts in courses with cohorts associated with studyplans these courses are in
 * @package    local_treestudyplan
 * @copyright  2023 P.M. Kuipers
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

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

require_once($CFG->libdir . '/externallib.php');

use local_treestudyplan\studyplan;

/**
 * Task class to synchronize enrolled cohorts in courses with cohorts associated with studyplans these courses are in
 */
class cascadecohortsync {
    /** Method to use for 'enrolment'
     * @var string
     */
    private const METHOD = "cohort";
    /** @var studyplan */
    private $studyplan;
    /** @var int */
    private $studyplanid;
    /** @var object */
    private $enrol;
    /** @var object */
    private $manualenrol;
    /** @var int */
    private $roleid;
    /** @var array */
    private $cohortids;

    /**
     * Create a synchronization task for a studyplan
     * @param studyplan $studyplan The studyplan to enrol students for
     */
    public function __construct(studyplan $studyplan) {
        $this->studyplan = $studyplan;
        $this->studyplanid = $studyplan->id();
        $this->enrol = \enrol_get_plugin(self::METHOD);
        $this->manualenrol = \enrol_get_plugin("manual");
        // Get the roleid to use for synchronizations.
        $this->roleid = get_config("local_treestudyplan", "csync_roleid");
        // And find the cohorts that are linked to this studyplan.
        $this->cohortids = $this->studyplan->get_linked_cohort_ids();
    }

    /**
     * Remove a value from an array
     * @param array $array The array to process
     * @param mixed $value The value to remove
     * @return array The array with the specified value removed
     */
    private static function array_remove_value($array, $value) {
        $a = [];
        foreach ($array as $v) {
            if ($v != $value) {
                $a[] = $v;
            }
        }
        return $a;
    }

    /**
     * Get or create a group for the cohort to synch to this course
     * @param int $courseid ID of the course
     * @param string $groupname Name of the group
     * @return int Id of the found or created group
     */
    private static function uploadenrolmentmethods_get_group($courseid, $groupname) {
        // Function shamelessly copied from tool/uploadenrolmentmethods/locallib.php.
        global $DB, $CFG;

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

        // Check to see if the group name already exists in this course.
        if ($DB->record_exists('groups', ['name' => $groupname, 'courseid' => $courseid])) {
            $group = $DB->get_record('groups', ['name' => $groupname, 'courseid' => $courseid]);
            return $group->id;
        }
        // The named group doesn't exist, so create a new one in the course.
        $groupdata = new \stdClass();
        $groupdata->courseid = $courseid;
        $groupdata->name = $groupname;
        $groupid = groups_create_group($groupdata);

        if (is_numeric($groupid)) {
            return $groupid;
        } else {
            return 0;
        }
    }

    /**
     * Find all cohort sync instances for a specific course
     * @param int $courseid Id of course to search for
     */
    private function list_cohortsyncs($courseid) {
        global $DB;
        // Find all cohort syncs for this course.
        $searchparams = [
            'courseid' => $courseid,
            'enrol' => self::METHOD,
            'roleid' => get_config("local_treestudyplan", "csync_roleid"),
        ];

        $list = [];
        $records = $DB->get_records("enrol", $searchparams);
        foreach ($records as $instance) {
            if (!empty($instance->customtext4)) {
                // Only add to the list if customtext4 is not empty.
                $list[] = $instance;
            }
        }
        return $list;
    }

    /**
     * Remove this studyplan reference from cohort sync
     * @param object $enrolinstance Enrol instance to unlink or update
     */
    private function unlink_cohortsync($enrolinstance) {
        // So it may or may not need to be removed.
        $plans = json_decode($enrolinstance->customtext4);
        if ($plans !== null && is_array($plans)) {
            // If a valid array is not returned, better leave it be, we don't want to mess with it.
            // Otherwise, check if we should remove it.
            if (in_array($this->studyplanid, $plans)) {
                // If this plan was referenced before.
                // First remove the link.
                $fplans = self::array_remove_value($plans, $this->studyplanid);
                if (count($fplans) == 0) {
                    // Delete the sync if there are no studyplan references left.

                    $this->enrol->delete_instance($enrolinstance);
                } else {
                    // Otherwise just update the references so this studyplan is no longer linked.

                    $this->enrol->update_instance($enrolinstance, (object)["customtext4" => json_encode($fplans)]);
                }
            }
        }
    }

    /**
     * Enroll all cohorts associated to the studyplan in the courses linked to this studyplan
     */
    public function sync() {

        /*  Explainer:
            This script uses {enrol}.customtext4 to store a json array of all studyplans that need this cohort sync to exist.
            Since the cohort-sync enrolment method uses only customint1 and customint2, this is a safe place to store the data.
            (Should the cohortsync script at any future time be modified to use customtext fields, it is still extremely unlikely
            that customtext4 will be used.)
            Because of the overhead involved in keeping an extra table up to date and clean it up if cohort syncs are
            removed outside of this script, it was determined to be the simplest and cleanest solution.
        */

        // Find the study lines associated to this studyplan.
        preloader::preload_courses($this->studyplan);
        $lines = $this->studyplan->get_all_studylines();
        foreach ($lines as $line) {
            $this->syncline($line);
        }
    }

    /**
     * Enroll all cohorts associated to the studyplan in the courses linked to the specified study line
     * @param studyline $line Studyline to sync enrolment for
     */
    public function syncline(studyline $line) {
        global $DB;
        // Find the courses that need to be synced to the associated cohorts.
        $courseids = $line->get_linked_course_ids();

        if ($line->enrollable()) {
            // Since the studyline is enrollable, we need to cascade by student.
            foreach ($courseids as $courseid) {
                /* 1: Associate the users by individual association */
                $course = preloader::get_course($courseid);
                $userids = $line->get_enrolled_userids();
                if (count($userids) > 0) {
                    // Get the manual enrol instance for this course.
                    $instanceparams = ['courseid' => $courseid, 'enrol' => 'manual'];
                    if (!($instance = $DB->get_record('enrol', $instanceparams))) {
                        if ($instanceid = $this->manualenrol->add_default_instance($course)) {
                            $instance = $DB->get_record('enrol', ['id' => $instanceid]);
                        } else {
                            // Instance not added for some reason, so report an error somewhere.
                            // (or not).
                            $instance = null;
                        }
                    }
                    if ($instance !== null) {
                        foreach ($userids as $uid) {
                            // Try a manual registration - it will just be updated if it is already there....
                            $this->manualenrol->enrol_user($instance, $uid, $this->roleid);
                        }
                    }
                }

                /* 2: Remove the cohort sync for this studyplan for this line if it exists,
                        since this course should not have cohort sync form this studyplan
                        Then - if no one uses the link anymore, deactivate it...
                        This does not remove the sync from courses that are unlinked from a studplan.
                        But maybe we do not want that anyway, since it is generally a good idea to keep student
                        access to courses available.
                */

                if (get_config("local_treestudyplan", "csync_autoremove")) {
                    // Only try the autoremove if the option is enabled.
                    foreach ($this->list_cohortsyncs($courseid) as $instance) {
                        $this->unlink_cohortsync($instance);
                    }
                }
            }
        } else {
            // Studyline is not enrollable, we can enrol by cohort...
            foreach ($courseids as $courseid) {
                $course = preloader::get_course($courseid);

                // First create any nonexistent links.
                foreach ($this->cohortids as $cohortid) {
                    $cohort = $DB->get_record('cohort', ['id' => $cohortid]);

                    $instanceparams = [
                        'courseid' => $courseid,
                        'customint1' => $cohortid,
                        'enrol' => self::METHOD,
                        'roleid' => get_config("local_treestudyplan", "csync_roleid"),
                    ];

                    $instancenewparams = [
                        'customint1' => $cohortid,
                        'enrol' => self::METHOD,
                        'roleid' => get_config("local_treestudyplan", "csync_roleid"),
                    ];

                    // Create group: .

                    // 1: check if a link exists.
                    // If not, make it (maybe use some of the custom text to list the studyplans involved).
                    if ($instance = $DB->get_record('enrol', $instanceparams)) {
                        // It already exists.
                        // Check if this studyplan is already referenced in customtext4 in json format.
                        $plans = json_decode($instance->customtext4);
                        if ($plans == false || !is_array(($plans))) {
                            // If the data was not an array (null or garbled), count it as manually added.
                            // This will prevent it's deletion upon.
                            if (get_config("local_treestudyplan", "csync_remember_manual_csync")) {
                                $plans = ["manual"];
                            } else {
                                $plans = [];
                            }
                        }
                        if (!in_array($this->studyplanid, $plans)) {
                            // If not, add it to the reference.

                            $plans[] = (int)($this->studyplanid);
                            $this->enrol->update_instance($instance, (object)["customtext4" => json_encode($plans)]);
                        }
                    } else {
                        // If method members should be added to a group, create it or get its ID.

                        if (get_config("local_treestudyplan", "csync_creategroup")) {
                            // Make or get new new cohort group - but only on creating of instances.
                            $groupname = $cohort->name . " "
                                . \core_text::strtolower(\get_string('defaultgroupname', 'core_group'));

                            // And make sure the group is assigned to all students.
                            $instancenewparams['customint2'] = self::uploadenrolmentmethods_get_group($courseid, $groupname);
                        }

                        if ($instanceid = $this->enrol->add_instance($course, $instancenewparams)) {
                            // Also record the (as of yet only) studyplans id requiring this association.
                            // In the customtext4 field in json format.

                            $instance = $DB->get_record('enrol', ['id' => $instanceid]);
                            $this->enrol->update_instance($instance, (object)[
                                "customtext4" => json_encode([(int)($this->studyplanid)])]);

                            // Successfully added a valid new instance, so now instantiate it.
                            // First synchronise the enrolment.
                            $cohorttrace = new \null_progress_trace();
                            enrol_cohort_sync($cohorttrace, $cohortid);
                            $cohorttrace->finished();
                        }
                    }
                }

                /* 2: Check if there are cohort links for this studyplan in this course that should be removed.
                        A: Check if there are cohort links that are no longer related to this studyplan.
                        B: Check if these links are valid through another studyplan...
                        If no one uses the link anymore, deactivate it...

                        This does not remove the sync from courses that are unlinked from a studplan.
                        But maybe we do not want that anyway, since it is generally a good idea to keep student
                        access to courses available .
                */

                if (get_config("local_treestudyplan", "csync_autoremove")) {
                    // Only try the autoremove if the option is enabled.

                    foreach ($this->list_cohortsyncs($courseid) as $instance) {
                            // Only check the records that have studyplan information in the customtext4 field.
                        // First check if the cohort is not one of the cohort id's we have associated.
                        if (!in_array($instance->customint1, $this->cohortids)) {
                            $this->unlink_cohortsync($instance);
                        }
                    }
                }
            }
        }
    }
}
