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

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

/**
 * Cross-domain (Évaluations/Stages/CAPP) read-only data layer for the
 * Comparateur de bilans. {@see flatten()} is the single source of truth
 * consumed identically by the on-screen matrix, the CSV export and the
 * XLSX export — the three must never diverge in what they include.
 *
 * Never writes. Every value is the retained (stored) niveau/commentaire —
 * the schema itself only ever stores one value per (entry, competency),
 * there is no raw/harmonised distinction to choose between.
 */
class comparateur_repository {

    /**
     * Active sources across the 3 domains for one promotion, normalised.
     *
     * @return \stdClass[] {id, type ('eval'|'stage'|'capp'), label}
     */
    public static function get_sources(int $promotionid): array {
        $sources = [];
        foreach (eval_repository::get_for_promotion($promotionid) as $eval) {
            if (!$eval->active) {
                continue;
            }
            $sources[] = (object) ['id' => (int) $eval->id, 'type' => 'eval', 'label' => format_string($eval->titre)];
        }
        foreach (stage_repository::get_for_promotion($promotionid) as $stage) {
            if (!$stage->active) {
                continue;
            }
            $sources[] = (object) ['id' => (int) $stage->id, 'type' => 'stage', 'label' => format_string($stage->libelle)];
        }
        foreach (capp_repository::get_active_sessions_for_promotion($promotionid) as $session) {
            $sources[] = (object) ['id' => (int) $session->id, 'type' => 'capp', 'label' => format_string($session->libelle)];
        }
        return $sources;
    }

    public static function source_key(string $type, int $id): string {
        return $type . '_' . $id;
    }

    /**
     * Which competencies each given source actually evaluates — "hors
     * périmètre" is the absence of a competencyid from this list, never
     * inferred from a missing result row (see get_coverage() call sites in
     * flatten()). CAPP has no scoping table: it always covers the full
     * active competency framework of the promotion.
     *
     * @param \stdClass[] $sources as returned by get_sources()
     * @return array<string, int[]> source_key() => competencyid[]
     */
    public static function get_coverage(array $sources, int $promotionid): array {
        $coverage = [];
        $fullframework = null;
        foreach ($sources as $source) {
            if ($source->type === 'eval') {
                $competencies = eval_repository::get_competencies($source->id);
            } else if ($source->type === 'stage') {
                $stage = stage_repository::get($source->id);
                $competencies = $stage ? stage_repository::get_scope_competencies($stage) : [];
            } else {
                if ($fullframework === null) {
                    $fullframework = competency_repository::get_active_for_promotion($promotionid);
                }
                $competencies = $fullframework;
            }
            $coverage[self::source_key($source->type, $source->id)] = array_map(
                fn($c) => (int) $c->id, array_values($competencies)
            );
        }
        return $coverage;
    }

    /**
     * The single shared flatten function — screen, CSV, XLSX all call this
     * and only this. Batched queries (one per domain for entries, one per
     * domain for results), never a per-student×source lookup loop.
     *
     * @param array $filters {
     *   promotionid: int (required),
     *   studentids: int[] (local_kid_student.id, required),
     *   sourcekeys: string[] (source_key() values, required),
     *   competencyids: int[] (empty = all active competencies of the promotion),
     *   niveaucodes: string[] (empty = all, e.g. ['COMPETENCE3','COMPETENCE4']),
     *   onlyobserved: bool (hide a student×competency row entirely if every
     *     selected source shows "Non observé" for it),
     * }
     * @return \stdClass[] one row per (student, competency, source) actually
     *   in scope — never a row for a competency outside a source's coverage.
     */
    public static function flatten(array $filters): array {
        global $DB;

        $promotionid = (int) ($filters['promotionid'] ?? 0);
        $studentids = array_values(array_unique(array_map('intval', $filters['studentids'] ?? [])));
        $sourcekeys = array_values(array_unique($filters['sourcekeys'] ?? []));
        $competencyids = array_map('intval', $filters['competencyids'] ?? []);
        $niveaucodes = $filters['niveaucodes'] ?? [];
        $onlyobserved = !empty($filters['onlyobserved']);

        if (!$promotionid || !$studentids || !$sourcekeys) {
            return [];
        }

        $promotion = promotion_repository::get($promotionid);
        if (!$promotion) {
            return [];
        }

        $sourcesbykey = [];
        foreach (self::get_sources($promotionid) as $source) {
            $sourcesbykey[self::source_key($source->type, $source->id)] = $source;
        }
        $selectedsources = array_values(array_intersect_key($sourcesbykey, array_flip($sourcekeys)));
        if (!$selectedsources) {
            return [];
        }
        $coverage = self::get_coverage($selectedsources, $promotionid);
        foreach ($coverage as $key => $ids) {
            $coverage[$key] = array_flip($ids); // O(1) "is covered" lookup.
        }

        $competencies = competency_repository::get_active_for_promotion($promotionid);
        if ($competencyids) {
            $competencies = array_filter($competencies, fn($c) => in_array((int) $c->id, $competencyids, true));
        }
        if (!$competencies) {
            return [];
        }

        $niveaux = niveau_repository::get_all();
        $niveauxbyid = [];
        $niveauzero = null;
        foreach ($niveaux as $niveau) {
            $niveauxbyid[(int) $niveau->id] = $niveau;
            if ($niveau->code === 'COMPETENCE0') {
                $niveauzero = $niveau;
            }
        }

        [$entries, $results] = self::fetch_entries_and_results($selectedsources, $studentids);

        [$studentinsql, $studentparams] = $DB->get_in_or_equal($studentids, SQL_PARAMS_NAMED, 'stu');
        $studentparams['promotionid'] = $promotionid;
        $studentrows = $DB->get_records_sql(
            "SELECT s.id, u.firstname, u.lastname
               FROM {local_kid_student} s
               JOIN {user} u ON u.id = s.userid
              WHERE s.id $studentinsql AND s.promotionid = :promotionid AND s.status = 'active'",
            $studentparams
        );

        // Pass 1: build every in-scope cell, ungated by the niveaux/onlyobserved
        // filters, tracking whether each (student, competency) has at least one
        // observed (non-COMPETENCE0) value across the selected sources.
        $cells = [];
        $anyobserved = [];
        foreach ($studentids as $studentid) {
            if (!isset($studentrows[$studentid])) {
                continue; // Not an active student of this promotion — never leaks in.
            }
            foreach ($competencies as $competency) {
                $competencyid = (int) $competency->id;
                foreach ($selectedsources as $source) {
                    $key = self::source_key($source->type, $source->id);
                    if (!isset($coverage[$key][$competencyid])) {
                        continue; // Hors périmètre de cette source pour cette compétence.
                    }
                    $entry = $entries[$key][$studentid] ?? null;
                    $result = $entry ? ($results[(int) $entry->id][$competencyid] ?? null) : null;
                    $niveauid = $result && $result->niveauid ? (int) $result->niveauid : ($niveauzero ? (int) $niveauzero->id : null);
                    $niveau = $niveauxbyid[$niveauid] ?? null;
                    if ($niveau && $niveau->code !== 'COMPETENCE0') {
                        $anyobserved[$studentid][$competencyid] = true;
                    }
                    $cells[] = (object) [
                        'promotionid' => $promotionid,
                        'promotionname' => format_string($promotion->name),
                        'studentid' => $studentid,
                        'studentname' => fullname($studentrows[$studentid]),
                        'competencyid' => $competencyid,
                        'competencycode' => $competency->codecourt,
                        'competencylabel' => competency_repository::label($competency),
                        'sourceid' => $source->id,
                        'sourcetype' => $source->type,
                        'sourcelabel' => $source->label,
                        'niveauid' => $niveauid,
                        'niveaucode' => $niveau->code ?? null,
                        'niveaulabel' => $niveau ? format_string($niveau->libelle) : '',
                        'niveauclass' => $niveau ? niveau_repository::badge_class($niveau->code) : '',
                        'commentaire' => $result ? (string) ($result->commentaire ?? '') : '',
                    ];
                }
            }
        }

        // Pass 2: apply onlyobserved (row-level, per student×competency) and
        // niveaux (per cell) — same two filters, identically, for screen/CSV/XLSX.
        $niveaucodeset = $niveaucodes ? array_flip($niveaucodes) : null;
        $out = [];
        foreach ($cells as $cell) {
            if ($onlyobserved && empty($anyobserved[$cell->studentid][$cell->competencyid])) {
                continue;
            }
            if ($niveaucodeset !== null && !isset($niveaucodeset[$cell->niveaucode])) {
                continue;
            }
            $out[] = $cell;
        }
        return $out;
    }

    /**
     * @param \stdClass[] $selectedsources
     * @param int[] $studentids
     * @return array{0: array<string, array<int, \stdClass>>, 1: array<int, array<int, \stdClass>>}
     *   [0] source_key() => studentid => entry row
     *   [1] entryid => competencyid => result row
     */
    private static function fetch_entries_and_results(array $selectedsources, array $studentids): array {
        global $DB;

        $bytype = ['eval' => [], 'stage' => [], 'capp' => []];
        foreach ($selectedsources as $source) {
            $bytype[$source->type][] = $source->id;
        }
        $domains = [
            'eval' => ['local_kid_eval_entry', 'evalid', 'local_kid_eval_result'],
            'stage' => ['local_kid_stage_entry', 'stageid', 'local_kid_stage_result'],
            'capp' => ['local_kid_capp_entry', 'sessionid', 'local_kid_capp_result'],
        ];

        $entries = [];
        $results = [];
        foreach ($domains as $type => [$entrytable, $idfield, $resulttable]) {
            $ids = $bytype[$type];
            if (!$ids) {
                continue;
            }
            [$idinsql, $idparams] = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'src');
            [$stuinsql, $stuparams] = $DB->get_in_or_equal($studentids, SQL_PARAMS_NAMED, 'stu');
            $rows = $DB->get_records_select(
                $entrytable, "$idfield $idinsql AND studentid $stuinsql", $idparams + $stuparams
            );
            $entryids = [];
            foreach ($rows as $row) {
                $entries[self::source_key($type, (int) $row->{$idfield})][(int) $row->studentid] = $row;
                $entryids[] = (int) $row->id;
            }
            if ($entryids) {
                [$entinsql, $entparams] = $DB->get_in_or_equal($entryids, SQL_PARAMS_NAMED, 'ent');
                $resultrows = $DB->get_records_select($resulttable, "entryid $entinsql", $entparams);
                foreach ($resultrows as $resultrow) {
                    $results[(int) $resultrow->entryid][(int) $resultrow->competencyid] = $resultrow;
                }
            }
        }
        return [$entries, $results];
    }
}
