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

/**
 * Single shared optimistic-conflict check for the three grid saves
 * (eval/stage/capp `save_results()`), scoped to the comment fields only
 * (per-competency `commentaire` + the domain's single global comment, when
 * it has one) — niveau values are deliberately left last-write-wins, out of
 * scope here (see the "Prévention des conflits sur les commentaires" spec).
 *
 * There is no pre-existing optimistic-locking mechanism anywhere in
 * local_kid to "reuse" (verified: no repository compares `timemodified`
 * before writing) — this class is the one place that logic now lives, built
 * once and called identically by the three grid page controllers, rather
 * than duplicated per domain.
 *
 * Detection is 3-way per field (original value the form was loaded with vs
 * the value just submitted vs the value now fresh in the DB), not a blunt
 * "any change since load blocks everything": a change to a field the user
 * never touched, or to an unrelated field (e.g. a niveau), must never
 * surface a false conflict on a comment the user is trying to save.
 */
final class entry_conflict_service {

    /**
     * @param \stdClass $freshentry current DB entry row, re-fetched by the
     *   caller (repository::get_entry()) AFTER $mform->get_data() — must
     *   carry `timemodified` and, when applicable, the domain's global
     *   comment column.
     * @param array $freshresults competencyid => \stdClass{niveauid,
     *   commentaire}, re-fetched by the caller (repository::get_results())
     *   at the same time as $freshentry — current DB state.
     * @param int $expectedtimemodified the entry's `timemodified` at the
     *   moment the form was rendered (hidden field `expectedtimemodified`).
     *   Fast path: unchanged means nothing on the entry could have moved,
     *   skip the per-field diff entirely.
     * @param array $submittedresults competencyid => ['commentaire' =>
     *   string, 'origcommentaire' => string] — the posted grid values, each
     *   paired with the value that field was rendered with.
     * @param string|null $globalfield entry column name for the domain's
     *   single global comment ('commentaireglobal'/'appreciationglobale'),
     *   or null when the domain has none (Évaluations).
     * @param string|null $origglobal the global comment's value when the
     *   form was rendered.
     * @param string|null $submittedglobal the global comment's posted value.
     * @return array|null null when there is nothing to arbitrate (safe to
     *   write $submittedresults/$submittedglobal as-is); otherwise:
     *   ['competencies' => [competencyid => ['mine' => string, 'db' =>
     *   string]], 'global' => ['mine' => string, 'db' => string]|null,
     *   'freshtimemodified' => int] — never both empty when non-null.
     */
    public static function detect(
        \stdClass $freshentry,
        array $freshresults,
        int $expectedtimemodified,
        array $submittedresults,
        ?string $globalfield,
        ?string $origglobal,
        ?string $submittedglobal
    ): ?array {
        if ((int) $freshentry->timemodified === $expectedtimemodified) {
            // Nothing touched this entry since the form was rendered.
            return null;
        }

        $competencyconflicts = [];
        foreach ($submittedresults as $competencyid => $data) {
            $orig = (string) ($data['origcommentaire'] ?? '');
            $mine = (string) ($data['commentaire'] ?? '');
            if ($mine === $orig) {
                // The user never touched this field.
                continue;
            }
            $fresh = isset($freshresults[$competencyid])
                ? (string) ($freshresults[$competencyid]->commentaire ?? '')
                : '';
            if ($fresh === $orig || $fresh === $mine) {
                // Nobody else changed it, or they landed on the same text.
                continue;
            }
            $competencyconflicts[$competencyid] = ['mine' => $mine, 'db' => $fresh];
        }

        $globalconflict = null;
        if ($globalfield !== null) {
            $orig = (string) ($origglobal ?? '');
            $mine = (string) ($submittedglobal ?? '');
            if ($mine !== $orig) {
                $fresh = (string) ($freshentry->{$globalfield} ?? '');
                if ($fresh !== $orig && $fresh !== $mine) {
                    $globalconflict = ['mine' => $mine, 'db' => $fresh];
                }
            }
        }

        if (!$competencyconflicts && !$globalconflict) {
            return null;
        }

        return [
            'competencies' => $competencyconflicts,
            'global' => $globalconflict,
            'globalfield' => $globalfield,
            'freshtimemodified' => (int) $freshentry->timemodified,
        ];
    }

    /**
     * Adds display data (competency label, author + timestamp of the
     * conflicting DB value) to a {@see detect()} result — kept separate so
     * detect() itself has no rendering/string/DB-lookup concerns, only the
     * three page controllers (evaluations.php/stages.php/capp.php) call
     * this, once each, right before handing the conflict to their grid page
     * output class.
     *
     * @param array $conflict a non-null {@see detect()} return value
     * @param string $domain 'eval'|'stage'|'capp', for the journal lookup
     * @param int $entryid
     * @param array $competencylabels competencyid => display label
     * @param string|null $globallabel display label for the domain's global
     *   comment field ('stage_commentaire_global'/'capp_appreciation_globale'),
     *   only needed when $conflict['global'] is set
     * @return array same shape as $conflict, each competency/global entry
     *   gaining 'label' and 'author'/'authortime' (fullname + userdate
     *   string, or null if no journal trace exists — e.g. the conflicting
     *   value predates the Journal feature)
     */
    public static function enrich(array $conflict, string $domain, int $entryid, array $competencylabels, ?string $globallabel = null): array {
        foreach ($conflict['competencies'] as $competencyid => $data) {
            $found = journal_service::find_last_author($domain, $entryid, (int) $competencyid);
            $conflict['competencies'][$competencyid] = $data + [
                'label' => $competencylabels[$competencyid] ?? (string) $competencyid,
                'author' => $found ? fullname(\core_user::get_user($found['userid'])) : null,
                'authortime' => $found
                    ? userdate($found['timecreated'], get_string('strftimedatetimeshort', 'core_langconfig'))
                    : null,
            ];
        }
        if ($conflict['global']) {
            // No per-field diff exists for the global comment (see
            // journal_service::find_last_author's docblock) — this is the
            // dossier's last save, not necessarily the exact edit that
            // produced this field's current value.
            $found = journal_service::find_last_author($domain, $entryid, null);
            $conflict['global'] += [
                'label' => $globallabel ?? '',
                'author' => $found ? fullname(\core_user::get_user($found['userid'])) : null,
                'authortime' => $found
                    ? userdate($found['timecreated'], get_string('strftimedatetimeshort', 'core_langconfig'))
                    : null,
            ];
        }
        return $conflict;
    }
}
