<?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\cohort_synced;
use local_kid\event\promotion_updated;

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

require_once($GLOBALS['CFG']->dirroot . '/cohort/lib.php');

/**
 * CRUD for promotions and their (optional) link to a Moodle cohort.
 *
 * A promotion can be renamed, (de)activated, or fully deleted via
 * delete() — deletion cascades explicitly through every eval/stage/CAPP
 * entry attached to its students rather than leaving them orphaned (see
 * delete()'s docblock). The caller (pages/promotion_action.php) is
 * responsible for the "not the currently displayed promotion" guard;
 * delete() itself only refuses to remove the last remaining promotion.
 */
class promotion_repository {

    public static function get_all(): array {
        global $DB;
        return $DB->get_records('local_kid_promotion', null, 'name ASC');
    }

    /**
     * Every promotion switcher/selector option list (sélecteur A and B
     * alike) is built from this, not get_all() — a deactivated promotion
     * must disappear from every "choose a promotion" dropdown. The
     * Administration > Promotions management table is the one deliberate
     * exception: it still lists inactive promotions via get_all(), since
     * that's the only screen a promotion can be reactivated from.
     */
    public static function get_active(): array {
        return array_filter(self::get_all(), fn($p) => (bool) $p->active);
    }

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

    public static function create(string $name, int $userid): int {
        global $DB;
        $now = time();
        $id = $DB->insert_record('local_kid_promotion', (object) [
            'name' => $name,
            'moodlecohortid' => null,
            'competencyframeworkid' => null,
            'active' => 1,
            'timecreated' => $now,
            'timemodified' => $now,
            'usermodified' => $userid,
        ]);
        self::log_promotion_event($id, 'created', $userid);
        return $id;
    }

    public static function rename(int $id, string $name, int $userid): void {
        global $DB;
        $DB->update_record('local_kid_promotion', (object) [
            'id' => $id,
            'name' => $name,
            'timemodified' => time(),
            'usermodified' => $userid,
        ]);
        self::log_promotion_event($id, 'renamed', $userid);
    }

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

    /**
     * Physically removes the promotion and every eval/stage/CAPP session
     * scoped to it (and, through those, their entries/results/docs — see
     * eval_repository::delete()/stage_repository::delete()/
     * capp_repository::delete_session()), the promotion's competency
     * referentiel, its students, and its cohort/export log history.
     *
     * Students are hard-deleted here even though local_kid_student is
     * normally soft-remove-only (see get_active_students()) — that
     * soft-remove policy exists so a student's historical results stay
     * attached to a real row across a routine cohort resync; once the
     * whole promotion (and therefore every result) is gone, keeping a
     * dangling student row serves no purpose.
     *
     * Refuses to delete the last remaining promotion; the caller is
     * expected to additionally refuse deleting the promotion currently
     * displayed to the acting user (see promotion_selector).
     */
    public static function delete(int $id, int $userid): void {
        global $DB;

        if ($DB->count_records('local_kid_promotion') <= 1) {
            throw new \moodle_exception('promotion_delete_blocked_last', 'local_kid');
        }

        $transaction = $DB->start_delegated_transaction();

        foreach (eval_repository::get_for_promotion($id) as $eval) {
            eval_repository::delete((int) $eval->id);
        }
        foreach (stage_repository::get_for_promotion($id) as $stage) {
            stage_repository::delete((int) $stage->id);
        }
        foreach (capp_repository::get_sessions_for_promotion($id) as $session) {
            capp_repository::delete_session((int) $session->id);
        }

        $DB->delete_records('local_kid_competency', ['promotionid' => $id]);
        $DB->delete_records('local_kid_student', ['promotionid' => $id]);
        $DB->delete_records('local_kid_promo_cohort_log', ['promotionid' => $id]);
        $DB->delete_records('local_kid_adesi_export_log', ['promotionid' => $id]);
        $DB->delete_records('local_kid_promotion', ['id' => $id]);

        self::log_promotion_event($id, 'deleted', $userid);

        $transaction->allow_commit();
    }

    /**
     * Live list of every Moodle cohort — never hardcoded.
     *
     * @return array id => name
     */
    public static function get_all_moodle_cohorts(): array {
        $result = cohort_get_all_cohorts(0, 1000, '');
        $cohorts = [];
        foreach ($result['cohorts'] as $cohort) {
            $cohorts[(int) $cohort->id] = format_string($cohort->name) .
                (($cohort->idnumber ?? '') !== '' ? " ({$cohort->idnumber})" : '');
        }
        return $cohorts;
    }

    public static function link_cohort(int $promotionid, int $cohortid, int $userid): array {
        global $DB;
        $DB->update_record('local_kid_promotion', (object) [
            'id' => $promotionid,
            'moodlecohortid' => $cohortid,
            'timemodified' => time(),
            'usermodified' => $userid,
        ]);
        return self::diff_and_apply_membership($promotionid, $cohortid, $userid, 'link');
    }

    public static function unlink_cohort(int $promotionid, int $userid): void {
        global $DB;
        $promotion = self::get($promotionid);
        if (!$promotion || !$promotion->moodlecohortid) {
            return;
        }
        $cohortid = (int) $promotion->moodlecohortid;
        $DB->update_record('local_kid_promotion', (object) [
            'id' => $promotionid,
            'moodlecohortid' => null,
            'timemodified' => time(),
            'usermodified' => $userid,
        ]);
        // Unlinking never removes existing local_kid_student rows — those
        // stay intact (soft-remove semantics), only the live cohort link
        // stops driving future resyncs.
        self::log_cohort_event($promotionid, $cohortid, 'unlink', 0, 0, $userid);
    }

    public static function resync(int $promotionid, int $userid): array {
        $promotion = self::get($promotionid);
        if (!$promotion || !$promotion->moodlecohortid) {
            return ['addedcount' => 0, 'removedcount' => 0];
        }
        return self::diff_and_apply_membership($promotionid, (int) $promotion->moodlecohortid, $userid, 'resync');
    }

    /**
     * Diffs {cohort_members} against local_kid_student and applies
     * soft-add/soft-remove. Never a physical DELETE — a removed student's
     * row is kept (status=removed) so their historical eval/stage/CAPP
     * results stay attached to a real student row.
     */
    private static function diff_and_apply_membership(int $promotionid, int $cohortid, int $userid, string $action): array {
        global $DB;

        // No high-level cohort_get_members() exists in Moodle core; {cohort_members}
        // is the documented storage and the pattern core itself uses (e.g. cohort_is_member()).
        // get_fieldset_select (not get_records_menu with the same column used
        // twice as key+value) — a duplicated column name collapses to a
        // single property when a DB row is cast to an object, silently
        // turning the "value" half of the pair into null.
        $memberids = $DB->get_fieldset_select('cohort_members', 'userid', 'cohortid = ?', [$cohortid]);
        $cohortmembers = array_combine($memberids, $memberids);
        $existing = $DB->get_records('local_kid_student', ['promotionid' => $promotionid], '', 'userid, id, status');

        $now = time();
        $added = 0;
        $removed = 0;

        foreach ($cohortmembers as $memberuserid) {
            if (!isset($existing[$memberuserid])) {
                $DB->insert_record('local_kid_student', (object) [
                    'promotionid' => $promotionid,
                    'userid' => $memberuserid,
                    'status' => 'active',
                    'timeadded' => $now,
                    'timeremoved' => null,
                    'usermodified' => $userid,
                ]);
                $added++;
            } else if ($existing[$memberuserid]->status !== 'active') {
                $DB->update_record('local_kid_student', (object) [
                    'id' => $existing[$memberuserid]->id,
                    'status' => 'active',
                    'timeremoved' => null,
                    'usermodified' => $userid,
                ]);
                $added++;
            }
        }

        foreach ($existing as $existinguserid => $row) {
            if ($row->status === 'active' && !isset($cohortmembers[$existinguserid])) {
                $DB->update_record('local_kid_student', (object) [
                    'id' => $row->id,
                    'status' => 'removed',
                    'timeremoved' => $now,
                    'usermodified' => $userid,
                ]);
                $removed++;
            }
        }

        self::log_cohort_event($promotionid, $cohortid, $action, $added, $removed, $userid);

        return ['addedcount' => $added, 'removedcount' => $removed];
    }

    public static function get_active_students(int $promotionid): array {
        global $DB;
        $sql = "SELECT s.id AS studentid, s.userid, u.firstname, u.lastname, u.email
                  FROM {local_kid_student} s
                  JOIN {user} u ON u.id = s.userid
                 WHERE s.promotionid = :promotionid AND s.status = 'active'
                 ORDER BY u.lastname, u.firstname";
        return $DB->get_records_sql($sql, ['promotionid' => $promotionid]);
    }

    public static function get_active_student_count(int $promotionid): int {
        global $DB;
        return $DB->count_records('local_kid_student', ['promotionid' => $promotionid, 'status' => 'active']);
    }

    /**
     * Display name for one local_kid_student.id — e.g. the comment-history
     * drawer's subtitle ({@see comment_history_service}), the one place
     * that needs just this single name rather than the whole promotion
     * roster get_active_students() already returns.
     */
    public static function get_student_fullname(int $studentid): string {
        global $DB;
        $user = $DB->get_record_sql(
            'SELECT u.firstname, u.lastname
               FROM {local_kid_student} s
               JOIN {user} u ON u.id = s.userid
              WHERE s.id = ?', [$studentid]);
        return $user ? fullname($user) : '';
    }

    /**
     * Resolves the local_kid_student.id for a Moodle userid within one
     * promotion — the bridge between "Vue élève" (which only ever knows a
     * Moodle userid: $USER->id) and every other domain repository (which
     * keys all *_entry.studentid columns on local_kid_student.id, never a
     * Moodle userid directly).
     */
    public static function get_active_student_record(int $promotionid, int $userid): ?\stdClass {
        global $DB;
        return $DB->get_record('local_kid_student', [
            'promotionid' => $promotionid, 'userid' => $userid, 'status' => 'active',
        ], '*', IGNORE_MISSING) ?: null;
    }

    /**
     * Resolves the (single) active promotion a student userid belongs to.
     * Assumes a student sits in one active promotion at a time — on an
     * unlikely overlap, the most recently added membership wins rather than
     * merging records from two promotions.
     */
    public static function get_active_promotion_for_user(int $userid): ?\stdClass {
        global $DB;
        $sql = "SELECT p.*
                  FROM {local_kid_promotion} p
                  JOIN {local_kid_student} s ON s.promotionid = p.id
                 WHERE s.userid = :userid AND s.status = 'active' AND p.active = 1
                 ORDER BY s.timeadded DESC";
        $records = $DB->get_records_sql($sql, ['userid' => $userid], 0, 1);
        return $records ? reset($records) : null;
    }

    private static function log_promotion_event(int $promotionid, string $action, int $userid): void {
        promotion_updated::create([
            'context' => \context_system::instance(),
            'objectid' => $promotionid,
            'userid' => $userid,
            'other' => ['action' => $action],
        ])->trigger();
    }

    private static function log_cohort_event(int $promotionid, int $cohortid, string $action, int $added, int $removed, int $userid): void {
        global $DB;
        $DB->insert_record('local_kid_promo_cohort_log', (object) [
            'promotionid' => $promotionid,
            'cohortid' => $cohortid,
            'action' => $action,
            'addedcount' => $added,
            'removedcount' => $removed,
            'userid' => $userid,
            'timecreated' => time(),
        ]);
        cohort_synced::create([
            'context' => \context_system::instance(),
            'objectid' => $promotionid,
            'userid' => $userid,
            'other' => ['action' => $action, 'cohortid' => $cohortid, 'addedcount' => $added, 'removedcount' => $removed],
        ])->trigger();
    }
}
