<?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.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Simulatore contabilità
 *
 * @package    mod_mtcontabilita
 * @copyright  2026 Mediatouch 2000 Srl <https://www.mediatouch.it>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @author     Marco Cappuccio <m.cappuccio@mediatouch.it>
 */

namespace mod_mtcontabilita;

use moodle_exception;
use stdClass;

/**
 * mtplayer class
 */
class mtplayer {
    /** @var cmid . */
    private $cmid;
    /** @var num . */
    private $num;
    /** @var tp . */
    private $tp;
    /** @var course . */
    private $course;
    /** @var attempt . */
    private $attempt;
    /** @var binit . */
    private $binit;
    /** @var bscritture . */
    private $bscritture;
    /** @var mtc . */
    private $mtc;
    /** @var pixvuota . */
    private $pixvuota;
    /** @var action Da scritture save o check. */
    private $action;
    /** @var macroclasse . */
    private $bmacroclasse;
    /** @var ndec . */
    private int $ndec;

    /**
     * Construct
     */
    public function __construct($cmid, $course, $attemptid, $num, $tp, $action="save2") {
        global $DB, $OUTPUT;
        $this->cmid = $cmid;
        $this->num = (int)$num;
        $this->tp = $tp;
        $this->course = $course;
        $this->attempt = $DB->get_record('mtcontabilita_attempts', ['id' => $attemptid]);
        if (!empty($this->attempt) && (int)$this->attempt->currentpage > 0) {
            // se num è 0 ma currentpage è 1+, preferisci currentpage
            if ($this->num === 0 && (int)$this->attempt->currentpage > 0) {
                $this->num = (int)$this->attempt->currentpage;
            }
        }

        $pars = ['mtcontabilitaid' => $this->attempt->mtcontabilitaid, 'type' => 0];
        $this->bscritture = $DB->record_exists('mtcontabilita_itemdet', $pars);
        // Dati del record principale.
        $sql = "SELECT mt.inline, mt.type, mt.binit, mt.slang, mt.bmacroclasse, mt.bdecimali, mt.options
                FROM {mtcontabilita} mt, {mtcontabilita_attempts} a
                WHERE mt.id=a.mtcontabilitaid AND a.id=?";
        $this->mtc = $DB->get_record_sql($sql, [$attemptid]);
        $this->binit = $this->mtc->binit;
        $this->bmacroclasse = !empty($this->mtc->bmacroclasse);

        $bdecimali = (int)($this->mtc->bdecimali ?? 2);
        $this->ndec = mtc_get_ndec_for_mtcontabilita((int)$this->attempt->mtcontabilitaid, $bdecimali);

        $this->pixvuota = $OUTPUT->pix_icon('i/completion-manual-n', '', 'moodle', ['class' => 'tree-icon']);
        $this->action = $action;



    }

    /**
     * Corpo centrale.
     */
    public function display($tp) {
        global $CFG, $DB, $OUTPUT;

        $rit = $pix = '';
        $pixpdf = $OUTPUT->pix_icon('t/download', '', 'moodle', ['class' => 'icon']);
        // SITUAZIONE INIZIALE.
        $txt = get_string('init', 'mtcontabilita');
        $pix = $this->pixvuota;
        // Se esiste un record significa che ho cominciato a rispondere.
        if ($DB->record_exists('mtcontabilita_attempt_items', ['attemptid' => $this->attempt->id, 'bitem' => 'i'])) {
            $pix = $OUTPUT->pix_icon('i/completion-manual-enabled', '', 'moodle', ['class' => 'tree-icon']);
            // Se non trovo campi vuoti ho risposto a tutto.
            $select = 'attemptid=:attemptid AND (scheme=:scheme OR itype=:itype OR da=:da) AND bitem=:bitem';
            $params = ['attemptid' => $this->attempt->id, 'scheme' => '', 'itype' => '', 'da' => '', 'bitem' => 'i'];
            if (!$DB->record_exists_select('mtcontabilita_attempt_items', $select, $params)) {
                $pix = $OUTPUT->pix_icon('i/completion-manual-y', '', 'moodle', ['class' => 'tree-icon']);
            }
        }
        // Pagina iniziale.
        // Pagina finale.
        $paginit = '';
        $pagend = "";
        if ($this->binit) {
            $paginit .= '<li>';
            if ($this->attempt->currentpage == 0) {
                $paginit .= $pix.$txt;
            } else {
                $paginit .= '<a href="'.$this->get_lnk(0, 0).'" class="">'.$pix.$txt.'</a>';
            }
            $paginit .= '</li>';
        }
            $txt = get_string('situazionefinale', 'mtcontabilita');
            $pix = $this->pixvuota;
            // SP.
            $pagend .= '<li>';
            if ($this->attempt->currentpage == 9999991) {
                $pagend .= $pix.$txt." ".get_string('sp', 'mtcontabilita');
            } else {
                $pagend .= '<a href="'.$this->get_lnk(9999991, 0).'" class="">'.
                    $pix.$txt.' '.get_string('sp', 'mtcontabilita').'</a>';
            }
            $pagend .= '</li>';
            // CE.
            $pagend .= '<li>';
            if ($this->attempt->currentpage == 9999992) {
                $pagend .= $pix.$txt." ".get_string('ce', 'mtcontabilita');
            } else {
                $pagend .= '<a href="'. $this->get_lnk(9999992, 0).'" class="">'.
                    $pix.$txt.' '.get_string('ce', 'mtcontabilita').'</a>';
            }
            $pagend .= '</li>';
        // }


        $mastroopen   = ($this->tp != 2);
        $giornaleopen = ($this->tp == 2);

        $mastroshow   = $mastroopen ? ' show' : '';
        $giornaleshow = $giornaleopen ? ' show' : '';

        $mastroexp    = $mastroopen ? 'true' : 'false';
        $giornaleexp  = $giornaleopen ? 'true' : 'false';



        $rit .= '<script src="'.$CFG->wwwroot.'/mod/mtcontabilita/js/jquery-3.6.0.min.js"></script>';
        $rit .= '<script src="'.$CFG->wwwroot.'/mod/mtcontabilita/js/jquery-ui.min.js"></script>';
        $rit .= '<div style="display: flex; height: 80vh;">';
        // Inizio menu.
        $rit .= '<div id="sidebar" style="width: 25%; background-color: #f0f0f0; padding: 20px;">';
        $rit .= '<ul style="list-style-type: none; padding-left: 0;">';
        // Inizio accordion per "Libro Mastro".
        $rit .= '
        <div class="accordion" id="accordionExample">
            <div class="card">
                <div id="d0" class="collapse show" aria-labelledby="l0" data-parent="#nocollapse">
                    <div class="card-body">';
                    $rit .= '<a href="pdf.php?tp=1&id='.$this->cmid.'">'.$pixpdf.get_string('scaricadesc', 'mtcontabilita').'</a>
                    </div>
                </div>
            </div>

            <div class="card">
                <div id="c0" class="collapse show" aria-labelledby="l0" data-parent="#nocollapse">
                    <div class="card-body">'
                    .$paginit.'
                    </div>
                </div>
            </div>

            <div class="card">
                <div class="card-header" id="headingOne">
                    <h2 class="mb-0">

                    <button class="btn btn-link btn-block text-left" type="button"
                        data-toggle="collapse" data-target="#collapseOne"
                    aria-expanded="'.$mastroexp.'" aria-controls="collapseOne">'.get_string('libromastromenu', 'mtcontabilita').'</button>

                    </h2>
                </div>
                <div id="collapseOne" class="collapse'.$mastroshow.'" aria-labelledby="headingOne" data-parent="#accordionExample">
                    <div class="card-body">';
                      $rit .= '            <ul style="list-style-type: none; padding-left: 0;">';
                      $rit .= $this->get_scritture(1); // Scritture dentro l'accordion.
                      $rit .= '            </ul>
                    </div>
                </div>
            </div>

            <div class="card">
                <div class="card-header" id="l2">
                    <strong>
                    <button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse"
                        data-target="#c2" aria-expanded="'.$giornaleexp.'" aria-controls="c2">
                    '.get_string('librogiornalemenu', 'mtcontabilita').'
                    </button>

                    </strong>
                </div>
                <div id="c2" class="collapse'.$giornaleshow.'" aria-labelledby="l2" data-parent="#accordionExample">
                    <div class="card-body">';
                        $rit .= '            <ul style="list-style-type: none; padding-left: 0;">';
                        $rit .= $this->get_scritture(2); // Scritture dentro l'accordion.
                        $rit .= '            </ul>
                    </div>
                </div>
            </div>

            <div class="card">
                <div class="card-header" id="l3">
                    <h2 class="mb-0">
                    <button class="btn btn-link btn-block text-left" type="button"
                        data-toggle="collapse" data-target="#c3" aria-expanded="false" aria-controls="c3">'.
                        get_string('situazionefinale', 'mtcontabilita').'</button>
                    </h2>
                </div>
                <div id="c3" class="collapse" aria-labelledby="l3" data-parent="#accordionExample">
                    <div class="card-body">';
                    $rit .= $pagend. '
                    </div>
                </div>
            </div>

        </div>';
        // Fine accordion per "Libro Mastro".
        $rit .= '</ul>';
        $rit .= '</div>';
        // Fine menu.

        // Inizio pagina.
        $rit .= '<div id="content" style="width: 75%; padding: 20px; background-color: #ffffff; overflow-y: auto;">';
        if ($this->binit && $this->attempt->currentpage == 0) {
            $rit .= $this->bodyi();
        } else if ($this->attempt->currentpage == 9999991) {
            $rit .= $this->bodyf('sp', $this->cmid, $this->attempt->mtcontabilitaid);
        } else if ($this->attempt->currentpage == 9999992) {
            $rit .= $this->bodyf('ce', $this->cmid, $this->attempt->mtcontabilitaid);
        } else {
            $rit .= $this->bodyx($this->num, $tp);
        }
        $rit .= '</div>';
        // Fine pagina.
        $rit .= '</div>';
        return $rit;
    }

    /**
     * Get scritture
     */
    private function get_scritture($tp) {
        global $DB, $OUTPUT;

        $scritture = "";
        if ($this->bscritture) {
            $sql = "SELECT *
                    FROM {mtcontabilita_itemdet}
                    WHERE mtcontabilitaid=:mtcontabilitaid AND type=:type
                    GROUP BY itemnum
                    ORDER BY sort,sortorder ASC";
            $recs = $DB->get_records_sql($sql, ['mtcontabilitaid' => $this->attempt->mtcontabilitaid, 'type' => 0]);
            foreach ($recs as $rec) {
                $pix = $this->pixvuota;
                // Se esiste un record significa che ho cominciato a rispondere.
                $params = ['attemptid' => $this->attempt->id, 'mtcontabilitaid' => $this->attempt->mtcontabilitaid, 'itemnum' => $rec->itemnum, 'bitem' => 's'];
                if ($DB->record_exists('mtcontabilita_attempt_items', $params)) {
                    $pix = $OUTPUT->pix_icon('i/completion-manual-enabled', '', 'moodle', ['class' => 'tree-icon']);
                    // Se non trovo campi vuoti ho risposto a tutto.
                    $params['listid'] = 0;
                    $params['scheme'] = '';
                    $params['bitem'] = 's';
                    $params['daamount'] = '';
                    $select = 'attemptid=:attemptid AND mtcontabilitaid=:mtcontabilitaid AND itemnum=:itemnum AND listid>:listid AND scheme!=:scheme
                    AND daamount!=:daamount AND bitem=:bitem';
                    if ($DB->record_exists_select('mtcontabilita_attempt_items', $select, $params)) {
                        $pix = $OUTPUT->pix_icon('i/completion-manual-y', '', 'moodle', ['class' => 'tree-icon']);
                    }
                }
                if ($this->attempt->currentpage == $rec->sort) {
                    $scritture .= '<li><a href="'.$this->get_lnk($rec->sort, $rec->id).'&tp='.$tp.'" class="vocesel">'.
                    $pix.get_string('scrittura', 'mtcontabilita', $rec->itemnum).'</a></li>';
                } else {
                    $scritture .= '<li><a href="'.$this->get_lnk($rec->sort, $rec->id).'&tp='.$tp.'" class="">'.
                    $pix.get_string('scrittura', 'mtcontabilita', $rec->itemnum).'</a></li>';
                }
            }
        }
        return $scritture;
    }

    /**
     * get lnk
     */
    private function get_lnk($n, $id) {
        return 'engine.php?det=' . $id .
            '&cmid=' . $this->cmid .
            '&num=' . $n .
            '&attemptid=' . $this->attempt->id;
    }

    /**
     * SITUAZIONE INIZIALE.
     */
    public function bodyi() {
        global $DB, $OUTPUT;

        $map = [true => 'ko', false => 'ok'];
        $elementi = [];
        $e = -1;
        $bcheck = false;
        $scoreut = 0;
        $scoretot = 0;

        // Estraggo prima del ciclo tutti i MC del corso, raggruppati per natura (SP / CE).
        $mcgroups = [];
        if ($this->bmacroclasse) {
            $sql = "SELECT macroclasse, description, scheme
                    FROM {mtcontabilita_listmc}
                    ORDER BY scheme, sortorder";
            $records = $DB->get_records_sql($sql, []);

            foreach ($records as $rec) {
                $schemekey = $rec->scheme;

                if (!isset($mcgroups[$schemekey])) {
                    $mcgroups[$schemekey] = [];
                }

                $mcgroups[$schemekey][] = [
                    'value' => $rec->macroclasse,
                    'label' => $rec->description,
                ];
            }
        }
        // Descrizione.
        $desc = $DB->get_field('mtcontabilita', 'description', ['id' => $this->attempt->mtcontabilitaid]);
        // 1. Recupera i dati dal database
        $sql = "SELECT d.id, l.name, d.amount, d.score, d.itemtype, l.scheme, l.macroclasse
                FROM {mtcontabilita_itemdet} d, {mtcontabilita_list} l
                WHERE d.listid=l.id
                AND d.mtcontabilitaid=? AND (d.type>? and d.type<?) ORDER BY l.name";
        $dati = $DB->get_records_sql($sql, [$this->attempt->mtcontabilitaid, 0, 5]);
        // 2. Prepara i dati per il rendering Mustache.
        foreach ($dati as $dato) {
            $params = ['attemptid' => $this->attempt->id, 'mtcontabilitaid' => $this->attempt->mtcontabilitaid, 'itemdetid' => $dato->id, 'bitem' => 'i'];
            if (!$r = $DB->get_record('mtcontabilita_attempt_items', $params, 'itemdetid,scheme,itype,da,macroclasse,bcheck,score')) {
                $r = (object)['itemdetid' => '', 'scheme' => '', 'itype' => '', 'da' => '', 'macroclasse' => '', 'bcheck' => 0, 'score' => 0];
            }
            // echo "<pre>"; var_dump($dato, $r); die(); // Risposta esatta - fornita
            $elementi[++$e] = [
                'itemdetid' => $dato->id,
                'denominazione_conto' => $dato->name,
                'saldo' => mtc_get_db($dato->amount, $this->ndec),
                'select_options' => [
                    ['value' => '', 'label' => get_string('choose'), 'selected' => $r->scheme === ''],
                    ['value' => 'ce', 'label' => get_string('ce', 'mtcontabilita'), 'selected' => $r->scheme === 'ce'],
                    ['value' => 'sp', 'label' => get_string('sp', 'mtcontabilita'), 'selected' => $r->scheme === 'sp'],
                ],
                'select_options1' => [
                    ['value' => '', 'label' => get_string('choose'), 'selected' => $r->itype === ''],
                    ['value' => '+', 'label' => '+', 'selected' => $r->itype === '+'],
                    ['value' => '-', 'label' => '-', 'selected' => $r->itype === '-'],
                ],
                'select_options2' => [
                    ['value' => '', 'label' => get_string('choose'), 'selected' => $r->da === ''],
                    ['value' => 'd', 'label' => get_string('tipo1', 'mtcontabilita'), 'selected' => $r->da === 'd'],
                    ['value' => 'a', 'label' => get_string('tipo2', 'mtcontabilita'), 'selected' => $r->da === 'a'],
                ],
            ];

            if ($this->bmacroclasse) {
                $elementi[$e]['select_options_mc'] = [];

                // Opzione vuota iniziale.
                $elementi[$e]['select_options_mc'][] = [
                    'isgroup'  => 0,
                    'value'    => '',
                    'label'    => get_string('choose'),
                    'selected' => ($r->macroclasse === ''),
                ];

                $order = ['CE', 'SP'];

                foreach ($order as $schemekey) {
                    $groupoptions = [];
                    foreach ($mcgroups[$schemekey] as $opt) {
                        $groupoptions[] = [
                            'value' => $opt['value'],
                            'label' => $opt['label'],
                            'selected' => ((string)$r->macroclasse === (string)$opt['value']),
                        ];
                    }

                    $grouplabel = ($schemekey === 'SP')
                        ? get_string('sp', 'mtcontabilita')
                        : get_string('ce', 'mtcontabilita');

                    $elementi[$e]['select_options_mc'][] = [
                        'isgroup' => 1,
                        'grouplabel' => $grouplabel,
                        'options' => $groupoptions,
                    ];
                }
            }
            $elementi[$e]['rowstatus'] = "";
            $elementi[$e]['score'] = $r->score;
            $scoretot += $dato->score;
            $scoreut += $r->score;

            // Flag default: tutto modificabile.
            $elementi[$e]['disable_scheme'] = false;
            $elementi[$e]['disable_da']     = false;
            $elementi[$e]['disable_mc']     = false;
            $corrections = [];
            // bcheck globale: almeno una riga è stata verificata
            $bcheck = $bcheck || ($r->bcheck == 1);

            if ($r->bcheck) {
                // Salvo le variabili di controllo.
                $error = [];
                $error[0] = (strtolower($r->scheme) !== strtolower($dato->scheme));
                $error[1] = (strtolower(@$r->da) !== strtolower($dato->itemtype));
                if ($this->bmacroclasse) {
                    $error[2] = (strtolower(@$r->macroclasse) !== strtolower($dato->macroclasse));
                }

                // Verifico se la risposta è esatta.
                $iserror = in_array(true, $error, true);
                $elementi[$e]['rowstatus'] = $map[$iserror];

                // Riga verificata.
                $elementi[$e]['bcheck'] = true;

                // *** NUOVO: abilita/disabilita le singole colonne ***
                // Se la colonna è corretta -> disabilita, se errata -> lasciata modificabile.
                $elementi[$e]['disable_scheme'] = !$error[0];
                $elementi[$e]['disable_da']     = !$error[1];
                if ($this->bmacroclasse && array_key_exists(2, $error)) {
                    $elementi[$e]['disable_mc'] = !$error[2];
                }

                // Gestione errore inline.
                if ($elementi[$e]['rowstatus'] == 'ko') {
                    if ($error[0]) {
                        $corrections['corretto_scheme'] = get_string(strtolower($dato->scheme), 'mtcontabilita');
                    }
                    if ($error[1]) {
                        $corrections['corretto_da'] = get_string(strtolower($dato->itemtype), 'mtcontabilita');
                    }
                    if ($this->bmacroclasse && !empty($error[2])) {
                        $corrections['corretto_mc'] = @$dato->macroclasse;
                    }
                }
            }
            $elementi[$e] = $elementi[$e] + $corrections;
        }
        // echo "<pre>"; var_export($elementi); die();
        // 3. Crea il contesto da passare al template
        $templatecontext = [
            'bcheck' => $bcheck,
            'cmid' => $this->cmid,
            'num' => $this->num,
            'inline' => $this->mtc->inline,
            'besame' => $this->mtc->type,
            'mtcontabilitaid' => $this->attempt->mtcontabilitaid,
            'attemptid' => $this->attempt->id,
            'elementi' => $elementi,
            'descrizione' => $desc,
            'scoreut' => $scoreut,
            'scoretot' => $scoretot,
            'bmacroclasse' => $this->bmacroclasse,   // <<< NEW
        ];
        // 4. Renderizza il template Mustache
        return $OUTPUT->render_from_template('mod_mtcontabilita/player/iniziale', $templatecontext);
    }

    /**
     * Valuta il punteggio pesato per una scrittura (sia tipo 1 che 2).
     *
     * $solutionentries: array di righe corrette, ogni elemento:
     *   [
     *     'listid' => int,      // conto
     *     'side'   => 'd'|'a',  // dare/avere
     *     'amount' => float,    // importo corretto
     *     'weight' => float,    // punteggio massimo di quella riga (es. 0.25)
     *   ]
     *
     * $studententries: array di righe inserite dallo studente, ogni elemento:
     *   [
     *     'listid' => int,
     *     'side'   => 'd'|'a',
     *     'amount' => float,
     *   ]
     *
     * $weights: percentuali prese da mtcontabilita->options:
     *   [
     *     'date'   => float,   // es. 10
     *     'account'=> float,   // es. 30
     *     'side'   => float,   // es. 30
     *     'amount' => float,   // es. 30
     *   ]
     *
     * $dateok: true se la data inserita coincide con quella corretta.
     *
     * Ritorna array [$scoreutente, $scoremassimo].
     */
    private function evaluate_weighted_score(
        array $solutionentries,
        array $studententries,
        array $weights,
        bool $dateok
    ): array {
        $wdate = $weights['date'] ?? 0.0;
        $waccount = $weights['account'] ?? 0.0;
        $wside = $weights['side'] ?? 0.0;
        $wamount = $weights['amount'] ?? 0.0;

        $scoremassimo = 0.0;
        foreach ($solutionentries as $se) {
            $scoremassimo += (float)($se['weight'] ?? 0.0);
        }

        // Segno tutti gli studententries come "non usati"
        foreach ($studententries as $k => $st) {
            $studententries[$k]['used'] = false;
        }

        $scoreutente = 0.0;

        // Per ogni riga corretta cerco la migliore riga studente compatibile
        foreach ($solutionentries as $se) {
            $rowmax = (float)($se['weight'] ?? 0.0);
            if ($rowmax <= 0) {
                continue;
            }

            // Parte di punteggio solo per la data (uguale per tutti, se corretta)
            $rowscore = 0.0;
            if ($dateok && $wdate > 0) {
                $rowscore += $rowmax * ($wdate / 100.0);
            }

            // Cerco tra le righe studente la migliore corrispondenza per questo conto
            $bestidx = null;
            $bestscore = 0.0;

            foreach ($studententries as $idx => $st) {
                if (!empty($st['used'])) {
                    continue;
                }

                if ((int)$st['listid'] !== (int)$se['listid']) {
                    continue; // conto diverso, niente
                }

                $partial = 0.0;

                // Conto giusto → peso "account"
                if ($waccount > 0) {
                    $partial += $rowmax * ($waccount / 100.0);
                }

                // Lato Dare/Avere corretto?
                if ($wside > 0 && $st['side'] === $se['side']) {
                    $partial += $rowmax * ($wside / 100.0);
                }

                // Importo corretto? (2 decimali)
                if ($wamount > 0) {
                    $asol = number_format((float)$se['amount'], 2, '.', '');
                    $astu = number_format((float)$st['amount'], 2, '.', '');
                    if ($asol === $astu) {
                        $partial += $rowmax * ($wamount / 100.0);
                    }
                }

                if ($partial > $bestscore) {
                    $bestscore = $partial;
                    $bestidx   = $idx;
                }
            }

            if ($bestidx !== null) {
                // ho trovato una riga studente da abbinare
                $studententries[$bestidx]['used'] = true;
                $rowscore += $bestscore;
            }

            $scoreutente += $rowscore;
        }

        return [$scoreutente, $scoremassimo];
    }

    /**
     * Scritture.
     */
    public function bodyx($num, $tp) {
        global $DB, $OUTPUT;

        $elencosuggerimenti = [];
        $mastrini = [];
        $lang = $this->mtc->slang;

        // Normalizzatore numerico lato PHP (coerente col JS).
        $normalizza = function(string $str) use ($lang): string {
            $str = trim($str);
            if ($str === '') {
                return '';
            }
            if (stripos($lang, 'en') === 0) {
                // EN: "1,234.56" -> "1234.56"
                $str = str_replace(',', '', $str);
                return $str;
            } else {
                // IT: "1.234,56" -> "1234.56"
                $str = str_replace('.', '', $str);
                $str = str_replace(',', '.', $str);
                return $str;
            }
        };

        // Numero scrittura (A), (B) ecc. per la pagina corrente.
        $itemnum = $DB->get_field('mtcontabilita_itemdet', 'itemnum',
            ['mtcontabilitaid' => $this->attempt->mtcontabilitaid, 'sort' => $num],
            IGNORE_MULTIPLE
        );

        // Descrizione esercizio
        $desc = $DB->get_field('mtcontabilita', 'description', ['id' => $this->attempt->mtcontabilitaid]);

        // Autocomplete: tutti i conti del corso, per lingua.
        $recs = $DB->get_records('mtcontabilita_list',
            ['courseid' => $this->course->id, 'lang' => $lang],
            'name ASC',
            'id,name'
        );
        foreach ($recs as $rec) {
            $elencosuggerimenti[] = ['id' => $rec->id, 'nome' => $rec->name];
        }

        // =========================
        // SOLUZIONE CORRETTA per la scrittura corrente (multiset di righe)
        // =========================
        $sqlsol = "SELECT id, listid, itemdate, itemtype, amount, score
                    FROM {mtcontabilita_itemdet}
                    WHERE mtcontabilitaid = :mtid
                    AND sort            = :sort
                    AND type            = 0
                    AND itemdate        > 0
                ORDER BY sortorder ASC,
                        CASE itemtype WHEN 'D' THEN 0 ELSE 1 END, id ASC";
        $paramssol = [
            'mtid' => $this->attempt->mtcontabilitaid,
            'sort' => $num,
        ];
        $soluzioni = $DB->get_records_sql($sqlsol, $paramssol);

        // Ogni riga soluzione → entry: listid | side(d/a) | amount
        $solutionentries = [];
        $solutioncount   = 0;
        $scoremassimo   = 0.0;
        $solutionfirstdate = 0;

        if ($soluzioni) {
            $first = reset($soluzioni);
            if ($first && !empty($first->itemdate)) {
                $solutionfirstdate = (int)$first->itemdate;
            }

            foreach ($soluzioni as $s) {
                $side = strtolower(trim($s->itemtype));
                if ($side !== 'd' && $side !== 'a') {
                    continue;
                }
                $amount = (float)$s->amount;
                $weight = (float)$s->score; // es. 0.25

                $solutionentries[] = [
                    'listid' => (int)$s->listid,
                    'side'   => $side,
                    'amount' => $amount,
                    'weight' => $weight,
                ];

                $solutioncount++;
                $scoremassimo += $weight;
            }
        }

        $solutionpos = []; // 0..N-1
        foreach ($solutionentries as $se) {
            $solutionpos[] = [
                'listid' => (int)$se['listid'],
                'side'   => (string)$se['side'], // 'd'/'a'
                'amount' => number_format((float)$se['amount'], 2, '.', ''),
            ];
        }
        $esitoglobale = 'ok';
        $scoretotale  = 0.0;
        $scoretotaledb = 0.0; // 🔹 somma dei punteggi salvati nei mastrini

        $itemdate = 0;
        $bcheck   = false; // true se almeno un record ha bcheck=1
        $attemptitemdate = 0;
        // Entry lato studente (come la soluzione: listid | side | amount).
        $studententries = [];
        // Leggo tutte le scritture salvate per questa pagina (sort=$num).
        $params = [
            'attemptid'       => $this->attempt->id,
            'mtcontabilitaid' => $this->attempt->mtcontabilitaid,
            'sort'            => $num,
            'bitem'           => 's',
        ];
        // Se apro il Libro Giornale (tp=2) elimino dal DB i "conti senza importo" (daamount vuoto).
        if ((int)$tp === 2) {
            $DB->delete_records_select(
                'mtcontabilita_attempt_items',
                'attemptid = :attemptid
                AND mtcontabilitaid = :mtcontabilitaid
                AND sort = :sort
                AND bitem = :bitem
                AND (daamount IS NULL OR daamount = :emptyjson OR daamount = :emptystr OR daamount = :nullstr)',
                [
                    'attemptid'       => $this->attempt->id,
                    'mtcontabilitaid' => $this->attempt->mtcontabilitaid,
                    'sort'            => $num,
                    'bitem'           => 's',
                    'emptyjson'       => '[]',
                    'emptystr'        => '',
                    'nullstr'         => 'null',
                ]
            );
        }

        $pl = 'id,listid,scheme,itype,da,daamount,parentid,bcheck,itemdate,score';
        if (!$recs = $DB->get_records('mtcontabilita_attempt_items', $params, 'id', $pl)) {
            // Nessuna scrittura ancora inserita: preparo un mastrino vuoto.
            $mastrini[] = [
                'listid'   => 0,
                'parentid' => 0,
                'testo'    => '',
                'righe'    => [ [ 'd' => '', 'a' => '' ] ],
                'righe2'   => [],
            ];
        } else {
            foreach ($recs as $rec) {
                // Sommo il punteggio di questo mastrino.
                $scoretotaledb += (float)$rec->score;

                if ($rec->itemdate) {
                    $itemdate = date('d/m/Y', $rec->itemdate);
                }
                if (empty($attemptitemdate)) {
                    $attemptitemdate = (int)$rec->itemdate;
                }

                // Dati riga utente: json → array di oggetti.
                $r = json_decode($rec->daamount);
                $rfors2 = json_decode($rec->daamount); // copia NON manipolata, solo per scrittura2.
                if (!is_array($r)) {
                    $r = (array)$r;
                }
                if (!is_array($rfors2)) {
                    $rfors2 = (array)$rfors2;
                }

                $listid   = $rec->listid;
                $listname = $DB->get_field('mtcontabilita_list', 'name', ['id' => $listid]);
                $righe    = [];

                // ===============================
                // COSTRUZIONE RIGHE MASTRINO PRINCIPALE (come prima)
                // ===============================
                if ($rec->parentid) {
                    // 1) Situazione iniziale.
                    $sql = "SELECT id.amount, ai.da
                            FROM {mtcontabilita_attempt_items} ai,
                                {mtcontabilita_itemdet}     id
                            WHERE ai.itemdetid = id.id
                            AND ai.id       = ?";
                    $sirec = $DB->get_record_sql($sql, [$rec->parentid]);

                    $delr0 = false;
                    $r0    = 0;

                    if ($sirec) {
                        $row0 = $r[0] ?? (object)['d' => '', 'a' => ''];

                        if ($sirec->da === 'd') {
                            if (!empty($row0->a)) {
                                $delr0 = true;
                            }
                            $righe[] = [
                                'd'        => mtc_get_db($sirec->amount, $this->ndec),
                                'a'        => mtc_get_db($row0->a, $this->ndec),
                                'dx'       => 'x',
                                'ax'       => '',
                                'readonly' => false,
                                'origine'  => '(i)',
                            ];
                        } else {
                            if (!empty($row0->d)) {
                                $delr0 = true;
                            }
                            $righe[] = [
                                'd'        => mtc_get_db($row0->d, $this->ndec),
                                'a'        => mtc_get_db($sirec->amount, $this->ndec),
                                'dx'       => '',
                                'ax'       => 'x',
                                'readonly' => false,
                                'origine'  => '(i)',
                            ];
                        }

                        // Elimino la riga 0 se è stata già incorporata.
                        if ($delr0) {
                            unset($r[0]);
                            $r0 = 1;
                        }
                    }

                    // 2) Scritture precedenti sullo stesso mastrino.
                    $sql = "SELECT ai.id, ai.daamount, ai.itemnum
                            FROM {mtcontabilita_attempt_items} ai
                            WHERE ai.attemptid       = ?
                            AND ai.mtcontabilitaid = ?
                            AND ai.itemnum        != ?
                            AND ai.sort           < ?
                            AND ai.bitem           = 's'
                            AND ai.parentid        = ?";
                    $precedenti = $DB->get_records_sql($sql, [
                        $this->attempt->id,
                        $this->attempt->mtcontabilitaid,
                        $itemnum,
                        $num,
                        $rec->parentid,
                    ]);

                    foreach ($precedenti as $prec) {
                        $righex = json_decode($prec->daamount);
                        if (!is_array($righex)) {
                            $righex = (array)$righex;
                        }

                        foreach ($righex as $riga) {
                            if (!is_object($riga)) {
                                $riga = (object)$riga;
                            }
                            $vd = isset($riga->d) ? $riga->d : '';
                            $va = isset($riga->a) ? $riga->a : '';

                            if ($vd === '' && $va === '') {
                                continue;
                            }

                            $righe[] = [
                                'd'        => mtc_get_db($vd, $this->ndec),
                                'a'        => mtc_get_db($va, $this->ndec),
                                'dx'       => ($vd !== '' ? 'x' : ''),
                                'ax'       => ($va !== '' ? 'x' : ''),
                                'readonly' => true,
                                'origine'  => '(' . $prec->itemnum . ')',
                            ];
                        }
                    }

                    // 3) Righe dell’utente per questa scrittura.
                    $r = array_values($r); // reindicizza
                    $countr = count($r);
                    for ($i = $r0; $i < $countr; $i++) {
                        $row = $r[$i];
                        if (!is_object($row)) {
                            $row = (object)$row;
                        }
                        $righe[] = [
                            'd' => mtc_get_db($row->d ?? '', $this->ndec),
                            'a' => mtc_get_db($row->a ?? '', $this->ndec),
                        ];
                    }
                } else {
                    // --- Nessun parentid: sole righe dell’utente per questa pagina ---
                    foreach ($r as $row) {
                        if (!is_object($row)) {
                            $row = (object)$row;
                        }
                        $righe[] = [
                            'd' => mtc_get_db($row->d ?? '', $this->ndec),
                            'a' => mtc_get_db($row->a ?? '', $this->ndec),
                        ];
                    }
                }

                // ===============================
                // Righe per SCRITTURA2 (tabella riassuntiva)
                // ===============================
                $righe2 = [];
                $righepost2 = [];
                $n = -1;

                foreach ((array)$rfors2 as $row) {
                    if (!is_object($row)) {
                        $row = (object)$row;
                    }

                    $vd = isset($row->d) ? trim((string)$row->d) : '';
                    $va = isset($row->a) ? trim((string)$row->a) : '';

                    if ($vd === '' && $va === '') {
                        continue;
                    }

                    $n++;

                    $mostradare  = ($vd !== '');
                    $mostraavere = ($va !== '');
                    $righepost2[] = [
                        'd' => mtc_get_db($vd, $this->ndec),
                        'a' => mtc_get_db($va, $this->ndec),
                    ];

                    // Valori formattati per display.
                    $righe2[$n] = [
                        'd'               => mtc_get_db($vd, $this->ndec),
                        'a'               => mtc_get_db($va, $this->ndec),
                        'mostra_dare'     => $mostradare,
                        'mostra_avere'    => $mostraavere,
                        'testo'           => $listname,
                        'itemnum'         => $itemnum,
                        // per ora niente lock/suggerimenti a livello di singolo campo
                        'disable_d'       => false,
                        'disable_a'       => false,
                        'corretto_d'      => '',
                        'corretto_a'      => '',
                        'conto_corretto'  => '',
                        'conto_sbagliato' => false,
                        'disable_conto_d' => false,
                        'disable_conto_a' => false,
                    ];

                    $righe2[$n]['_pos'] = $n; // posizione riga nel giornale
                    $righe2[$n]['_listid'] = (int)$listid;
                    $righe2[$n]['_side']   = $mostradare ? 'd' : 'a';
                    $righe2[$n]['_amount'] = $mostradare ? trim($vd) : trim($va); // raw numerico "inglese"
                    // ======= COSTRUZIONE ENTRIES LATO STUDENTE =======
                    // Ogni importo non vuoto genera una entry (listid, side, amount) per il confronto con la soluzione.
                    if ($mostradare && $vd !== '') {
                        // Il valore nel JSON è già in formato "inglese" (es. 1000.00)
                        $norm = trim($vd);
                        if ($norm !== '' && is_numeric($norm)) {
                            $studententries[] = [
                                'listid' => (int)$listid,
                                'side'   => 'd',
                                'amount' => (float)$norm,
                            ];
                        }
                    }

                    if ($mostraavere && $va !== '') {
                        $norm = trim($va);
                        if ($norm !== '' && is_numeric($norm)) {
                            $studententries[] = [
                                'listid' => (int)$listid,
                                'side'   => 'a',
                                'amount' => (float)$norm,
                            ];
                        }
                    }
                }

                // Accodo il mastrino.
                $mastrini[] = [
                    'listid'   => $listid,
                    'parentid' => $rec->parentid,
                    'testo'    => $listname,
                    'righe'    => ((int)$tp === 2) ? $righepost2 : $righe,
                    'righe2'   => $righe2,
                ];

                $bcheck = $bcheck || (bool)$rec->bcheck;
            } // foreach $recs
            $dateok = false;
            $solutiondatestr = '';
            $dateclass = '';
            if (!empty($solutionfirstdate) && !empty($attemptitemdate)) {
                $dateok = ($solutionfirstdate === $attemptitemdate);
            }
            if (!empty($solutionfirstdate)) {
                $solutiondatestr = date('d/m/Y', (int)$solutionfirstdate);
            }

            // Pesature da mtcontabilita->options
            $optsraw = $this->mtc->options ?? '[]';
            $opts    = json_decode($optsraw, true);
            if (!is_array($opts)) {
                $opts = [];
            }

            // I primi 3 valori sono per la Situazione iniziale.
            // Per le scritture usiamo gli indici 3,4,5,6 → 10,30,30,30 nell'esempio.
            $wdate    = isset($opts[3]) ? (float)$opts[3] : 10.0;
            $waccount = isset($opts[4]) ? (float)$opts[4] : 30.0;
            $wside    = isset($opts[5]) ? (float)$opts[5] : 30.0;
            $wamount  = isset($opts[6]) ? (float)$opts[6] : 30.0;

            $weights = [
                'date'    => $wdate,
                'account' => $waccount,
                'side'    => $wside,
                'amount'  => $wamount,
            ];
        }
        $scoretotale  = 0.0;

        if ($bcheck && !empty($solutionentries)) {

            // Pesi (0..1) da options (10/30/30/30 => 0.10/0.30/0.30/0.30)
            $ptot = 0.0;
            foreach ($weights as $w) { $ptot += (float)$w; }
            if ($ptot <= 0) { $ptot = 100.0; }

            $w_date    = ((float)($weights['date'] ?? 0.0))    / $ptot;
            $w_account = ((float)($weights['account'] ?? 0.0)) / $ptot;
            $w_side    = ((float)($weights['side'] ?? 0.0))    / $ptot;
            $w_amount  = ((float)($weights['amount'] ?? 0.0))  / $ptot;

            // 1) Punteggio data (indipendente)
            if ($w_date > 0 && !empty($solutionfirstdate) && !empty($attemptitemdate)) {
                $dateok = ((int)$solutionfirstdate === (int)$attemptitemdate);
                if ($dateok) {
                    $scoretotale += $w_date;
                }
            } else {
                $dateok = false;
            }

            // 2) Punteggio righe (ripartito sulle righe soluzione)
            $N = count($solutionentries);
            if ($N > 0) {
                $p_account = ($w_account > 0) ? ($w_account / $N) : 0.0;
                $p_side    = ($w_side > 0)    ? ($w_side / $N)    : 0.0;
                $p_amount  = ($w_amount > 0)  ? ($w_amount / $N)  : 0.0;

                // pool di soluzioni "non usate"
                $solpool = [];
                foreach ($solutionentries as $se) {
                    $solpool[] = [
                        'listid' => (int)$se['listid'],
                        'side'   => (string)$se['side'], // 'd'/'a'
                        'amount' => number_format((float)$se['amount'], 2, '.', ''),
                        'used'   => false,
                    ];
                }

                // helper: chiave amount normalizzata
                $fmtamount = function($v): string {
                    return number_format((float)$v, 2, '.', '');
                };

                // Match greedy: per ogni riga studente cerco la "miglior" soluzione libera
                foreach ($studententries as $st) {
                    $stlistid = (int)$st['listid'];
                    $stside   = (string)$st['side'];
                    $stamount = $fmtamount($st['amount']);

                    $best = -1;
                    $bestscore = -1;

                    // scoring match: 3=perfetto, 2=conto+side, 1=solo conto, 0=niente
                    foreach ($solpool as $i => $sol) {
                        if ($sol['used']) {
                            continue;
                        }
                        if ($sol['listid'] !== $stlistid) {
                            continue;
                        }

                        $m = 1; // almeno conto
                        if ($sol['side'] === $stside) {
                            $m = 2;
                            if ($sol['amount'] === $stamount) {
                                $m = 3;
                            }
                        } else {
                            // conto ok ma lato no: resta 1, ma potresti comunque avere amount uguale: non lo considero "meglio" del lato
                        }

                        if ($m > $bestscore) {
                            $bestscore = $m;
                            $best = $i;
                            if ($m === 3) { // perfetto, posso fermarmi
                                break;
                            }
                        }
                    }

                    if ($best >= 0) {
                        // consumo quella soluzione
                        $sol = $solpool[$best];
                        $solpool[$best]['used'] = true;

                        // conto corretto => punti account
                        $scoretotale += $p_account;

                        // lato corretto => punti side
                        if ($sol['side'] === $stside) {
                            $scoretotale += $p_side;
                        }

                        // importo corretto => punti amount
                        if ($sol['side'] === $stside && $sol['amount'] === $stamount) {
                            $scoretotale += $p_amount;
                        }
                    }
                }
            }

            // clamp + arrotondamento finale
            if ($scoretotale < 0) { $scoretotale = 0.0; }
            if ($scoretotale > 1) { $scoretotale = 1.0; }
            $scoretotale = $scoretotale * $scoremassimo;
            $scoretotale = round($scoretotale, 4);

            // Esito globale: OK solo se ho preso tutti i punti (include già la data se pesa).
            $eps = 0.00001;
            if ($scoremassimo > 0 && abs($scoretotale - $scoremassimo) < $eps) {
                $esitoglobale = 'ok';
            } else {
                $esitoglobale = 'ko';
            }


        } else {
            $esitoglobale = 'ko';
            // scoretotale resta 0
        }


        // NORMALIZZAZIONE hidden inputs per POST (come prima).
        foreach ($mastrini as $mi => &$m) {
            $m['idx'] = $mi;

            if (!empty($m['righe']) && is_array($m['righe'])) {
                foreach ($m['righe'] as $ri => &$riga) {
                    $riga['ridx'] = $ri;

                    $dx = !empty($riga['dx']) && $riga['dx'] === 'x';
                    $ax = !empty($riga['ax']) && $riga['ax'] === 'x';
                    $ro = !empty($riga['readonly']);

                    $hasd = array_key_exists('d', $riga) && $riga['d'] !== '';
                    $hasa = array_key_exists('a', $riga) && $riga['a'] !== '';

                    $darereadonly  = $dx || ($ro && $hasd);
                    $averereadonly = $ax || ($ro && $hasa);

                    $riga['d_post'] = $darereadonly ? 'x' : ($hasd ? (string)$riga['d'] : '');
                    $riga['a_post'] = $averereadonly ? 'x' : ($hasa ? (string)$riga['a'] : '');
                }
                unset($riga);
            }
        }
        unset($m);

        // POPUP SOLUZIONE CORRETTA (solo scrittura1, invariato)
        $soluzionehtml = '';
        $showsolution  = false;
        if ( $bcheck && $esitoglobale === 'ko') {
            $soluzionehtml = $this->build_correct_solution_html($num);
            if (!empty($soluzionehtml)) {
                $showsolution = true;
            }
        }

        // ===== STATUS PER-RIGA (tp=2) =====

        // Pool delle soluzioni (multiset): key = listid|side|amount(2dec)
        $pool = [];
        foreach ($solutionentries as $se) {
            $k = (int)$se['listid'] . '|' . $se['side'] . '|' . number_format((float)$se['amount'], 2, '.', '');
            $pool[$k] = ($pool[$k] ?? 0) + 1;
        }

        // Se la data è sbagliata, puoi decidere: tutte KO.
        // (di solito ha senso: se la data è requisito globale, tutto rosso)
        if (!empty($solutionfirstdate) && !empty($attemptitemdate)) {
            $dateok = ((int)$solutionfirstdate === (int)$attemptitemdate);
        } else {
            $dateok = false;
        }

        // Evidenziazione data solo dopo check
        if ($bcheck) {
            $dateclass = $dateok ? 'is-valid' : 'is-invalid';
        }

        // ===== STATUS + SUGGERIMENTI (tp=2, ordine libero) =====
        if ((int)$tp === 2) {
            // pool soluzioni: array di righe con flag "used"
            $solpool = [];
            foreach ($solutionpos as $i => $sp) {
                $solpool[] = [
                    'listid' => (int)$sp['listid'],
                    'side'   => (string)$sp['side'],   // d/a
                    'amount' => (string)$sp['amount'], // "10000.00"
                    'used'   => false,
                ];
            }

            // helper: trova indice soluzione secondo predicato (prima non usata)
            $findsol = function(callable $pred) use (&$solpool): int {
                foreach ($solpool as $i => $s) {
                    if (!$s['used'] && $pred($s)) {
                        return $i;
                    }
                }
                return -1;
            };

            // helper: format amount corretta sul lato giusto
            $setcorrectamount = function(array $sol, array &$r2) {
                if ($sol['side'] === 'd') {
                    $r2['corretto_d'] = mtc_get_db($sol['amount'], $this->ndec);
                    $r2['corretto_a'] = '';
                } else {
                    $r2['corretto_a'] = mtc_get_db($sol['amount'], $this->ndec);
                    $r2['corretto_d'] = '';
                }
            };

            foreach ($mastrini as &$m) {
                if (empty($m['righe2']) || !is_array($m['righe2'])) {
                    continue;
                }
                foreach ($m['righe2'] as &$r2) {
                    $r2['status'] = '';   // ok/ko
                    $r2['conto_sbagliato'] = false;
                    $r2['pos_conto_sbagliata'] = false;
                    $r2['conto_corretto'] = '';
                    $r2['importo_sbagliato'] = false;
                    $r2['pos_importo_sbagliata'] = false;
                    $r2['corretto_d'] = '';
                    $r2['corretto_a'] = '';
                    $r2['side_corretto'] = '';
                    $r2['show_sugg_dare']  = false;
                    $r2['show_sugg_avere'] = false;


                    if (!$bcheck) {
                        continue;
                    }
                    $stlistid = (int)($r2['_listid'] ?? 0);
                    $stside   = (string)($r2['_side'] ?? '');
                    $stamount = trim((string)($r2['_amount'] ?? ''));
                    $stamountfmt = (is_numeric($stamount)) ? number_format((float)$stamount, 2, '.', '') : '';

                    // Se riga incompleta, la segno KO
                    if ($stlistid <= 0 || ($stside !== 'd' && $stside !== 'a') || $stamountfmt === '') {
                        $r2['status'] = 'ko';
                        continue;
                    }
                    // 1) match perfetto
                    $idx = $findsol(fn($s) => $s['listid'] === $stlistid && $s['side'] === $stside && $s['amount'] === $stamountfmt);
                    // 2) conto+side
                    if ($idx < 0) {
                        $idx = $findsol(fn($s) => $s['listid'] === $stlistid && $s['side'] === $stside);
                    }
                    // 3) conto+amount
                    if ($idx < 0) {
                        $idx = $findsol(fn($s) => $s['listid'] === $stlistid && $s['amount'] === $stamountfmt);
                    }
                    // 4) side+amount
                    if ($idx < 0) {
                        $idx = $findsol(fn($s) => $s['side'] === $stside && $s['amount'] === $stamountfmt);
                    }
                    // 5) solo conto
                    if ($idx < 0) {
                        $idx = $findsol(fn($s) => $s['listid'] === $stlistid);
                    }
                    // se non ho trovato nulla, prendo "prima disponibile" solo per suggerire
                    $suggestidx = $idx;
                    if ($suggestidx < 0) {
                        $suggestidx = $findsol(fn($s) => true);
                    }

                    if ($idx >= 0) {
                        // ho una soluzione “associata” (anche se non perfetta)
                        $solpool[$idx]['used'] = true;
                        $sol = $solpool[$idx];

                        $r2['show_sugg_dare']  = ($sol['side'] === 'd');
                        $r2['show_sugg_avere'] = ($sol['side'] === 'a');

                        $accountexact = ($sol['listid'] === $stlistid);
                        $sideexact    = ($sol['side'] === $stside);
                        $amountexact  = ($sol['amount'] === $stamountfmt);
                        // status ok solo se perfetto.
                        $r2['status'] = ($accountexact && $sideexact && $amountexact) ? 'ok' : 'ko';

                        // conto: se sbagliato suggerisco SEMPRE il conto corretto della soluzione abbinata.
                        if (!$accountexact) {
                            $r2['conto_sbagliato'] = true;
                            $r2['conto_corretto']  = (string)$DB->get_field('mtcontabilita_list', 'name', ['id' => $sol['listid']]) ?: '';
                        }
                        // Lato: se sbagliato suggerisco il lato.
                        if (!$sideexact) {
                            $r2['pos_conto_sbagliata'] = true;
                            $r2['pos_importo_sbagliata'] = true;
                            $r2['side_corretto'] = ($sol['side'] === 'd')
                                ? get_string('tipo1', 'mtcontabilita')
                                : get_string('tipo2', 'mtcontabilita');
                        }
                        // importo: se sbagliato suggerisco importo corretto sul lato della soluzione.
                        if (!$amountexact) {
                            $r2['importo_sbagliato'] = true;
                            $setcorrectamount($sol, $r2);
                        }

                    } else {
                        // non matchata: KO e suggerisco (se posso) una soluzione rimasta.
                        $r2['status'] = 'ko';

                        if ($suggestidx >= 0) {
                            $sol = $solpool[$suggestidx];
                            $r2['show_sugg_dare']  = ($sol['side'] === 'd');
                            $r2['show_sugg_avere'] = ($sol['side'] === 'a');
                            $r2['conto_sbagliato'] = true;
                            $r2['conto_corretto']  = (string)$DB->get_field('mtcontabilita_list', 'name', ['id' => $sol['listid']]) ?: '';
                            $r2['pos_conto_sbagliata'] = true;
                            $r2['side_corretto'] = ($sol['side'] === 'd')
                                ? get_string('tipo1', 'mtcontabilita')
                                : get_string('tipo2', 'mtcontabilita');
                            $r2['importo_sbagliato'] = true;
                            $setcorrectamount($sol, $r2);
                        }
                    }
                }
                unset($r2);
            }
            unset($m);
        }
        $scorestring = '';
        if ($bcheck && $scoremassimo > 0) {
            $scorestring = number_format($scoretotale, 2, ',', '.') . ' / ' .
                            number_format($scoremassimo, 2, ',', '.');
        }
        
        // Context per Mustache
        $templatecontext = [
            'esito'   => strtoupper($esitoglobale),
            'clesito' => ($esitoglobale === 'ok') ? 'success' : 'danger',
            'score'   => $scorestring,              // 👈 stringa tipo "8,50 / 10,00"
            'score_raw' => $scoretotale,            // 👈 numerico 8.5 se ti serve
            'tp'                 => $tp,
            'itemdate'           => $itemdate,
            'dateclass'          => $dateclass,
            'dateok'             => $dateok,
            'solutiondate'       => $solutiondatestr,
            'bcheck'             => $bcheck,
            'inline'             => $this->mtc->inline,
            'cmid'               => $this->cmid,
            'num'                => $this->num,
            'itemnum'            => $itemnum,
            'mastrini'           => $mastrini,
            'mastriniJSON'       => json_encode($mastrini),
            'mtcontabilitaid'    => $this->attempt->mtcontabilitaid,
            'attemptid'          => $this->attempt->id,
            'descrizione'        => $desc,
            'elencoSuggerimenti' => json_encode($elencosuggerimenti),
            'lang'               => $this->mtc->slang,
            'showsolution'       => $showsolution,
            'soluzionehtml'      => $soluzionehtml,
            'ndec'               => $this->ndec,   // oppure dove hai già calcolato ndec
        ];

        return $OUTPUT->render_from_template('mod_mtcontabilita/player/scrittura'.$this->tp, $templatecontext);
    }

    /**
     * Costruisce l'HTML della soluzione corretta per la scrittura (pagina) $sort.
     * Usata SOLO per scrittura1 (tp=1).
     *
     * @param int $sort Numero di pagina / sort corrente.
     * @return string HTML tabellare con Conto / Dare / Avere.
     */
    protected function build_correct_solution_html(int $sort): string {
        global $DB;

        // Prendo TUTTE le righe di soluzione per quella scrittura
        // Prima colonna: id (unico) per evitare il warning "Did you remember..."
        $sql = "SELECT id, listid, itemtype, amount
                FROM {mtcontabilita_itemdet}
                WHERE mtcontabilitaid = :mtid
                AND sort            = :sort
                AND type            = 0
                AND itemdate        > 0
            ORDER BY sortorder ASC,
                    CASE itemtype WHEN 'D' THEN 0 ELSE 1 END, id ASC";
        $params = [
            'mtid' => $this->attempt->mtcontabilitaid,
            'sort' => $sort,
        ];

        $records = $DB->get_records_sql($sql, $params);
        if (!$records) {
            return '';
        }

        // Precarico i nomi dei conti.
        $listids = [];
        foreach ($records as $r) {
            $listids[$r->listid] = $r->listid;
        }

        $names = [];
        if (!empty($listids)) {
            list($insql, $inparams) = $DB->get_in_or_equal($listids, SQL_PARAMS_NAMED);
            $names = $DB->get_records_select_menu('mtcontabilita_list', "id $insql", $inparams, '', 'id, name');
        }

        // Costruisco la tabella HTML (Denominazione conto | Dare | Avere)
        $out  = \html_writer::start_tag('table', ['class' => 'generaltable mtc-solution-table']);
        $out .= \html_writer::start_tag('thead');
        $out .= \html_writer::tag('tr',
            \html_writer::tag('th', get_string('denominazioneconto', 'mtcontabilita')) .
            \html_writer::tag('th', get_string('d', 'mtcontabilita')) .
            \html_writer::tag('th', get_string('a', 'mtcontabilita'))
        );
        $out .= \html_writer::end_tag('thead');

        $out .= \html_writer::start_tag('tbody');

        foreach ($records as $r) {
            $nome = $names[$r->listid] ?? '';

            $dare  = ($r->itemtype === 'D') ? mtc_get_db($r->amount, $this->ndec) : '';
            $avere = ($r->itemtype === 'A') ? mtc_get_db($r->amount, $this->ndec) : '';

            $out .= \html_writer::tag('tr',
                \html_writer::tag('td', s($nome)) .
                \html_writer::tag('td', $dare,  ['class' => 'cellright', 'style' => 'text-align: right']) .
                \html_writer::tag('td', $avere, ['class' => 'cellright', 'style' => 'text-align: right'])
            );
        }

        $out .= \html_writer::end_tag('tbody');
        $out .= \html_writer::end_tag('table');

        return $out;
    }

    /**
     * Pagina finale.
     */
    public function bodyf($sez, $cmid, $id) {
        global $DB, $CFG;

        $slist = [];
        require_once($CFG->dirroot."/mod/mtcontabilita/locallib.php");
        $alist = ['spd' => [], 'spa' => [], 'ced' => [], 'cea' => [], 'pnd' => [], 'pna' => []];
        $atot = ['spd' => 0, 'spa' => 0, 'ced' => 0, 'cea' => 0, 'pnd' => 0, 'pna' => 0];
        $parsez = $sez == "sp" ? "'sp','pn'" : "'ce'";
        // *********************************************************************
        // Elenco scritture. type=0
        // *********************************************************************
        $sql = "SELECT i.id, l.name, l.scheme, i.listid, i.daamount
        FROM {mtcontabilita_attempt_items} i, {mtcontabilita_list} l
        WHERE i.listid=l.id
        AND i.mtcontabilitaid=? AND i.attemptid=?";// AND l.scheme in ($parsez)";
        $recs = $DB->get_records_sql($sql, [$id, $this->attempt->id, $sez]);
        foreach ($recs as $rec) {
            // Da daamount devo ricavare amount=##### e da=d|a.
            $rows = json_decode($rec->daamount);
            foreach ($rows as $ddaa) {
                foreach ($ddaa as $k => $amount) {
                    // Se la cifra è maggiore di 0 allora registro la riga.
                    if ($amount > 0) {
                        $col = $this->get_col($rec->scheme.$k);
                        if (!isset($slist[$rec->listid][$col])) {
                            $slist[$rec->listid][$col] = [$rec->name, 0];
                        }
                        $slist[$rec->listid][$col][1] += $amount;
                    }
                }
            }
        }
        // *********************************************************************
        // SITUAZIONE INIZIALE. type=1
        // *********************************************************************
        $sql = "SELECT d.id, l.name, d.amount, d.listid, a.da, a.scheme
                FROM {mtcontabilita_attempt_items} a, {mtcontabilita_itemdet} d, {mtcontabilita_list} l
                WHERE l.id=d.listid AND a.itemdetid=d.id
                AND d.type=? AND d.mtcontabilitaid=? AND a.attemptid=? AND a.da!=? "; // AND a.scheme IN ($parsez)
        $sql .= "ORDER BY a.da,d.sortorder";
        $recs = $DB->get_records_sql($sql, [1, $id, $this->attempt->id, ""]);
        foreach ($recs as $rec) {
            // echo "<pre>"; var_dump($rec); die();
            $col = $this->get_col($rec->scheme.$rec->da);
            if (!isset($slist[$rec->listid][$col])) {
                $slist[$rec->listid][$col] = [$rec->name, 0];
            }
            $slist[$rec->listid][$col][1] += $rec->amount;
        }
        // *** NUOVO: accorpamento voci presenti su entrambi i lati ***
        $slist = $this->merge_slist_contraposti($slist, $sez);

        $col1 = []; // Dare.
        $col2 = []; // avere.
        $col3 = []; // dare.
        $col4 = []; // avere.

        foreach ($slist as $listid => $columns) {
            foreach ($columns as $col => $val) {
                switch ($col) {
                    case 1:
                        $col1[] = $val;
                        break;
                    case 2:
                        $col2[] = $val;
                        break;
                    case 3:
                        $col3[] = $val;
                        break;
                    case 4:
                        $col4[] = $val;
                        break;
                }
            }
        }
        // var_dump($sez); die();
        // Calcola la lunghezza massima
        $maxlen = max(count($col1), count($col2), count($col3), count($col4));
        // Riempie con valori vuoti
        $col1 = array_pad($col1, $maxlen, ["", ""]);
        $col2 = array_pad($col2, $maxlen, ["", ""]);
        $col3 = array_pad($col3, $maxlen, ["", ""]);
        $col4 = array_pad($col4, $maxlen, ["", ""]);

        $table = $this->display_table_sf($col1, $col2, $col3, $col4, $maxlen, $sez);
        return $table;
    }

    /**
     * get_col
     */
    private function get_col($ctrl) {
        switch (strtolower($ctrl)) {
            case "spd":
            case "pnd":
                $rit = 1;
                break;
            case "spa":
            case "pna":
                $rit = 2;
                break;
            case "ced":
                $rit = 3;
                break;
            case "cea":
                $rit = 4;
                break;
        }
        return $rit;
    }

    /**
     * Accorpa le voci presenti su entrambi i lati (es. stesso conto in Attivo e Passivo,
     * oppure in Costi e Ricavi), mantenendo solo il lato con importo maggiore
     * e usando la differenza come nuovo importo.
     *
     * @param array  $slist [listid => [col => [name, amount]]]
     * @param string $sez   'sp' oppure 'ce'
     * @return array
     */
    private function merge_slist_contraposti(array $slist, string $sez): array {
        foreach ($slist as $listid => $columns) {

            if ($sez === 'sp') {
                // Colonne 1 = Attivo, 2 = Passivo
                $attivo  = isset($columns[1]) ? (float)$columns[1][1] : 0.0;
                $passivo = isset($columns[2]) ? (float)$columns[2][1] : 0.0;

                if ($attivo > 0 && $passivo > 0) {
                    if ($attivo > $passivo) {
                        // Rimane solo Attivo con la differenza.
                        $columns[1][1] = $attivo - $passivo;
                        unset($columns[2]);
                    } else if ($passivo > $attivo) {
                        // Rimane solo Passivo con la differenza.
                        $columns[2][1] = $passivo - $attivo;
                        unset($columns[1]);
                    } else {
                        // Importi uguali: il conto si annulla.
                        unset($columns[1], $columns[2]);
                    }
                }

            } else if ($sez === 'ce') {
                // Colonne 3 = Costi, 4 = Ricavi
                $costi  = isset($columns[3]) ? (float)$columns[3][1] : 0.0;
                $ricavi = isset($columns[4]) ? (float)$columns[4][1] : 0.0;

                if ($costi > 0 && $ricavi > 0) {
                    if ($costi > $ricavi) {
                        // Rimangono solo i Costi (col 3) con differenza.
                        $columns[3][1] = $costi - $ricavi;
                        unset($columns[4]);
                    } else if ($ricavi > $costi) {
                        // Rimangono solo i Ricavi (col 4) con differenza.
                        $columns[4][1] = $ricavi - $costi;
                        unset($columns[3]);
                    } else {
                        // Importi uguali: il conto si annulla.
                        unset($columns[3], $columns[4]);
                    }
                }
            }

            // Se dopo le operazioni non è rimasto nulla, rimuovo del tutto il listid.
            if (empty($columns)) {
                unset($slist[$listid]);
            } else {
                $slist[$listid] = $columns;
            }
        }

        return $slist;
    }

    /**
     * display table sf
     */
    private function display_table_sf($col1, $col2, $col3, $col4, $maxlen, $sez) {
        global $OUTPUT;
        // echo "<pre>"; var_dump($col1, $col2, $col3, $col4); die();
        $body = "";
        $tot = [0, 0, 0, 0];
        for ($i = 0; $i < $maxlen; $i++) {
            $body .= '<tr>';
            // Valori formattati (vuoto se importo = 0).
            $v11 = !empty($col1[$i][1]) ? mtc_get_db($col1[$i][1], $this->ndec) : '';
            $v12 = !empty($col2[$i][1]) ? mtc_get_db($col2[$i][1], $this->ndec) : '';
            $v13 = !empty($col3[$i][1]) ? mtc_get_db($col3[$i][1], $this->ndec) : '';
            $v14 = !empty($col4[$i][1]) ? mtc_get_db($col4[$i][1], $this->ndec) : '';
            if ($sez != "ce") {
                $body .= '<td>'.$col1[$i][0].'</td><td align="right">'.$v11.'</td>';
                $body .= '<td>'.$col2[$i][0].'</td><td align="right">'.$v12.'</td>';
            } else {
                $body .= '<td>'.$col3[$i][0].'</td><td align="right">'.$v13.'</td>';
                $body .= '<td>'.$col4[$i][0].'</td><td align="right">'.$v14.'</td>';
            }
            $tot[0] += $col1[$i][1] > 0 ? $col1[$i][1] : 0;
            $tot[1] += $col2[$i][1] > 0 ? $col2[$i][1] : 0;
            $tot[2] += $col3[$i][1] > 0 ? $col3[$i][1] : 0; // Costi.
            $tot[3] += $col4[$i][1] > 0 ? $col4[$i][1] : 0; // Ricavi.
            $body .= '</tr>';
        }
        $totricavi = $tot[3];
        $totcosti = $tot[2];
        $totb = $totricavi - $totcosti;
        // *********************************************************************
        $str = '<th class="col-1">'.get_string('denominazioneconto', 'mtcontabilita').'</th><td align="right"><b>'.get_string('saldo', 'mtcontabilita').'</b></td>';
        $strtotb = '<tr>';
        $strtotb .= '<th>&nbsp;</th><th>&nbsp;</th>';
        $strtotb .= '<th>&nbsp;</th><th>&nbsp;</th>';
        $cspan = 4;
        switch ($sez) {
            case "sp":
                $thead = '<th class="col-4" colspan="4"><div align="center">'.get_string('statopatrimoniale', 'mtcontabilita').'</div></th>';
                $thead2 = '<th class="col-2" colspan="2"><div align="center">'.get_string('attivo', 'mtcontabilita').'</div></th>
                <th class="col-2" colspan="2"><div align="center">'.get_string('passivor', 'mtcontabilita').'</div></th>';
                $thead3 = $str.$str;
                $strtot = '<td>Tot '.get_string('attivo', 'mtcontabilita').'</td><td align="right"><b>'.mtc_get_db(abs($tot[0]), $this->ndec).'</b></td>';
                $strtot .= '<td>Tot '.get_string('passivor', 'mtcontabilita').'</td><td align="right"><b>'.mtc_get_db(abs($tot[1] + $totb), $this->ndec).'</b></td>';
                break;
            case "ce":
                $thead = '<th class="col-4" colspan="4"><div align="center">'.get_string('contoeconomico', 'mtcontabilita').'</div></th>';
                $thead2 = '<th class="col-2" colspan="2"><div align="center">'.get_string('costi', 'mtcontabilita').'</div></th>
                <th class="col-2" colspan="2"><div align="center">'.get_string('ricavi', 'mtcontabilita').'</div></th>';
                $thead3 = $str.$str;
                // Stabilisco a chi devo sommare utile/perdita.
                if ($totricavi >= $totcosti) {
                    $aa = $totcosti + abs($totb);
                    $bb = $totricavi;
                } else {
                    $aa = $totcosti;
                    $bb = $totricavi + abs($totb);
                }
                $strtot = '<td>'.get_string('total', 'mtcontabilita').'</td><td align="right"><b>'.mtc_get_db(abs($aa), $this->ndec).'</b></td>';
                $strtot .= '<td>'.get_string('total', 'mtcontabilita').'</td><td align="right"><b>'.mtc_get_db(abs($bb), $this->ndec).'</b></td>';
                $strtotb = "";
                break;
        }
        $strtotb .= '</tr>';
        // *********************************************************************
        $table = '';
        $table .= '<form method="POST" name="frm_final">';
        $table .= '<table class="table table-bordered">';
        $table .= '<thead>';
        $table .= '<tr>'.$thead.'</tr>';
        $table .= '<tr>'.$thead2.'</tr>';
        $table .= '<tr>'.$thead3.'</tr>';
        $table .= '</thead>';
        $table .= '<tbody>';
        $table .= $body;
        $table .= $strtotb;
        $table .= '<tr><td colspan="'.$cspan.'">&nbsp;</td></tr>';
        if ($sez == 'ce') {
            if ($totricavi >= $totcosti) {
                $table .= '<tr><td>'.get_string('utileperdita1', 'mtcontabilita').'</td><td align="right">' . mtc_get_db(abs($totb), $this->ndec) . '</td></tr>';
            } else {
                $table .= '<tr><td>&nbsp;</td><td>&nbsp;</td><td>'.get_string('utileperdita2', 'mtcontabilita').'</td><td align="right">'.mtc_get_db(abs($totb), $this->ndec).
                '</td></tr>';
            }
        } else {
            $label = $totb < 0 ? 'utileperdita2' : 'utileperdita1';
            if ($totb < 0) {
                $strtotb = '<span style="color:red">('.mtc_get_db(abs($totb), $this->ndec).')</span>';
            } else {
                $strtotb = mtc_get_db(abs($totb), $this->ndec);
            }
            $table .= '<tr><td>&nbsp;</td><td>&nbsp;</td><td>'.get_string($label, 'mtcontabilita').'</td><td align="right">'.$strtotb.'</td></tr>';
        }
        $table .= '<tr>'.$strtot.'</tr>';
        $table .= '</tbody>';
        $table .= '</table>';

        $table .= '<div class="btn-group mt-6">
            <button type="submit" name="action" value="finish" class="btn btn-primary" id="action">'.get_string('finish', 'mtcontabilita').'</button>
        </div>';
        $table .= '</form>';
        return $table;
    }

    /**
     * Conclude il tentativo e riporta ad una pagina finale di cortesia.
     */
    public function finish_attempt() {
        global $DB;

        $rec = $this->attempt;
        $adesso = time();
        $rec->timefinish = $adesso;
        $rec->timemodified = $adesso;
        $rec->state = 'finished';
        $DB->update_record('mtcontabilita_attempts', $rec);
        $params = ['id' => $this->cmid, 'aid' => $this->attempt->id];
        $url = new \moodle_url('/mod/mtcontabilita/player/endattempt.php', $params);
        redirect($url);
    }
}
