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

namespace local_kid\local;

use local_kid\event\csv_imported;
use local_kid\event\result_saved;

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

class eval_repository {

    public static function get_for_promotion(int $promotionid): array {
        global $DB;
        return $DB->get_records('local_kid_eval', ['promotionid' => $promotionid], 'timecreated DESC');
    }

    public static function get(int $id): ?\stdClass {
        global $DB;
        return $DB->get_record('local_kid_eval', ['id' => $id], '*', IGNORE_MISSING) ?: null;
    }

    public static function create(int $promotionid, string $titre, string $sourcetype, ?int $cmid, array $competencyids, int $userid): int {
        global $DB;
        $now = time();
        $courseid = null;
        if ($sourcetype === 'moodle_devoir' && $cmid) {
            $cm = get_coursemodule_from_id('assign', $cmid, 0, false, IGNORE_MISSING);
            $courseid = $cm ? $cm->course : null;
        }
        $id = $DB->insert_record('local_kid_eval', (object) [
            'promotionid' => $promotionid,
            'titre' => $titre,
            'sourcetype' => $sourcetype,
            'courseid' => $courseid,
            'cmid' => $sourcetype === 'moodle_devoir' ? $cmid : null,
            'evaldate' => $now,
            'active' => 1,
            'timecreated' => $now,
            'timemodified' => $now,
            'usermodified' => $userid,
        ]);
        self::set_competencies($id, $competencyids);
        self::ensure_entries($id, $promotionid, $userid);
        return $id;
    }

    public static function update(int $id, string $titre, array $competencyids, int $userid): void {
        global $DB;
        $DB->update_record('local_kid_eval', (object) [
            'id' => $id,
            'titre' => $titre,
            'timemodified' => time(),
            'usermodified' => $userid,
        ]);
        self::set_competencies($id, $competencyids);
    }

    public static function toggle_active(int $id, int $userid): void {
        global $DB;
        $eval = self::get($id);
        if (!$eval) {
            return;
        }
        $DB->update_record('local_kid_eval', (object) [
            'id' => $id,
            'active' => $eval->active ? 0 : 1,
            'timemodified' => time(),
            'usermodified' => $userid,
        ]);
    }

    /**
     * Physically removes the evaluation and everything scoped to it: the
     * competency mapping, every student entry and its results, any CAPP
     * consultative attachment pointing at it, and matching publication log
     * rows (orphaned otherwise once their entryid stops existing).
     */
    public static function delete(int $id): void {
        global $DB;
        $transaction = $DB->start_delegated_transaction();

        $entryids = $DB->get_fieldset_select('local_kid_eval_entry', 'id', 'evalid = ?', [$id]);
        if ($entryids) {
            [$insql, $inparams] = $DB->get_in_or_equal($entryids);
            $DB->delete_records_select('local_kid_eval_result', "entryid $insql", $inparams);
            $DB->delete_records_select('local_kid_publication_log', "domain = 'eval' AND entryid $insql", $inparams);
        }
        $DB->delete_records('local_kid_eval_entry', ['evalid' => $id]);
        $DB->delete_records('local_kid_eval_competency', ['evalid' => $id]);
        $DB->delete_records('local_kid_capp_source', ['sourcetype' => 'eval', 'sourceid' => $id]);
        $DB->delete_records('local_kid_eval', ['id' => $id]);

        $transaction->allow_commit();
    }

    /** Full replace on every save — never an incremental diff. */
    private static function set_competencies(int $evalid, array $competencyids): void {
        global $DB;
        $DB->delete_records('local_kid_eval_competency', ['evalid' => $evalid]);
        foreach (array_unique($competencyids) as $competencyid) {
            $DB->insert_record('local_kid_eval_competency', (object) [
                'evalid' => $evalid,
                'competencyid' => (int) $competencyid,
            ]);
        }
    }

    public static function get_competencies(int $evalid): array {
        global $DB;
        $sql = "SELECT c.* FROM {local_kid_competency} c
                  JOIN {local_kid_eval_competency} ec ON ec.competencyid = c.id
                 WHERE ec.evalid = :evalid
                 ORDER BY c.sortorder ASC";
        return $DB->get_records_sql($sql, ['evalid' => $evalid]);
    }

    /** Lazily creates a local_kid_eval_entry row for every active student who doesn't have one yet. */
    public static function ensure_entries(int $evalid, int $promotionid, int $userid): void {
        global $DB;
        $students = promotion_repository::get_active_students($promotionid);
        $existing = $DB->get_records_menu('local_kid_eval_entry', ['evalid' => $evalid], '', 'studentid, id');
        $now = time();
        foreach ($students as $student) {
            if (isset($existing[$student->studentid])) {
                continue;
            }
            $DB->insert_record('local_kid_eval_entry', (object) [
                'evalid' => $evalid,
                'studentid' => $student->studentid,
                'workflowstatus' => workflow_service::STATUS_SAISIE,
                'published' => 0,
                'publishedon' => null,
                'publishedby' => null,
                'feedbacktext' => null,
                'feedbackformat' => null,
                'moodlefeedbacktimemodified' => null,
                'lastsyncat' => null,
                'feedbackchanged' => 0,
                'timecreated' => $now,
                'timemodified' => $now,
            ]);
        }
    }

    public static function get_entries_for_eval(int $evalid): array {
        global $DB;
        $sql = "SELECT e.*, u.firstname, u.lastname
                  FROM {local_kid_eval_entry} e
                  JOIN {local_kid_student} s ON s.id = e.studentid
                  JOIN {user} u ON u.id = s.userid
                 WHERE e.evalid = :evalid
                 ORDER BY u.lastname, u.firstname";
        return $DB->get_records_sql($sql, ['evalid' => $evalid]);
    }

    public static function get_entry(int $entryid): ?\stdClass {
        global $DB;
        return $DB->get_record('local_kid_eval_entry', ['id' => $entryid], '*', IGNORE_MISSING) ?: null;
    }

    public static function get_entry_for_student(int $evalid, int $studentid): ?\stdClass {
        global $DB;
        return $DB->get_record('local_kid_eval_entry', ['evalid' => $evalid, 'studentid' => $studentid], '*', IGNORE_MISSING) ?: null;
    }

    public static function get_results(int $entryid): array {
        global $DB;
        return $DB->get_records('local_kid_eval_result', ['entryid' => $entryid], '', 'competencyid, id, niveauid, commentaire');
    }

    /**
     * Full replace of every (competency → niveau/commentaire) pair on this
     * entry, in one call — the grid always saves its whole visible state.
     *
     * @param array $results competencyid => ['niveauid' => ?int, 'commentaire' => string]
     */
    public static function save_results(int $entryid, array $results, int $userid): void {
        $diff = self::write_results($entryid, $results);
        self::log_saved($entryid, $userid, $diff);
    }

    /**
     * Only touches the competencies present in $results — used by CSV
     * import so a second import (e.g. a follow-up file for the rest of the
     * promotion) never erases a competency entered by an earlier import.
     */
    public static function upsert_results(int $entryid, array $results, int $userid): void {
        $diff = self::write_results($entryid, $results);
        self::log_saved($entryid, $userid, $diff);
    }

    /**
     * @return array competencyid => ['old' => ['niveauid' => ?int, 'commentaire' => string], 'new' => [...]],
     *   only for competencies whose stored niveauid/commentaire actually changed — for the Journal diff
     *   (see journal_service::observe()). Never influences what gets written, only what gets reported.
     */
    private static function write_results(int $entryid, array $results): array {
        global $DB;
        $now = time();
        $diff = [];
        foreach ($results as $competencyid => $data) {
            $existing = $DB->get_record('local_kid_eval_result', ['entryid' => $entryid, 'competencyid' => $competencyid], '*', IGNORE_MISSING);
            $newniveauid = $data['niveauid'] ? (int) $data['niveauid'] : null;
            $newcommentaire = (string) ($data['commentaire'] ?? '');
            // $DB->get_record() returns every column as a string (Moodle DB
            // layer convention, confirmed here on pgsql) — cast niveauid
            // back to int before comparing, otherwise "2" !== 2 makes every
            // resubmission of an unchanged value look like a real change.
            $oldniveauid = $existing && $existing->niveauid ? (int) $existing->niveauid : null;
            $oldcommentaire = $existing ? (string) ($existing->commentaire ?? '') : '';
            if ($oldniveauid !== $newniveauid || $oldcommentaire !== $newcommentaire) {
                $diff[$competencyid] = [
                    'old' => ['niveauid' => $oldniveauid, 'commentaire' => $oldcommentaire],
                    'new' => ['niveauid' => $newniveauid, 'commentaire' => $newcommentaire],
                ];
            }
            $record = (object) [
                'entryid' => $entryid,
                'competencyid' => $competencyid,
                'niveauid' => $newniveauid,
                'commentaire' => $newcommentaire,
                'timemodified' => $now,
            ];
            if ($existing) {
                $record->id = $existing->id;
                $DB->update_record('local_kid_eval_result', $record);
            } else {
                $record->timecreated = $now;
                $DB->insert_record('local_kid_eval_result', $record);
            }
        }
        $DB->update_record('local_kid_eval_entry', (object) ['id' => $entryid, 'timemodified' => $now]);
        return $diff;
    }

    private static function log_saved(int $entryid, int $userid, array $diff): void {
        $other = ['domain' => 'eval', 'entryid' => $entryid];
        if ($diff) {
            $other['diff'] = $diff;
        }
        result_saved::create([
            'context' => \context_system::instance(),
            'userid' => $userid,
            'other' => $other,
        ])->trigger();
    }

    /**
     * Builds the downloadable CSV template — one row per (student ×
     * competency) pair linked to this evaluation, niveau/commentaire left
     * blank — restricted to a single student when given. Same format
     * regardless of sourcetype: the Moodle-synced feedback (feedbacktext)
     * is a separate, single, read-only value and never appears in this
     * grid — only the manually-entered niveau/commentaire per competency
     * round-trips through this template.
     */
    public static function build_csv_template(\stdClass $eval, ?int $onlystudentid = null): string {
        $students = promotion_repository::get_active_students((int) $eval->promotionid);
        if ($onlystudentid) {
            $students = array_filter($students, fn($s) => (int) $s->studentid === $onlystudentid);
        }
        return csv_import_service::build_template($students, self::get_competencies((int) $eval->id));
    }

    /**
     * @param string $content raw CSV file content
     * @return array{added:int, errors:string[]}
     */
    public static function import_csv(\stdClass $eval, string $content, int $userid): array {
        $parsed = csv_import_service::parse($content);
        $competencies = self::get_competencies((int) $eval->id);
        $competenciesbylibelle = [];
        foreach ($competencies as $competency) {
            $competenciesbylibelle[strtolower(trim(format_string($competency->libelle)))] = $competency;
        }
        $niveaux = niveau_repository::get_all();
        $niveauxbylabel = [];
        foreach ($niveaux as $niveau) {
            $niveauxbylabel[strtolower($niveau->libelle)] = $niveau;
            $niveauxbylabel[strtolower($niveau->code)] = $niveau;
        }

        $entriesbystudentid = [];
        foreach (self::get_entries_for_eval((int) $eval->id) as $entry) {
            $entriesbystudentid[(int) $entry->studentid] = $entry;
        }

        $errors = [];
        $touchedentries = []; // entryid => [competencyid => [niveauid, commentaire]]

        foreach ($parsed['rows'] as $row) {
            $line = $row['_line'];
            $studentid = (int) ($row['code_eleve'] ?? 0);
            $competencelabel = strtolower(trim($row['competence'] ?? ''));
            $niveaulabel = strtolower(trim($row['niveau'] ?? ''));
            $commentaire = trim($row['commentaire'] ?? '');

            if (!$studentid || !isset($entriesbystudentid[$studentid])) {
                $errors[] = get_string('eval_import_error_student', 'local_kid', $line);
                continue;
            }
            if (!isset($competenciesbylibelle[$competencelabel])) {
                $errors[] = get_string('eval_import_error_competency', 'local_kid', $line);
                continue;
            }
            if ($niveaulabel !== '' && !isset($niveauxbylabel[$niveaulabel])) {
                $errors[] = get_string('eval_import_error_niveau', 'local_kid', $line);
                continue;
            }

            $entry = $entriesbystudentid[$studentid];
            $competency = $competenciesbylibelle[$competencelabel];
            $niveau = $niveauxbylabel[$niveaulabel] ?? null;

            $touchedentries[$entry->id][$competency->id] = [
                'niveauid' => $niveau ? $niveau->id : null,
                'commentaire' => $commentaire,
            ];
        }

        foreach ($touchedentries as $entryid => $results) {
            self::upsert_results((int) $entryid, $results, $userid);
        }

        csv_imported::create([
            'context' => \context_system::instance(),
            'userid' => $userid,
            'other' => [
                'domain' => 'eval', 'promotionid' => (int) $eval->promotionid, 'itemid' => (int) $eval->id,
                'added' => count($touchedentries), 'errorcount' => count($errors),
            ],
        ])->trigger();

        return [
            'added' => count($touchedentries),
            'errors' => $errors,
        ];
    }

    /**
     * Caches the Moodle assignment feedback text on the entry. Never writes
     * to local_kid_eval_result — the plugin's own niveaux/commentaires stay
     * strictly independent of whatever the Moodle grader wrote, this is a
     * read-only mirror plus a "changed since last ack" flag.
     */
    public static function sync_feedback(int $entryid): void {
        global $DB;

        $entry = self::get_entry($entryid);
        if (!$entry) {
            return;
        }
        $eval = self::get($entry->evalid);
        if (!$eval || $eval->sourcetype !== 'moodle_devoir' || !$eval->cmid) {
            return;
        }
        $student = $DB->get_record('local_kid_student', ['id' => $entry->studentid], '*', MUST_EXIST);

        $feedback = moodle_assign_bridge::get_feedback((int) $eval->cmid, (int) $student->userid);
        if (!$feedback) {
            return;
        }

        $DB->update_record('local_kid_eval_entry', (object) [
            'id' => $entryid,
            'feedbacktext' => $feedback->feedbacktext,
            'feedbackformat' => $feedback->feedbackformat,
            'moodlefeedbacktimemodified' => $feedback->timemodified,
            'lastsyncat' => time(),
            'feedbackchanged' => 0,
        ]);
    }

    /**
     * True if the live Moodle feedback has changed since the last sync —
     * recomputable on the fly for the interactive screen, without needing a
     * fresh sync to know.
     */
    public static function has_live_feedback_changed(\stdClass $entry, \stdClass $eval): bool {
        if ($eval->sourcetype !== 'moodle_devoir' || !$eval->cmid) {
            return false;
        }
        global $DB;
        $student = $DB->get_record('local_kid_student', ['id' => $entry->studentid], '*', IGNORE_MISSING);
        if (!$student) {
            return false;
        }
        $feedback = moodle_assign_bridge::get_feedback((int) $eval->cmid, (int) $student->userid);
        if (!$feedback) {
            return false;
        }
        return $feedback->timemodified > (int) ($entry->moodlefeedbacktimemodified ?? 0);
    }
}
