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

/**
 * Read-only comment version history, built entirely from the pre-existing
 * `local_kid_journal` (append-only, `oldvalue`/`newvalue` JSON snapshots
 * already written on every save by eval_repository/stage_repository/
 * capp_repository::log_saved() — see journal_service::split_diff()) — no
 * new storage, shared by the three domains, one place.
 */
final class comment_history_service {

    /** @var array<string,string> domain => *_result table name. */
    private const RESULT_TABLES = [
        'eval' => 'local_kid_eval_result',
        'stage' => 'local_kid_stage_result',
        'capp' => 'local_kid_capp_result',
    ];

    /**
     * Every competency's commentaire history for one entry, most recent
     * version first. One `local_kid_journal` query per entry (not per
     * competency) — a single save already bundles every changed
     * competency's diff into one journal row, so this groups them back out
     * client-side rather than re-querying per competency.
     *
     * @return array<int, array> competencyid => list of
     *   {commentaire, userid, fullname, initials, timecreated}, ordered
     *   newest first. A competency with no real edit ever recorded (empty
     *   commentaire, nothing in the Journal) is simply absent from the
     *   returned array — callers use that to hide the entry point entirely.
     */
    public static function get_for_entry(string $domain, int $entryid): array {
        global $DB;

        $rows = $DB->get_records('local_kid_journal',
            ['domain' => $domain, 'entryid' => $entryid, 'action' => 'result_saved'],
            'timecreated DESC, id DESC', 'id, userid, oldvalue, newvalue, timecreated');

        $byuser = [];
        $history = [];
        // Earliest known "old" value per competency, e.g. a value written by
        // a path that never went through write_results()/log_saved() (like
        // capp_repository::seed_commentaires_from_sources()) before its
        // first ever tracked save — appended to $history below, after this
        // loop. $rows is ordered newest-first, so overwriting on every row
        // that mentions a competency leaves, once the loop ends, the value
        // from the OLDEST such row.
        $earliestold = [];
        foreach ($rows as $row) {
            $decoded = $row->newvalue ? json_decode($row->newvalue, true) : null;
            if (is_array($decoded)) {
                foreach ($decoded as $competencyid => $value) {
                    $competencyid = (int) $competencyid;
                    $commentaire = (string) ($value['commentaire'] ?? '');
                    if ($commentaire === '') {
                        // A save that cleared the comment back to empty isn't a
                        // "version" worth surfacing — nothing to read there.
                        continue;
                    }
                    $history[$competencyid][] = [
                        'commentaire' => $commentaire,
                        'userid' => (int) $row->userid,
                        'timecreated' => (int) $row->timecreated,
                    ];
                }
            }
            $decodedold = $row->oldvalue ? json_decode($row->oldvalue, true) : null;
            if (is_array($decodedold)) {
                foreach ($decodedold as $competencyid => $value) {
                    $competencyid = (int) $competencyid;
                    $earliestold[$competencyid] = [
                        'commentaire' => (string) ($value['commentaire'] ?? ''),
                        'timecreated' => (int) $row->timecreated,
                    ];
                }
            }
        }

        // A competency's very first tracked save can itself have a non-empty
        // "old" value — a state the comment already had before ever going
        // through the Journal (e.g. seed_commentaires_from_sources()'s
        // pre-fill). That state is never a `newvalue` of any row, so without
        // this it silently drops out of the drawer the moment a real edit
        // supersedes it, even though `oldvalue` still holds it in the DB.
        // Appended as the oldest version (end of the newest-first list).
        foreach ($earliestold as $competencyid => $initial) {
            if ($initial['commentaire'] === '') {
                continue;
            }
            $history[$competencyid][] = [
                'commentaire' => $initial['commentaire'],
                'userid' => null,
                'timecreated' => $initial['timecreated'],
            ];
        }

        // Defence in depth: if the live stored value doesn't match the most
        // recent Journal-derived version (Journal predates 2026-08-13, or a
        // write path outside the normal save flow touched the row), prepend
        // a synthetic "current" entry from the live data itself rather than
        // silently showing a stale/incomplete history. Author unknown in
        // that case (no usermodified column exists on the *_result tables).
        // A dedicated query here (rather than reusing the domain
        // repository's own get_results(), whose SELECT list omits
        // timemodified) keeps this the one place that needs it.
        $liveresults = $DB->get_records(self::RESULT_TABLES[$domain], ['entryid' => $entryid],
            '', 'competencyid, commentaire, timemodified, timecreated');
        foreach ($liveresults as $competencyid => $result) {
            $competencyid = (int) $competencyid;
            $livecommentaire = (string) ($result->commentaire ?? '');
            if ($livecommentaire === '') {
                continue;
            }
            $top = $history[$competencyid][0]['commentaire'] ?? null;
            if ($top !== $livecommentaire) {
                // $history[$competencyid] may not exist yet at all (a
                // competency whose commentaire was written by a path that
                // never went through write_results()/log_saved() — e.g.
                // capp_repository::seed_commentaires_from_sources() — has
                // no Journal trace whatsoever) — array_unshift() on an
                // unset offset auto-vivifies it as null, not [], and
                // throws a TypeError; initialise it first.
                $history[$competencyid] ??= [];
                array_unshift($history[$competencyid], [
                    'commentaire' => $livecommentaire,
                    'userid' => null,
                    'timecreated' => (int) ($result->timemodified ?: $result->timecreated),
                ]);
            }
        }

        foreach ($history as $competencyid => $versions) {
            $history[$competencyid] = array_map(function ($version) use (&$byuser) {
                if ($version['userid'] !== null && !isset($byuser[$version['userid']])) {
                    $byuser[$version['userid']] = \core_user::get_user($version['userid']);
                }
                $user = $version['userid'] !== null ? ($byuser[$version['userid']] ?? null) : null;
                $version['fullname'] = $user ? fullname($user) : get_string('history_author_unknown', 'local_kid');
                $version['initials'] = $user ? self::initials($user) : '?';
                $version['timelabel'] = userdate($version['timecreated'], get_string('history_date_format', 'local_kid'));
                return $version;
            }, $versions);
        }

        return $history;
    }

    /**
     * The "Voir l'historique (N versions)" entry point for one competency's
     * commentaire — a single `<button>` carrying the whole version list as
     * a `data-history` JSON attribute, read client-side by
     * amd/src/comment_history.js with zero AJAX round-trip on open (same
     * pattern as the Comparateur's detail drawer, amd/src/comparateur.js:
     * "data already in the DOM via data-* attributes ... no server
     * round-trip"). Never called for an empty $versions — see
     * entry_grid_form.php/capp_grid_form.php's own emptiness check, which
     * skips rendering this element entirely rather than showing a
     * "0 versions" link.
     *
     * @param array $versions non-empty, as returned by get_for_entry()
     * @param string $title drawer subtitle, e.g. "Stage · Emeline BAUDET · C01 …"
     */
    public static function render_link(array $versions, string $title): string {
        $count = count($versions);
        $label = $count === 1
            ? get_string('history_link_single', 'local_kid')
            : get_string('history_link_multiple', 'local_kid', $count);
        $payload = json_encode(['title' => $title, 'versions' => $versions]);
        return \html_writer::tag('button', s($label), [
            'type' => 'button',
            'class' => 'kid-history-link',
            'data-role' => 'kid-history-link',
            'data-history' => $payload,
        ]);
    }

    private static function initials(\stdClass $user): string {
        $first = \core_text::substr((string) $user->firstname, 0, 1);
        $last = \core_text::substr((string) $user->lastname, 0, 1);
        return \core_text::strtoupper($first . $last) ?: '?';
    }
}
