<?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/>.
/**
 * Class to collect course completion info for a given course
 * @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');
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->dirroot.'/course/lib.php');

use core_competency\course_competency;
use core_competency\user_competency_course;
use core_competency\competency;
use core_competency\api as c_api;
use core_competency\competency_rule_points;
use core_competency\evidence;
use core_competency\user_competency;
use stdClass;

/**
 * Class to collect course competencty info for a given course
 */
class coursecompetencyinfo {
    /** @var \stdClass */
    private $course;
    /** @var \course_modinfo */
    private $modinfo;
    /** @var studyitem */
    private $studyitem;

    /**
     * Course id of relevant course
     */
    public function id() {
        return $this->course->id;
    }
    /**
     * Construct new object for a given course
     * @param \stdClass $course Course database record
     * @param studyitem $studyitem Studyitem the course is linked in
     */
    public function __construct($course, $studyitem) {
        global $DB;
        $this->course = $course;
        $this->studyitem = $studyitem;
        $this->completion = new \completion_info($this->course);
        $this->modinfo = get_fast_modinfo($this->course);
    }

    /**
     * Webservice structure for completion stats
     * @param int $value Webservice requirement constant
     */
    public static function completionstats_structure($value = VALUE_OPTIONAL): \external_description {
        return new \external_single_structure([
            "ungraded" => new \external_value(PARAM_INT, 'number of ungraded submissions'),
            "completed" => new \external_value(PARAM_INT, 'number of completed students'),
            "students" => new \external_value(PARAM_INT, 'number of students that should submit'),
            "completed_pass" => new \external_value(PARAM_INT, 'number of completed-pass students'),
            "completed_fail" => new \external_value(PARAM_INT, 'number of completed-fail students'),
        ], "details about gradable submissions", $value);
    }

    /**
     * Convert completion stats to web service model.
     * @param object $stats Stats object
     */
    protected static function completionstats($stats) {
        return [
            "students" => $stats->count,
            "completed" => 0,
            "ungraded" => $stats->nneedreview,
            "completed_pass" => $stats->nproficient,
            "completed_fail" => $stats->nfailed,
        ];
    }

    /**
     * Generic competency info structure for individual competency stats
     * @param bool $recurse True if child competencies may be included
     */
    public static function competencyinfo_structure($recurse=true): \external_description {
        $struct = [
            "id" => new \external_value(PARAM_INT, 'competency id'),
            "title" => new \external_value(PARAM_RAW, 'competency display title'),
            "details" => new \external_value(PARAM_RAW, 'competency details'),
            "description" => new \external_value(PARAM_RAW, 'competency description'),
            "ruleoutcome" => new \external_value(PARAM_TEXT, 'competency rule outcome text', VALUE_OPTIONAL),
            "rule" => new \external_value(PARAM_RAW, 'competency rule description', VALUE_OPTIONAL),
            "path" => new \external_multiple_structure(new \external_single_structure([
                "id" => new \external_value(PARAM_INT),
                "title" => new \external_value(PARAM_RAW),
                "type" => new \external_value(PARAM_TEXT),
            ]), 'competency path'),
            'ucid' => new \external_value(PARAM_INT, 'user competencyid', VALUE_OPTIONAL),
            "grade" => new \external_value(PARAM_TEXT, 'competency grade', VALUE_OPTIONAL),
            "coursegrade" => new \external_value(PARAM_TEXT, 'course competency grade', VALUE_OPTIONAL),
            "proficient" => new \external_value(PARAM_BOOL, 'competency proficiency', VALUE_OPTIONAL),
            "courseproficient" => new \external_value(PARAM_BOOL, 'course competency proficiency', VALUE_OPTIONAL),
            "needreview" => new \external_value(PARAM_BOOL, 'waiting for review or review in progress', VALUE_OPTIONAL),
            "completionstats" => static::completionstats_structure(VALUE_OPTIONAL),
            "required" => new \external_value(PARAM_BOOL, 'if required in parent competency rule', VALUE_OPTIONAL),
            "points" => new \external_value(PARAM_INT, 'number of points in parent competency rule', VALUE_OPTIONAL),
            "progress" => new \external_value(PARAM_INT, 'number completed child competencies/points', VALUE_OPTIONAL),
            "count" => new \external_value(PARAM_INT, 'number of child competencies/points required', VALUE_OPTIONAL),
            "feedback" => new \external_value(PARAM_RAW, 'feedback provided with this competency', VALUE_OPTIONAL),
        ];
        if ($recurse) {
            $struct["children"] = new \external_multiple_structure(
                self::competencyinfo_structure(false), 'child competencies', VALUE_OPTIONAL);
        }
        return new \external_single_structure($struct, 'course completion info');
    }

    /**
     * Webservice structure for editor info
     * @param int $value Webservice requirement constant
     */
    public static function editor_structure($value = VALUE_REQUIRED): \external_description {
        return new \external_single_structure([
           "competencies" => new \external_multiple_structure(self::competencyinfo_structure(), 'competencies'),
           "proficient" => new \external_value(PARAM_INT, 'number of proficient user/competencys ', VALUE_OPTIONAL),
           "total" => new \external_value(PARAM_INT, 'total number of gradable user/competencies', VALUE_OPTIONAL),
        ], 'course completion info', $value);
    }

    /**
     * Webservice structure for userinfo
     * @param int $value Webservice requirement constant
     */
    public static function user_structure($value = VALUE_REQUIRED): \external_description {
        return new \external_single_structure([
            "competencies" => new \external_multiple_structure(self::competencyinfo_structure(), 'competencies'),
            "progress" => new \external_value(PARAM_INT, 'number completed competencies'),
            "count" => new \external_value(PARAM_INT, 'number of competencies', VALUE_OPTIONAL),
        ], 'course completion info', $value);
    }

    /**
     * Create basic competency information model from competency
     * @param object $competency The competency to model
     * @param int|null $userid Optional userid to include completion data for
     */
    private function competencyinfo_model($competency, $userid=null): array {
        $displayfield = get_config("local_treestudyplan", "competency_displayname");
        $detailfield = get_config("local_treestudyplan", "competency_detailfield");
        $headingfield = ($displayfield != 'description') ? $displayfield : "shortname";
        $framework = $competency->get_framework();

        $heading = $framework->get($headingfield);
        if (empty(trim($heading))) {
            $heading = $framework->get('shortname'); // Fall back to shortname if heading field is empty.
        }
        $path = [[
            'id' => $framework->get('id'),
            'title' => $heading,
            'contextid' => $framework->get('contextid'),
            'type' => 'framework',
        ]];
        foreach ($competency->get_ancestors() as $c) {
            $competencypath[] = $c->get('shortname');
            $heading = $c->get($headingfield);
            if (empty(trim($heading))) {
                $heading = $c->get('shortname'); // Fall back to shortname if heading field is empty.
            }
            $path[] = [
                'id' => $c->get('id'),
                'title' => $heading,
                'contextid' => $framework->get('contextid'),
                'type' => 'competency',
            ];
        }

        $heading = $competency->get($headingfield);
        if (empty(trim($heading))) {
            $heading = $competency->get('shortname'); // Fall back to shortname if heading field is empty.
        }
        $path[] = [
            'id' => $competency->get('id'),
            'title' => $heading,
            'contextid' => $framework->get('contextid'),
            'type' => 'competency',
        ];

        $title = $competency->get($displayfield);
        if (empty(trim($title))) {
            $title = $competency->get('shortname'); // Fall back to shortname if heading field is empty.
        }
        $model = [
            'id' => $competency->get('id'),
            'title' => $title,
            'details' => $competency->get($detailfield),
            'description' => $competency->get('description'),
            'path' => $path,
        ];
        if ($userid) {
            $model['ucid'] = self::get_user_competency($userid, $competency->get('id'))->get('id');
        }

        return  $model;
    }

    /**
     * Webservice model for editor info
     * @param int[]|null $studentlist List of user id's to use for checking issueing progress within a study plan
     * @return array Webservice data model
     */
    public function editor_model(?array $studentlist = null) {
        $coursecompetencies = $this->course_competencies();
        // Next create the data model, and check user proficiency for each competency.

        $count = 0;
        $nproficient = 0;

        $cis = [];
        foreach ($coursecompetencies as $c) {
            $ci = $this->competencyinfo_model($c);
            if (!empty($studentlist)) {
                $stats = $this->proficiency_stats($c, $studentlist);
                $count += $stats->count;
                $nproficient += $stats->nproficient;
                // Copy proficiency stats to model.
                foreach ((array)$stats as $key => $value) {
                    $ci[$key] = $value;
                }
                $ci['completionstats'] = self::completionstats($stats);

            }
            $ci['required'] = $this->is_required($c);

            $rule = $c->get_rule_object();
            $ruleoutcome = $c->get('ruleoutcome');
            if ($rule && $ruleoutcome != competency::OUTCOME_NONE) {
                $ruletext = $rule->get_name();
                $ruleconfig = $c->get('ruleconfig');

                if ($ruleoutcome == competency::OUTCOME_EVIDENCE) {
                    $outcometag = "evidence";
                } else if ($ruleoutcome == competency::OUTCOME_COMPLETE) {
                    $outcometag = "complete";
                } else if ($ruleoutcome == competency::OUTCOME_RECOMMEND) {
                    $outcometag = "recommend";
                } else {
                    $outcometag = "none";
                }
                $ci["ruleoutcome"] = get_string("coursemodulecompetencyoutcome_{$outcometag}", "core_competency");

                if ($rule instanceof competency_rule_points) {
                    $ruleconfig = json_decode($ruleconfig);
                    $points = $ruleconfig->base->points;

                    // Make a nice map of the competency rule config.
                    $crlist = [];
                    foreach ($ruleconfig->competencies as $cr) {
                        $crlist[$cr->id] = $cr;
                    }
                    $ci["rule"] = $ruletext . " ({$points} ".get_string("points", "core_grades").")";
                } else {
                    $ci["rule"] = $ruletext;
                }

                // Get one level of children.
                $dids = competency::get_descendants_ids($c);
                if (count($dids) > 0) {
                    $children = [];
                    foreach ($dids as $did) {
                        $cc = new competency($did);
                        $cci = $this->competencyinfo_model($cc);
                        if (!empty($studentlist)) {
                            $stats = $this->proficiency_stats($cc, $studentlist);
                            $cci['completionstats'] = self::completionstats($stats);
                        }
                        if ($rule instanceof competency_rule_points) {
                            if (array_key_exists($did, $crlist)) {
                                $cr = $crlist[$did];
                                $cci["points"] = (int) $cr->points;
                                $cci["required"] = (int) $cr->required;
                            }
                        }
                        $children[] = $cci;
                    }

                    $ci["children"] = $children;
                }
            }
            $cis[] = $ci;
        }
        $info = [
            "competencies" => $cis,
        ];
        if (!empty($studentlist)) {
            $info["proficient"] = $nproficient;
            $info["total"] = $count;
        }
        return $info;
    }

    /**
     * Webservice model for user course completion info
     * @param int $userid ID of user to check specific info for
     * @return array Webservice data model
     */
    public function user_model($userid) {
        $competencies = $this->course_competencies();
        $progress = 0;

        $cis = [];
        foreach ($competencies as $c) {
            $ci = $this->competencyinfo_model($c, $userid);
             // Add user info if $userid is set.
            $p = $this->proficiency($c, $userid);
            // Copy proficiency info to model.
            foreach ((array)$p as $key => $value) {
                $ci[$key] = $value;
            }
            $ci['required'] = $this->is_required($c);
            if ($p->proficient || $p->courseproficient) {
                $progress += 1;
            }

            // Retrieve feedback.
            $ci["feedback"] = $this->retrievefeedback($c, $userid);

            $rule = $c->get_rule_object();
            $ruleoutcome = $c->get('ruleoutcome');
            if ($rule && $ruleoutcome != competency::OUTCOME_NONE) {
                $ruletext = $rule->get_name();
                $ruleconfig = $c->get('ruleconfig');

                if ($ruleoutcome == competency::OUTCOME_EVIDENCE) {
                    $outcometag = "evidence";
                } else if ($ruleoutcome == competency::OUTCOME_COMPLETE) {
                    $outcometag = "complete";
                } else if ($ruleoutcome == competency::OUTCOME_RECOMMEND) {
                    $outcometag = "recommend";
                } else {
                    $outcometag = "none";
                }
                $ci["ruleoutcome"] = get_string("coursemodulecompetencyoutcome_{$outcometag}", "core_competency");

                if ($rule instanceof competency_rule_points) {
                    $ruleconfig = json_decode($ruleconfig);
                    $pointsreq = $ruleconfig->base->points;
                    $points = 0;
                    // Make a nice map of the competency rule config.
                    $crlist = [];
                    foreach ($ruleconfig->competencies as $cr) {
                        $crlist[$cr->id] = $cr;
                    }

                }

                // Get one level of children.
                $dids = competency::get_descendants_ids($c);
                if (count($dids) > 0) {
                    $dcount = 0;
                    $dprogress = 0;
                    $children = [];
                    foreach ($dids as $did) {
                        $cc = new competency($did);
                        $cci = $this->competencyinfo_model($cc, $userid);
                        $cp = $p = $this->proficiency($cc, $userid);
                        // Copy proficiency info to model.
                        foreach ((array)$cp as $key => $value) {
                            $cci[$key] = $value;
                        }
                        // Retrieve feedback.
                        $cci["feedback"] = $this->retrievefeedback($cc, $userid);

                        if ($rule instanceof competency_rule_points) {
                            if (array_key_exists($did, $crlist)) {
                                $cr = $crlist[$did];
                                $cci["points"] = (int) $cr->points;
                                $cci["required"] = (int) $cr->required;
                                if ($cp->proficient) {
                                    $points += (int) $cr->points;
                                }
                            }
                        } else {
                            $dcount += 1;
                            if ($cp->proficient) {
                                $dprogress += 1;
                            }
                        }
                        $children[] = $cci;
                    }

                    $ci["children"] = $children;

                }

                if ($rule instanceof competency_rule_points) {
                    $ci["rule"] = $ruletext . " ({$points} / {$pointsreq} ".get_string("points", "core_grades").")";
                    $ci["count"] = $pointsreq;
                    $ci["progress"] = $points;
                } else {
                    $ci["rule"] = $ruletext;
                    $ci["count"] = $dcount;
                    $ci["progress"] = $dprogress;
                }

            }
            $cis[] = $ci;
        }

        $info = [
            'progress' => $progress,
            "count" => count($competencies),
            "competencies" => $cis,
        ];

        return $info;
    }

    /**
     * Get the course's competencies with user status
     * @return array of Competencies Webservice data model
     */
    public function course_competencies() {
        $list = [];

        $coursecompetencies = course_competency::list_course_competencies($this->course->id);
        $competencies = course_competency::list_competencies($this->course->id);

        foreach ($coursecompetencies as $key => $coursecompetency) {
            $list[] = $competencies[$coursecompetency->get('competencyid')];
        }

        return $list;
    }

    /**
     * Determine proficiency stats
     * @param object $competency
     * @param array $studentlist
     */
    protected function proficiency_stats($competency, $studentlist) {
        $r = new \stdClass();
        $r->count = 0;
        $r->nproficient = 0;
        $r->ncourseproficient = 0;
        $r->nneedreview = 0;
        $r->nfailed = 0;

        foreach ($studentlist as $sid) {
            $p = $this->proficiency($competency, $sid);
            $r->count += 1;
            $r->nproficient += ($p->proficient === true) ? 1 : 0;
            $r->nfailed += ($p->proficient === false) ? 1 : 0;
            $r->ncourseproficient += ($p->courseproficient) ? 1 : 0;
            $r->nneedreview += ($p->needreview) ? 1 : 0;
        }
        return $r;
    }

    /**
     * Get a user competency. (Copied from competency api and stripped out permission checks)
     *
     * @param int $userid The user ID.
     * @param int $competencyid The competency ID.
     * @return user_competency
     */
    public static function get_user_competency($userid, $competencyid) {
        c_api::require_enabled();
        $existing = user_competency::get_multiple($userid, [$competencyid]);
        $uc = array_pop($existing);

        if (!$uc) {
            $uc = user_competency::create_relation($userid, $competencyid);
            $uc->create();
        }

        return $uc;
    }

    /**
     * Get a user competency in a course. (Copied from competency api and stripped out permission checks)
     *
     * @param int $courseid The id of the course to check.
     * @param int $userid The id of the course to check.
     * @param int $competencyid The id of the competency.
     * @return user_competency_course
     */
    public static function get_user_competency_in_course($courseid, $userid, $competencyid) {
        c_api::require_enabled();
        // First we do a permissions check.
        $context = \context_course::instance($courseid);

        // This will throw an exception if the competency does not belong to the course.
        $competency = course_competency::get_competency($courseid, $competencyid);

        $params = ['courseid' => $courseid, 'userid' => $userid, 'competencyid' => $competencyid];
        $exists = user_competency_course::get_record($params);
        // Create missing.
        if ($exists) {
            $ucc = $exists;
        } else {
            $ucc = user_competency_course::create_relation($userid, $competency->get('id'), $courseid);
            $ucc->create();
        }

        return $ucc;
    }

    /**
     * Retrieve course proficiency and overall proficiency for a competency and user
     *
     * @param \core_competency\competency $competency
     * @param int $userid
     *
     * @return stdClass
     *
     */
    public function proficiency($competency, $userid) {
        $scale = $competency->get_scale();
        $competencyid = $competency->get('id');
        $r = new \stdClass();

        $uc = self::get_user_competency($userid, $competencyid);
        $proficiency = $uc->get('proficiency');
        $r->proficient = $proficiency;
        $r->grade = $scale->get_nearest_item($uc->get('grade'));
        $r->needreview = (!($r->proficient) && ($uc->get('status') > user_competency::STATUS_IDLE));
        $r->failed = $proficiency === false;
        try {
            // Only add course grade and proficiency if the competency is included in the course.
            $ucc = self::get_user_competency_in_course($this->course->id, $userid, $competencyid);
            $r->courseproficient = $ucc->get('proficiency');
            $r->coursegrade = $scale->get_nearest_item($ucc->get('grade'));
        } catch (\Exception $x) {
            $ucc = null;
        }
        return $r;
    }

    /**
     * Retrieve course proficiency and overall proficiency for a competency and user
     *
     * @param \core_competency\competency $competency
     * @param int $userid
     *
     * @return stdClass
     *
     */
    public function retrievefeedback($competency, $userid) {
        $competencyid = $competency->get('id');
        $uc = self::get_user_competency($userid, $competencyid);

        // Get evidences and sort by creation date (newest first).
        $evidence = evidence::get_records_for_usercompetency($uc->get('id'), \context_system::instance(), 'timecreated', "DESC");

        // Get the first valid note and return it.
        foreach ($evidence as $e) {
            if (in_array($e->get('action'), [evidence::ACTION_OVERRIDE, evidence::ACTION_COMPLETE])) {
                return  $e->get('note');
                break;
            }
        }
        return null;
    }

    /**
     * Webservice executor to mark competency as required
     * @param int $competencyid ID of the competency
     * @param int $itemid ID of the study item
     * @param bool $required Mark competency as required or not
     * @return success Always returns successful success object
     */
    public static function require_competency(int $competencyid, int $itemid, bool $required) {
        global $DB;
        $item = studyitem::find_by_id($itemid);
        // Make sure conditions are properly configured.
        $conditions = [];
        try {
            $conditions = json_decode($item->conditions(), true);
        } catch (\Exception $x) {
            $conditions = [];
        }

        // Make sure the competencied field exists.
        if (!isset($conditions["competencies"]) || !is_array($conditions["competencies"])) {
            $conditions["competencies"] = [];
        }

        // Make sure a record exits.
        if (!array_key_exists($competencyid, $conditions["competencies"])) {
            $conditions["competencies"][$competencyid] = [
                "required" => boolval($required),
            ];
        } else {
            $conditions["competencies"][$competencyid]["required"] = boolval($required);
        }

        // Store conditions.
        $item->edit(["conditions" => json_encode($conditions)]);
        return success::success();
    }

    /**
     * Check if this competency is marked required in the studyitem
     * @param object $competency The competency to check
     * @return bool
     */
    public function is_required($competency) {
        if ($this->studyitem) {
            $conditions = [];
            try {
                $conditions = json_decode($this->studyitem->conditions(), true);
            } catch (\Exception $x) {
                $conditions = [];
            }

            // Make sure the competencied field exists.
            if (isset($conditions["competencies"])
                 && is_array($conditions["competencies"])
                 && isset($conditions["competencies"][$competency->get("id")])
                 && isset($conditions["competencies"][$competency->get("id")]["required"])
                 ) {
                return boolval($conditions["competencies"][$competency->get("id")]["required"]);
            }
        }
        return(false);
    }

}
