<?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/>.
/**
 * Webservice related to courses
 * @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\courseinfo;
use \local_treestudyplan\associationservice;
use \local_treestudyplan\local\helpers\webservicehelper;
use \local_treestudyplan\completionscanner;
use \local_treestudyplan\gradingscanner;
use \core_course_category;

/**
 * Webservice related to courses
 */
class courseservice extends \external_api {
    /**
     * Capability required to edit study plans
     * @var string
     */
    const CAP_EDIT = "local/treestudyplan:editstudyplan";
    /**
     * Capability required to view studyplans (for other users)
     * @var string
     */
    const CAP_VIEW = "local/treestudyplan:viewuserreports";


    /**
     * Return value description for map_categories function
     */
    public static function map_categories_parameters() : \external_function_parameters {
        return new \external_function_parameters( [
            "root_id" => new \external_value(PARAM_INT, 'root category to use as base', VALUE_DEFAULT),
         ] );
    }

    /**
     * Parameter description for map_categories function
     */
    public static function map_categories_returns() : \external_description {
        return new \external_multiple_structure(static::map_category_structure(false));
    }

    /**
     * Structure description for category map, used in a number of return descriptions
     * @param bool $lazy
     * @param int $value
     */
    protected static function map_category_structure($lazy = false, $value = VALUE_REQUIRED) {
        $s = [
            "id" => new \external_value(PARAM_INT, 'course category id'),
            "context_id" => new \external_value(PARAM_INT, 'course category context id'),
            "category" => contextinfo::structure(VALUE_OPTIONAL),
            "haschildren" => new \external_value(PARAM_BOOL, 'true if the category has child categories'),
            "hascourses" => new \external_value(PARAM_BOOL, 'true if the category contains courses'),
            "studyplancount" => new \external_value(PARAM_INT, 'number of linked studyplans', VALUE_OPTIONAL),
        ];

        if (!$lazy > 0) {
            $s["courses"] = new \external_multiple_structure( courseinfo::editor_structure() );
            $s["children"] = new \external_multiple_structure( static::map_category_structure(true));
        }
        return  new \external_single_structure($s, "CourseCat info", $value);
    }

    /**
     * Get a category map, and optionally specify a root category to search for
     * User's top category will be used if none specified
     * @param int $rootid Optional starting category for the map
     * @return array
     */
    public static function map_categories($rootid = 0) {
        global $CFG, $DB;

        $root = \core_course_category::get($rootid);
        $context = $root->get_context();
        // Make sure the user has access to the context for editing purposes.
        webservicehelper::require_capabilities(self::CAP_EDIT, $context);

        // Determine top categories from provided context.

        if ($root->id == 0) {
            // On the system level, determine the user's topmost allowed catecories.
            $usertop = \core_course_category::user_top();
            if ($usertop->id == 0) {
                // Top category..
                $children = $root->get_children(); // Returns a list of çore_course_category, let it overwrite $children.
            } else {
                $children = [$usertop];
            }
        } else if ($root->is_uservisible()) {
            $children = [$root];
        }

        foreach ($children as $cat) {
            $list[] = static::map_category($cat, false);
        }
        return $list;
    }

    /**
     * Return value description for get_category function
     */
    public static function get_category_parameters() : \external_function_parameters {
        return new \external_function_parameters( [
            "id" => new \external_value(PARAM_INT, 'id of category'),
        ] );
    }

    /**
     * Parameter description for get_category function
     */
    public static function get_category_returns() : \external_description {
        return static::map_category_structure(false);
    }

    /**
     * Get category info by id
     * @param mixed $id
     * @return array
     */
    public static function get_category($id) {
        $cat = \core_course_category::get($id);
        return static::map_category($cat);
    }

    /**
     * Create a category map, given a specific category
     * @param core_course_category $cat The category to scan
     * @param bool $lazy If lazy loading, do not scan child categories
     * @return array
     */
    protected static function map_category(core_course_category $cat, $lazy = false) {
        global $DB;
        $catcontext = $cat->get_context();
        $ctxinfo = new contextinfo($catcontext);
        $children = $cat->get_children(); // Only shows children visible to the current user.
        $courses = $cat->get_courses();
        $model = [
            "id" => $cat->id,
            "context_id" => $catcontext->id,
            "category" => $ctxinfo->model(),
            "haschildren" => !empty($children),
            "hascourses" => !empty($courses),
        ];

        if (!$lazy) {
            $model["courses"] = [];
            foreach ($courses as $course) {
                $courseinfo = new courseinfo($course->id);
                $model["courses"][] = $courseinfo->editor_model();
            }

            $model["children"] = [];
            foreach ($children as $child) {
                $model["children"][] = static::map_category($child, true);
            }
        }

        return $model;
    }

    /**
     * Return value description for list_accessible_categories function
     */
    public static function list_accessible_categories_parameters() : \external_function_parameters {
        return new \external_function_parameters( [
            "operation" => new \external_value(PARAM_TEXT, 'type of operation ["view"|"edit"]', VALUE_DEFAULT), ]
        );
    }

    /**
     * Parameter description for list_accessible_categories function
     */
    public static function list_accessible_categories_returns() : \external_description {
        return new \external_multiple_structure(static::map_category_structure(true));
    }

    /**
     * [Description for list_accessible_categories]
     * @param string $operation
     * @return array
     */
    public static function list_accessible_categories($operation = "edit") {
        if ($operation == "edit") {
            $capability = self::CAP_EDIT;
        } else { // Operation == "view" || default.
            $capability = self::CAP_VIEW;
        }

        $cats = static::categories_by_capability($capability);

        $list = [];

        foreach ($cats as $cat) {
            $list[] = static::map_category($cat, true);
        }
        return $list;

    }

    /**
     * [Description for categories_by_capability]
     * @param mixed $capability
     * @param core_course_category|null $parent
     * @return array
     */
    public static function categories_by_capability($capability, core_course_category $parent = null) {
        // List the categories in which the user has a specific capability.
        $list = [];
        // Initialize parent if needed.
        if ($parent == null) {
            $parent = \core_course_category::user_top();
            if (has_capability($capability, $parent->get_context())) {
                $list[] = $parent;
            }
        }

        $children = $parent->get_children();
        foreach ($children as $child) {
            // Check if we should add this category.
            if (has_capability($capability, $child->get_context())) {
                $list[] = $child;
                // For optimization purposes, we include all its children now, since they will have inherited the permission.
                // #PREMATURE_OPTIMIZATION  ???.
                $list = array_merge($list, self::recursive_child_categories($child));
            } else {
                if ($child->get_children_count() > 0) {
                    $list = array_merge($list, self::categories_by_capability($capability, $child));
                }
            }
        }

        return $list;
    }

    /**
     * Recursively create a list of all categories unter a specified parent
     * @param core_course_category $parent
     * @return core_course_category[]
     */
    protected static function recursive_child_categories(core_course_category $parent) {
        $list = [];
        $children = $parent->get_children();
        foreach ($children as $child) {
            $list[] = $child;
            if ($child->get_children_count() > 0) {
                $list = array_merge($list, self::recursive_child_categories($child));
            }
        }
        return $list;
    }

    /**
     * Return value description for list_used_categories function
     */
    public static function list_used_categories_parameters() : \external_function_parameters {
        return new \external_function_parameters( [
            "operation" => new \external_value(PARAM_TEXT, 'type of operation ["view"|"edit"]', VALUE_DEFAULT),
        ]);
    }

    /**
     * Parameter description for list_used_categories function
     */
    public static function list_used_categories_returns() : \external_description {
        return new \external_multiple_structure(static::map_category_structure(true));
    }

    /**
     * List all categories available to the current user for editing or viewing studyplans
     * @param string $operation The operation to scan usage for [edit, view]
     * @return array
     */
    public static function list_used_categories($operation = 'edit') {
        global $DB;
        if ($operation == "edit") {
            $capability = self::CAP_EDIT;
        } else { // Operation == "view" || default.
            $capability = self::CAP_VIEW;
        }
        $contextids = [];
        $rs = $DB->get_recordset_sql("SELECT DISTINCT context_id, COUNT(*) as num FROM {local_treestudyplan}
                                         GROUP BY context_id");
        foreach ($rs as $r) {
            $contextids[$r->context_id] = $r->num;
        }
        $rs->close();

        // Now filter the categories that the user has acces to by the used context id's.
        // (That should filter out irrelevant stuff).
        $cats = static::categories_by_capability($capability);

        $list = [];
        foreach ($cats as $cat) {
            $count = 0;
            $ctxid = $cat->get_context()->id;
            if (array_key_exists($ctxid, $contextids)) {
                $count = $contextids[$ctxid];
            }

            $o = static::map_category($cat, true);
            $o["studyplancount"] = $count;
            $list[] = $o;
        }
        return $list;
    }

    /**
     * List all categories available to the current user for editing or viewing studyplans and add information about their usage
     * (Not a webservice function)
     * @param string $operation
     * @return stdClass[]
     */
    public static function list_accessible_categories_with_usage($operation = 'edit') {
        global $DB;
        if ($operation == "edit") {
            $capability = self::CAP_EDIT;
        } else { // Operation == "view" || default.
            $capability = self::CAP_VIEW;
        }
        // Retrieve context ids used.
        $contextids = [];
        $rs = $DB->get_recordset_sql("SELECT DISTINCT context_id, COUNT(*) as num FROM {local_treestudyplan}
                                         GROUP BY context_id");
        foreach ($rs as $r) {
            $contextids[$r->context_id] = $r->num;
        }
        $rs->close();

        // Now filter the categories that the user has acces to by the used context id's.
        // (That should filter out irrelevant stuff).
        $cats = static::categories_by_capability($capability);

        $list = [];
        foreach ($cats as $cat) {
            $count = 0;
            $ctxid = $cat->get_context()->id;
            if (array_key_exists($ctxid, $contextids)) {
                $count = $contextids[$ctxid];
            }
            $o = new \stdClass();
            $o->cat = $cat;
            $o->ctxid = $ctxid;
            $o->count = $count;
            $list[] = $o;
        }
        return $list;
    }

    /**************************************
     *
     * Progress scanners for teacherview
     *
     **************************************/

    /**
     * Return value description for scan_grade_progress function
     * @return external_function_parameters
     */
    public static function scan_grade_progress_parameters() : \external_function_parameters {
        return new \external_function_parameters( [
            "gradeitemid" => new \external_value(PARAM_INT, 'Grade item ID to scan progress for', VALUE_DEFAULT),
            "studyplanid" => new \external_value(PARAM_INT, 'Study plan id to check progress in', VALUE_DEFAULT),

        ]);
    }

    /**
     * Parameter description for scan_grade_progress function
     */
    public static function scan_grade_progress_returns() : \external_description {
        return gradingscanner::structure(VALUE_REQUIRED);
    }

    /**
     * Scan grade item for progress statistics
     * @param mixed $gradeitemid Grade item id
     * @param mixed $studyplanid Id of studyitem the grade is selected in
     * @return array
     */
    public static function scan_grade_progress($gradeitemid, $studyplanid) {
        global $DB;
        // Verify access to the study plan.
        $o = studyplan::find_by_id($studyplanid);
        webservicehelper::require_capabilities(self::CAP_VIEW, $o->context());

        // Retrieve grade item.
        $gi = \grade_item::fetch(["id" => $gradeitemid]);

        // Validate course is linked to studyplan.
        $courseid = $gi->courseid;
        if (!$o->course_linked($courseid)) {
            throw new \webservice_access_exception(
                    "Course {$courseid} linked to grade item {$gradeitemid} is not linked to studyplan {$o->id()}"
                );
        }

        $scanner = new gradingscanner($gi);
        return $scanner->model();
    }

    /**
     * Return value description for scan_completion_progress function
     */
    public static function scan_completion_progress_parameters() : \external_function_parameters {
        return new \external_function_parameters( [
            "criteriaid" => new \external_value(PARAM_INT, 'CriteriaID to scan progress for', VALUE_DEFAULT),
            "studyplanid" => new \external_value(PARAM_INT, 'Study plan id to check progress in', VALUE_DEFAULT),
            "courseid" => new \external_value(PARAM_INT, 'Course id of criteria', VALUE_DEFAULT),
        ]);
    }

    /**
     * Parameter description for scan_completion_progress function
     */
    public static function scan_completion_progress_returns() : \external_description {
        return completionscanner::structure(VALUE_REQUIRED);
    }

    /**
     * Scan criterium for progress statistics
     * @param mixed $criteriaid Id of criterium
     * @param mixed $studyitemid Id of studyplan relevant to this criteria
     * @return array
     */
    public static function scan_completion_progress($criteriaid, $studyitemid) {
        global $DB;
        // Verify access to the study plan.
        $item = studyitem::find_by_id($studyitemid);
        $o = $item->studyline()->studyplan();
        webservicehelper::require_capabilities(self::CAP_VIEW, $o->context());

        $crit = \completion_criteria::fetch(["id" => $criteriaid]);
        $scanner = new completionscanner($crit, $studyitemid);
        return $scanner->model();
    }

    /**
     * Return value description for scan_badge_progress function
     */
    public static function scan_badge_progress_parameters() : \external_function_parameters {
        return new \external_function_parameters( [
            "badgeid" => new \external_value(PARAM_INT, 'Badge to scan progress for', VALUE_DEFAULT),
            "studyplanid" => new \external_value(PARAM_INT,
                                'Study plan id to limit progress search to (to determine which students to scan)', VALUE_DEFAULT),
        ]);
    }

    /**
     * Parameter description for scan_badge_progress function
     */
    public static function scan_badge_progress_returns() : \external_description {
        return new \external_single_structure([
            "total" => new \external_value(PARAM_INT, 'Total number of students scanned'),
            "issued" => new \external_value(PARAM_INT, 'Number of issued badges'),
        ]);
    }

    /**
     * Scan badge for completion progress statistica
     * @param mixed $badgeid ID of the badge
     * @param mixed $studyplanid ID of the relevant study plan
     * @return array
     */
    public static function scan_badge_progress($badgeid, $studyplanid) {
        global $DB;
        // Check access to the study plan.
        $o = studyplan::find_by_id($studyplanid);
        webservicehelper::require_capabilities(self::CAP_VIEW, $o->context());

        // Validate that badge is linked to studyplan.
        if (!$o->badge_linked($badgeid)) {
            throw new \webservice_access_exception("Badge {$badgeid} is not linked to studyplan {$o->id()}");
        }

        // Get badge info.
        $badge = new \core_badges\badge($badgeid);
        $badgeinfo = new badgeinfo($badge);

        // Get the connected users.
        $students = associationservice::all_associated($studyplanid);
        // Just get the user ids.
        $studentids = array_map(function ($a) {
            return $a["id"];
        }, $students);

        return [
            "total" => count($studentids),
            "issued" => $badgeinfo->count_issued($studentids),
        ];
    }
}