<?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\entry_published;
use local_kid\event\entry_reopened;
use local_kid\event\entry_unpublished;
use local_kid\event\workflow_changed;

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

/**
 * The single write path for saisie→valide→finalise and publish/reopen,
 * shared by the eval/stage/capp domains (each just passes its own
 * `*_entry` table name). Every transition here writes exactly one
 * publication_log row and fires exactly one event.
 */
class workflow_service {

    const STATUS_SAISIE = 'saisie';
    const STATUS_VALIDE = 'valide';
    const STATUS_FINALISE = 'finalise';

    const VALID_TRANSITIONS = [
        self::STATUS_SAISIE => [self::STATUS_VALIDE],
        self::STATUS_VALIDE => [self::STATUS_SAISIE, self::STATUS_FINALISE],
        self::STATUS_FINALISE => [self::STATUS_VALIDE],
    ];

    /**
     * Single source of truth for the badge CSS class(es) a workflowstatus
     * maps to — reused by every screen that shows a status badge (dashboard
     * cross-tabs, récapitulatifs, Vue élève) so the colour coding stays
     * consistent. Falls back to the "unknown" modifier for any value outside
     * the three known statuses (the column has no DB-level enum, so this is
     * a real defensive case, not dead code).
     *
     * DSFR badge classes (fr-badge / fr-badge--info / fr-badge--success),
     * not a legacy local-kid-status--* modifier — the plain (no modifier)
     * "saisie"/"unknown" case relies on `.fr-badge`'s own default grey.
     * This is the DSFR framework's own default badge palette (see
     * thirdparty/dsfr), deliberately not customised — unlike the niveau
     * badges (see niveau_repository::badge_class()), which keep their
     * existing colours per explicit instruction.
     */
    public static function status_badge_class(string $status): string {
        return [
            self::STATUS_SAISIE => 'fr-badge',
            self::STATUS_VALIDE => 'fr-badge fr-badge--info',
            self::STATUS_FINALISE => 'fr-badge fr-badge--success',
        ][$status] ?? 'fr-badge';
    }

    /**
     * Single source of truth for the human label a workflowstatus maps to.
     * Reuses the existing workflow_saisie/valide/finalise language strings —
     * never invents a new label. Falls back to echoing the raw status value
     * for anything unexpected, so this never throws (get_string() would, on
     * an unmapped key).
     */
    public static function status_label(string $status): string {
        $known = [self::STATUS_SAISIE, self::STATUS_VALIDE, self::STATUS_FINALISE];
        return in_array($status, $known, true)
            ? get_string('workflow_' . $status, 'local_kid')
            : $status;
    }

    /**
     * Single rendering point for the workflow status badge: every screen
     * that shows a Saisie/Validé/Finalisé status must call this instead of
     * building its own <span> markup, so the label lookup, the CSS mapping
     * and the HTML structure each stay defined in exactly one place.
     */
    public static function render_status_badge(string $status): string {
        return \html_writer::span(
            s(self::status_label($status)),
            self::status_badge_class($status)
        );
    }

    /**
     * @param string $domain 'eval'|'stage'|'capp'
     * @param string $table full table name, e.g. 'local_kid_eval_entry'
     */
    public static function set_status(string $domain, string $table, int $entryid, string $tostatus, int $userid, ?string $reason = null): bool {
        global $DB;

        $entry = $DB->get_record($table, ['id' => $entryid], '*', MUST_EXIST);
        $fromstatus = $entry->workflowstatus;

        if ($fromstatus === $tostatus) {
            return true;
        }
        if (!in_array($tostatus, self::VALID_TRANSITIONS[$fromstatus] ?? [], true)) {
            return false;
        }

        $DB->update_record($table, (object) [
            'id' => $entryid,
            'workflowstatus' => $tostatus,
            'timemodified' => time(),
        ]);

        self::log_and_trigger($domain, $entryid, 'status_change', $fromstatus, $tostatus, null, null, $reason, $userid);
        return true;
    }

    /**
     * Publishing requires `finalise` — refuses (returns false) otherwise,
     * never silently force-finalises.
     */
    public static function publish(string $domain, string $table, int $entryid, int $userid): bool {
        global $DB;

        $entry = $DB->get_record($table, ['id' => $entryid], '*', MUST_EXIST);
        if ($entry->workflowstatus !== self::STATUS_FINALISE) {
            return false;
        }
        if ($entry->published) {
            return true;
        }

        $now = time();
        $DB->update_record($table, (object) [
            'id' => $entryid,
            'published' => 1,
            'publishedon' => $now,
            'publishedby' => $userid,
            'timemodified' => $now,
        ]);

        self::log_and_trigger($domain, $entryid, 'publish', null, null, 0, 1, null, $userid);
        entry_published::create([
            'context' => \context_system::instance(),
            'userid' => $userid,
            'other' => ['domain' => $domain, 'entryid' => $entryid],
        ])->trigger();
        return true;
    }

    /**
     * Publishes every finalised, not-yet-published entry among the given
     * ids — silently skips entries not in `finalise`, never a hard block on
     * a bulk "publish whole promotion" action.
     *
     * @return int number actually published
     */
    public static function publish_bulk(string $domain, string $table, array $entryids, int $userid): int {
        $count = 0;
        foreach ($entryids as $entryid) {
            if (self::publish($domain, $table, (int) $entryid, $userid)) {
                $count++;
            }
        }
        return $count;
    }

    /**
     * The "Masquer" side of the grouped actions bar: clears `published`
     * without touching `workflowstatus` (unlike {@see reopen()}, which also
     * drops the entry back to `valide` — a different, broader action not
     * exposed by the grouped bar). Always succeeds/idempotent: an
     * already-hidden entry is a silent no-op, never reported as "ignored"
     * (unlike publish(), there is no eligibility rule for hiding).
     */
    public static function unpublish(string $domain, string $table, int $entryid, int $userid): bool {
        global $DB;

        $entry = $DB->get_record($table, ['id' => $entryid], '*', MUST_EXIST);
        if (!$entry->published) {
            return true;
        }

        $now = time();
        $DB->update_record($table, (object) [
            'id' => $entryid,
            'published' => 0,
            'timemodified' => $now,
        ]);

        self::log_and_trigger($domain, $entryid, 'unpublish', null, null, 1, 0, null, $userid);
        entry_unpublished::create([
            'context' => \context_system::instance(),
            'userid' => $userid,
            'other' => ['domain' => $domain, 'entryid' => $entryid],
        ])->trigger();
        return true;
    }

    /**
     * Reopening a published/finalised entry always lands back on `valide`
     * (never `saisie` — product decision) and clears publication.
     */
    public static function reopen(string $domain, string $table, int $entryid, int $userid, ?string $reason = null): void {
        global $DB;

        $entry = $DB->get_record($table, ['id' => $entryid], '*', MUST_EXIST);
        $fromstatus = $entry->workflowstatus;
        $frompublished = (int) $entry->published;

        $now = time();
        $DB->update_record($table, (object) [
            'id' => $entryid,
            'workflowstatus' => self::STATUS_VALIDE,
            'published' => 0,
            'timemodified' => $now,
        ]);

        self::log_and_trigger($domain, $entryid, 'reopen', $fromstatus, self::STATUS_VALIDE, $frompublished, 0, $reason, $userid);
        entry_reopened::create([
            'context' => \context_system::instance(),
            'userid' => $userid,
            'other' => ['domain' => $domain, 'entryid' => $entryid],
        ])->trigger();
    }

    private static function log_and_trigger(
        string $domain, int $entryid, string $action,
        ?string $fromstatus, ?string $tostatus, ?int $frompublished, ?int $topublished,
        ?string $reason, int $userid
    ): void {
        global $DB;

        $DB->insert_record('local_kid_publication_log', (object) [
            'domain' => $domain,
            'entryid' => $entryid,
            'action' => $action,
            'fromworkflowstatus' => $fromstatus,
            'toworkflowstatus' => $tostatus,
            'frompublished' => $frompublished,
            'topublished' => $topublished,
            'reason' => $reason,
            'userid' => $userid,
            'timecreated' => time(),
        ]);

        if ($action === 'status_change') {
            workflow_changed::create([
                'context' => \context_system::instance(),
                'userid' => $userid,
                'other' => ['domain' => $domain, 'entryid' => $entryid, 'fromstatus' => $fromstatus, 'tostatus' => $tostatus],
            ])->trigger();
        }
    }
}
