<?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\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
     */
    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);
    }

    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 $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'),
            "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),
            "count" => new \external_value(PARAM_INT, 'number of students in stats',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
     */
    private function competencyinfo_model($competency) : 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,
        ];

        return  $model;
    }

    /**
     * Webservice model for editor info
     * @param int[] $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;

        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);
                        $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 = [];
        // First retrieve all the competencies associates with this course.
        $coursecompetencies = c_api::list_course_competencies($this->course->id);
        // Next create the data model, and check user proficiency for each competency.
        foreach($coursecompetencies as $ccinfo) {
            $list[] = $ccinfo['competency'];
        }
        return $list;
    }

    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;
    }

    /**
     * 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 = c_api::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 = c_api::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) {}
        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 = c_api::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) {}

        // 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 gradable item is marked required in the studyitem
     * @return bool
     */
    public function is_required($competency) {
        if ($this->studyitem) {
            $conditions = [];
            try {
                $conditions = json_decode($this->studyitem->conditions(),true);
            } catch (\Exception $x) {}
    
            // 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);
    }

}