<?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>
 */

require_once('../../../config.php');
require_once($CFG->dirroot.'/mod/mtcontabilita/question2/locallib.php');

require_login();

require_once($CFG->dirroot.'/mod/mtcontabilita/locallib.php');

$debugscr = 0;

// Se c'è attemptid in POST, enforce del timelimit.
if (!empty($_POST['attemptid'])) {
    $aidpost = 0;
    if (!empty($_POST['attemptid'])) {
        $aidpost = (int)$_POST['attemptid'];
    }

    if (!empty($aidpost)) {
        $tinfo = mtc_timeinfo_for_attempt($aidpost);
        if ($tinfo['overdue']) {
            $rec = $DB->get_record('mtcontabilita_attempts', ['id' => $aidpost], '*', MUST_EXIST);
            if ($rec->state !== 'finished') {
                $rec->state = 'finished';
                $rec->timefinish = time();
                $rec->timemodified = $rec->timefinish;
                $DB->update_record('mtcontabilita_attempts', $rec);
            }
            $cmid = required_param('cmid', PARAM_INT);
            redirect(new moodle_url('/mod/mtcontabilita/player/endattempt.php', ['id' => $cmid, 'aid' => $aidpost]));
        }
    }
}

$action  = required_param('action', PARAM_ALPHAEXT); // 'save' o 'check'
$ischeck = ($action === 'check');

$tp     = required_param('tp',     PARAM_INT);   // 1=Scrittura1, 2=Scrittura2 (giornale)

// NB: non uso optional_param_array perché data_submitted() restituisce già tutto.
if ($data = data_submitted()) {
    // echo "<pre>"; var_dump($data); die();
    $adesso          = time();
    $attemptid = isset($data->attemptid) ? (int)$data->attemptid : 0;
    if (empty($attemptid)) {
        throw new moodle_exception('Missing attemptid in POST');
    }
    $mtcontabilitaid = (int)$data->mtcontabilitaid;
    $itemnum         = (string)$data->itemnum; // testo (es. "a)")
    $cmid            = (int)$data->cmid;
    $num             = (int)$data->num;        // sort/pagina

    // Pesi dell'istanza (options): [0..2 SI], [3..6 scritture]
    $apesi = $DB->get_field('mtcontabilita', 'options', ['id' => $mtcontabilitaid]);
    $peso  = $apesi ? json_decode($apesi, true) : [];

    // Pesi scrittura (indici 3,4,5,6): data / conto / dare-avere / importo
    $pdata    = isset($peso[3]) ? (int)$peso[3] : 0;
    $pconto   = isset($peso[4]) ? (int)$peso[4] : 0;
    $pside    = isset($peso[5]) ? (int)$peso[5] : 0;
    $pimporto = isset($peso[6]) ? (int)$peso[6] : 0;
    $ptot     = $pdata + $pconto + $pside + $pimporto;
    if ($ptot == 0) {
        $ptot = 100; // fallback: evitiamo div/0
    }

    $mastrini        = $data->mastrini ?? [];
    // ===== Mappa conti per risolvere listid da "testo" (valido per scrittura1 e scrittura2) =====
    $mtrec = $DB->get_record('mtcontabilita', ['id' => $mtcontabilitaid], 'id, course, slang', MUST_EXIST);
    $courseid = (int)$mtrec->course;
    $slang    = $mtrec->slang ?: 'it';

    $elencoconti = $DB->get_records('mtcontabilita_list',
        ['courseid' => $courseid, 'lang' => $slang],
        '',
        'id, name'
    );
    // mappa "nome lower" -> id
    $mapconti = [];
    foreach ($elencoconti as $c) {
        $key = core_text::strtolower(trim($c->name));
        $mapconti[$key] = (int)$c->id;
    }
    // --- itemdate (d/m/Y -> timestamp a mezzanotte) ---
    $itemdate = 0;
    $idt = isset($data->itemdate) ? trim($data->itemdate) : '';
    if ($idt !== '') {
        $dt = DateTime::createFromFormat('d/m/Y', $idt);
        if ($dt instanceof DateTime) {
            $dt->setTime(0, 0, 0);
            $itemdate = $dt->getTimestamp();
        }
    }
    // Cast robusti: $mastrini può essere array di oggetti o array
    if (is_object($mastrini)) {
        $mastrini = (array)$mastrini;
    }
    // 🔹 ORDINA I MASTRINI: prima quelli con DARE, poi misti, poi solo AVERE
    $mastrini = mtc_ordina_mastrini_dare_avere($mastrini);


    $debugdata   = [];
    $transaction = null;

    try {
        // In modalità normale (NO debug o azione != check) usiamo la transazione.
        if (!$debugscr || !$ischeck) {
            $transaction = $DB->start_delegated_transaction();

            // 1) PULIZIA: per la pagina corrente -> usa sort, non itemnum (molto più solido)
            $DB->delete_records('mtcontabilita_attempt_items', [
                'attemptid'       => $attemptid,
                'mtcontabilitaid' => $mtcontabilitaid,
                'sort'            => $num,
                'bitem'           => 's',
            ]);
        }

        // ==========================
        // PRECALCOLO GLOBALE PER LA SCRITTURA (sort = $num)
        // punti totali e righe attese dal docente
        // + righe inserite dallo studente (anche se conto errato)
        // ==========================
        $sqlexpected = "SELECT id, score, itemdate
                        FROM {mtcontabilita_itemdet}
                        WHERE mtcontabilitaid = :mtid
                            AND sort            = :sort
                            AND type            = 0";
        $expectedrows = $DB->get_records_sql($sqlexpected, [
            'mtid' => $mtcontabilitaid,
            'sort' => $num,
        ]);

        $expectednall = count($expectedrows);
        $totalpointsall = 0.0;
        $solitemdateall = 0;
        foreach ($expectedrows as $er) {
            $totalpointsall += (float)$er->score;
            // prendi una data non-zero (la prima che trovi)
            if (!$solitemdateall && !empty($er->itemdate)) {
                $solitemdateall = (int)$er->itemdate;
            }
        }
        // ==========================
        // CALCOLO PUNTEGGIO DATA (UNA SOLA VOLTA)
        // ==========================
        $scoredat = 0.0;
        if ($pdata > 0 && $itemdate > 0 && $solitemdateall > 0 && $itemdate === $solitemdateall) {
            // La data vale il X% del punteggio massimo della scrittura (es. 3 * 10/100 = 0.3)
            $scoredat = ($totalpointsall * $pdata / $ptot);
        }
        $scoredat = round($scoredat, 4);

        // Conta quante "righe" ha inserito lo studente in questa scrittura.
        // Regola: se il conto è selezionato ma non c'è importo, vale comunque come 1 riga inserita.
        $studentnall = 0;
        foreach ($mastrini as $mx) {
            if (is_object($mx)) {
                $mx = (array)$mx;
            }
            if (!empty($mx['deleted'])) {
                continue;
            }
            // listid (come fai sotto)
            $lid = isset($mx['listid']) ? (int)$mx['listid'] : 0;
            if ($lid === 0) {
                $testo = '';
                if (isset($mx['testo'])) {
                    $testo = is_array($mx['testo']) ? (string)reset($mx['testo']) : (string)$mx['testo'];
                }
                $testo = trim($testo);
                if ($testo !== '') {
                    $k = function_exists('core_text::strtolower') ? core_text::strtolower($testo) : mb_strtolower($testo);
                    if (isset($mapconti[$k])) {
                        $lid = (int)$mapconti[$k];
                    }
                }
            }
            // Righe “non vuote”.
            $rig = $mx['righe'] ?? [];
            if (is_object($rig)) {
                $rig = (array)$rig;
            }
            $countrows = 0;
            foreach ($rig as $rr) {
                if (is_object($rr)) {
                    $rr = (array)$rr;
                }
                $d = isset($rr['d']) ? normalizza_valore($rr['d']) : '';
                $a = isset($rr['a']) ? normalizza_valore($rr['a']) : '';

                // Conta SOLO se c'è davvero un importo (dopo normalizzazione).
                if ($d !== '' || $a !== '') {
                    $countrows++;
                }
            }

            $studentnall += $countrows;

        }

        // rowpoints globale della scrittura
        $den = max($expectednall, $studentnall, 1);
        $rowpointsglobal = ($totalpointsall > 0) ? ($totalpointsall / $den) : 0.0;
        // 2) INSERIMENTO / CALCOLO
        $datagiven = false;
        foreach ($mastrini as $mkey => $mastrino) {
            // Supporta sia stdClass che array
            if (is_object($mastrino)) {
                $mastrino = (array)$mastrino;
            }
            // 🔴 Se marcato come cancellato dal client → non reinserire
            if (!empty($mastrino['deleted'])) {
                continue;
            }

            // 1) Leggi listid così come arriva dal POST
            $listid = isset($mastrino['listid']) ? (int)$mastrino['listid'] : 0;

            // 2) Se listid è vuoto ma abbiamo un "testo" (nome conto), prova a risolverlo lato server
            if ($listid === 0) {
                $testo = '';
                if (isset($mastrino['testo'])) {
                    // può essere stringa o array (in alcuni casi)
                    if (is_array($mastrino['testo'])) {
                        $testo = reset($mastrino['testo']);
                    } else {
                        $testo = $mastrino['testo'];
                    }
                }
                $testo = trim((string)$testo);

                if ($testo !== '') {
                    $key = function_exists('core_text::strtolower')
                        ? core_text::strtolower($testo)
                        : mb_strtolower($testo);
                    if (isset($mapconti[$key])) {
                        $listid = $mapconti[$key];
                    }
                }
            }

            // 3) Parentid come prima, ma usando il (nuovo) $listid
            $parentid = 0;
            if ($listid > 0) {
                $params = [
                    'attemptid'       => $attemptid,
                    'mtcontabilitaid' => $mtcontabilitaid,
                    'itemnum'         => 0,
                    'listid'          => $listid,
                    'bitem'           => 'i',
                ];
                $sql = "SELECT ai.id
                          FROM {mtcontabilita_attempt_items} ai
                          JOIN {mtcontabilita_itemdet} d ON ai.itemdetid = d.id
                         WHERE ai.attemptid       = :attemptid
                           AND ai.mtcontabilitaid = :mtcontabilitaid
                           AND d.itemnum          = :itemnum
                           AND d.listid           = :listid
                           AND ai.bitem           = :bitem";
                $parentid = $DB->get_field_sql($sql, $params, IGNORE_MULTIPLE);
            }

            $righe = $mastrino['righe'] ?? [];
            if (is_object($righe)) {
                $righe = (array)$righe;
            }

            // Pulisci righe e separa DARE / AVERE
            $righed = [];
            $righea = [];

            foreach ($righe as $rkey => $riga) {
                if (is_object($riga)) {
                    $riga = (array)$riga;
                }

                $d = isset($riga['d']) ? normalizza_valore($riga['d']) : '';
                $a = isset($riga['a']) ? normalizza_valore($riga['a']) : '';

                // Salta righe “vuote” o con solo flag 'x' (readonly)
                $emptyd = ($d === '' || $d === 'x');
                $emptya = ($a === '' || $a === 'x' || $a === null);
                // Se l’utente ha modificato un valore, non lo scartiamo mai
                if ($d === '' && $a === '') {
                    continue; // riga completamente vuota → skip
                }

                if ($emptyd && $emptya) {
                    continue;
                }

                $row = ['d' => $d, 'a' => $a];

                // Prima DARE, poi AVERE
                if (!$emptyd && $emptya) {
                    // solo DARE
                    $righed[] = $row;
                } else if ($emptyd && !$emptya) {
                    // solo AVERE
                    $righea[] = $row;
                } else {
                    // caso “strano” (sia d che a valorizzati) -> tienilo con i DARE
                    $righed[] = $row;
                }
            }

            // Unisci: prima tutte le DARE, poi tutte le AVERE
            $righepulite = array_merge($righed, $righea);

            // Se non ci sono righe, consenti comunque il salvataggio SOLO se c'è un conto selezionato.
            // (caso "ho messo solo il conto"). Score sarà 0.
            if (empty($righepulite)) {
                if ($listid > 0) {
                    $righepulite = []; // salvo mastrino vuoto
                } else {
                    continue; // niente conto e niente righe: non salvo
                }
            }

            // ==========================
            // CALCOLO DELLO SCORE (NUOVE REGOLE)
            // - score riga-per-riga
            // - se conto errato => righe tutte 0
            // - per-riga points = TOT/N se M<=N, altrimenti TOT/M
            // ==========================
            $score = 0.0;

            // 1) Recupera le righe SOLUZIONE per questo conto+scrittura (sort)
            $solrows = [];
            if ($listid > 0) {
                $sql = "SELECT id, itemdate, itemtype, amount, score, sortorder
                        FROM {mtcontabilita_itemdet}
                        WHERE mtcontabilitaid = :mtid
                        AND sort            = :sort
                        AND type            = 0
                        AND listid          = :listid
                    ORDER BY sortorder ASC, id ASC";
                $solrows = $DB->get_records_sql($sql, [
                    'mtid'   => $mtcontabilitaid,
                    'sort'   => $num,
                    'listid' => $listid,
                ]);
            }

            // 2) Se NON esistono soluzioni per quel conto -> conto errato => score 0 (tutto rosso, niente parziali)
            if (!$solrows) {
                $score = 0.0;
            } else {
                // N atteso e TOT punti del docente per quel mastrino (somma score delle righe soluzione)
                $expectedn = count($solrows);
                $totalpoints = 0.0;
                // $solitemdate = 0;

                foreach ($solrows as $s) {
                    $totalpoints += (float)$s->score;
                    // $solitemdate = (int)$s->itemdate; // di solito uguale per tutte
                }

                // Righe studente
                $studentrows = $righepulite;               // array di righe ['d'=>..,'a'=>..]
                $studentn = count($studentrows);

                // Se lo studente ha messo solo il conto (0 righe) => 0
                if ($studentn === 0) {
                    $score = 0.0;
                } else {
                    // Prepara soluzioni in un array "matchabile"
                    $solutions = [];
                    foreach ($solrows as $s) {
                        $solutions[] = [
                            'side'   => strtolower(trim($s->itemtype)), // 'd' o 'a'
                            'amount' => (float)$s->amount,
                            'used'   => false,
                        ];
                    }

                    // Funzione per estrarre lato/importo dalla riga studente
                    $parsestudent = function(array $r): array {
                        $d = isset($r['d']) && $r['d'] !== '' ? (float)$r['d'] : 0.0;
                        $a = isset($r['a']) && $r['a'] !== '' ? (float)$r['a'] : 0.0;

                        // lato determinabile solo se uno dei due è valorizzato
                        if ($d > 0.0 && $a == 0.0) {
                            return ['side' => 'd', 'amount' => $d, 'sideknown' => true, 'amountknown' => true];
                        }
                        if ($a > 0.0 && $d == 0.0) {
                            return ['side' => 'a', 'amount' => $a, 'sideknown' => true, 'amountknown' => true];
                        }

                        // Se sono entrambi 0 o entrambi valorizzati (caso strano) => non matcho bene
                        // (ma salvo lo stesso)
                        $side = '';
                        $amount = 0.0;

                        $sideknown = false;
                        $amountknown = false;

                        if ($d > 0.0 || $a > 0.0) {
                            // caso "strano": scegliamo D come default se c'è d, altrimenti A
                            if ($d > 0.0) {
                                $side = 'd';
                                $amount = $d;
                            } else {
                                $side = 'a';
                                $amount = $a;
                            }
                            $sideknown = true;
                            $amountknown = true;
                        }

                        return ['side' => $side, 'amount' => $amount, 'sideknown' => $sideknown, 'amountknown' => $amountknown];
                    };

                    // Funzione per calcolare pesopar della singola riga (SOLO se conto corretto)
                    $calcpesopar = function(string $sidestu, float $amountstu, bool $sideknown, bool $amountknown, array $sol, int $itemdateuser, int $solitemdate)
                        use ($pdata, $pconto, $pside, $pimporto): int {

                        $pesopar = 0;

                        // CONTO sempre (conto corretto perché siamo in questo ramo)
                        $pesopar += $pconto;

                        // DATA (vale solo se conto corretto)
                        // if ($itemdateuser > 0 && $solitemdate > 0 && $itemdateuser === $solitemdate) {
                        //     $pesopar += $pdata;
                        // }

                        // LATO
                        if ($sideknown && $sidestu !== '' && $sidestu === $sol['side']) {
                            $pesopar += $pside;
                        }

                        // IMPORTO
                        if ($amountknown && $sideknown && $sidestu === $sol['side']) {
                            if (abs($amountstu - (float)$sol['amount']) < 0.01) {
                                $pesopar += $pimporto;
                            }
                        }

                        return $pesopar;
                    };

                    // Matching greedy: per ogni riga studente, trova la migliore soluzione non usata
                    // (priorità: match side+amount, poi side-only, ecc.)
                    foreach ($studentrows as $r) {
                        $parsed = $parsestudent((array)$r);

                        $bestidx = -1;
                        $bestpesopar = -1;

                        for ($i = 0; $i < count($solutions); $i++) {
                            if ($solutions[$i]['used']) {
                                continue;
                            }

                            $pesopar = $calcpesopar(
                                $parsed['side'],
                                (float)$parsed['amount'],
                                (bool)$parsed['sideknown'],
                                (bool)$parsed['amountknown'],
                                $solutions[$i],
                                (int)$itemdate,
                                (int)$solitemdateall
                            );

                            // scegliamo il massimo pesopar
                            if ($pesopar > $bestpesopar) {
                                $bestpesopar = $pesopar;
                                $bestidx = $i;
                            }
                        }

                        if ($bestidx >= 0 && $bestpesopar > 0) {
                            // assegna la soluzione a questa riga
                            $solutions[$bestidx]['used'] = true;
                            $score += ($ptot > 0) ? ($rowpointsglobal * $bestpesopar / $ptot) : 0.0;
                        }
                    }

                    $score = round($score, 4);
                }

            }
            // Aggiungi il punteggio data SOLO al primo mastrino salvato (anche se il conto è errato).
            if (!$datagiven && $scoredat > 0) {
                $score = round($score + $scoredat, 4);
                $datagiven = true;
            }


            // ==========================
            // DEBUG: raccogli informazioni per questo mastrino
            // ==========================
            if ($debugscr) {
                $debugdata[] = [
                    'GLOBAL' => true,
                    'expectedN_all' => $expectednall,
                    'studentN_all' => $studentnall,
                    'den' => $den,
                    'totalPoints_all' => $totalpointsall,
                    'rowpoints_global' => $rowpointsglobal,
                    'itemdate_user' => $itemdate,
                    'sol_itemdate_all' => $solitemdateall,
                    'p_data' => $pdata,
                    'p_tot' => $ptot,

                    // 'mkey'          => $mkey,
                    // 'listid'        => $listid,
                    // 'testo_conto'   => isset($mastrino['testo']) ? $mastrino['testo'] : null,
                    // 'righe_raw'     => $mastrino['righe'] ?? [],
                    // 'righe_pulite'  => $righepulite,
                    // 'itemdate_user' => $itemdate,
                    // 'sol_rows'      => $solrows,
                    // 'p_data'        => $pdata,
                    // 'p_conto'       => $pconto,
                    // 'p_side'        => $pside,
                    // 'p_importo'     => $pimporto,
                    // 'p_tot'         => $ptot,
                    // 'score'         => $score,
                ];
            }

            // ==========================
            // INSERT del record tentativo (solo in modalità NON debug)
            // ==========================
            if ($transaction) {
                $record = (object)[
                    'attemptid'       => $attemptid,
                    'mtcontabilitaid' => $mtcontabilitaid,
                    'itemdetid'       => 0,
                    'scheme'          => '0',
                    'itype'           => null,
                    'daamount'        => json_encode($righepulite, JSON_UNESCAPED_UNICODE),
                    'da'              => '',
                    'macroclasse'     => '',
                    'itemnum'         => $itemnum,
                    'sort'            => $num,
                    'itemdate'        => $itemdate,
                    'listid'          => $listid,
                    'parentid'        => $parentid,
                    'bitem'           => 's',
                    'score'           => $score,
                    'scoremanual'     => 0.0,
                    'bcheck'          => $ischeck ? 1 : 0,
                    'timecreated'     => $adesso,
                    'timemodified'    => $adesso,
                ];

                $DB->insert_record('mtcontabilita_attempt_items', $record);
            }
        }

        if ($transaction) {
            $transaction->allow_commit();
        }

    } catch (Throwable $e) {
        if ($transaction) {
            $transaction->rollback($e);
        }
        throw $e; // comportamento normale in caso di errore reale
    }
    if ($debugscr && $ischeck) {
        echo '<h1>DEBUG SCRITTURE - saveattemptmastrino</h1>';
        echo '<p><strong>AttemptID: </strong>' . (int)$attemptid . '</p>';
        echo '<p><strong>Scrittura sort(num): </strong>' . (int)$num . '</p>';
        echo '<p><strong>Itemnum: </strong>' . s($itemnum) . '</p>';
        echo '<p><strong>mtcontabilitaid: </strong>' . (int)$mtcontabilitaid . '</p>';
        echo '<hr>';

        echo '<pre>';
        var_dump($debugdata);
        echo '</pre>';

        // Stop qui: niente redirect
        exit;
    }
    // Redirect coerente con attempt.php (includo anche tp e action per rientrare dove eri)
    $params = [
    'cmid'      => $cmid,
    'num'       => $num,
    'attemptid' => $attemptid,
    'action'    => $action,
    'tp'        => $tp,
    ];

    redirect(new moodle_url('/mod/mtcontabilita/player/attempt.php', $params));
}


/**
 * Converte stringhe numeriche localizzate "it-IT" a numero US.
 * Ritorna '' se 'x' o vuoto (righe readonly / situazione iniziale).
 */
/**
 * Normalizza un numero scritto con separatori locali in formato "US" (punto decimale, niente migliaia).
 * Esempi:
 *  - "1.234,56" -> "1234.56"
 *  - "1,234.56" -> "1234.56"
 *  - "1,000"    -> "1000"
 *  - "1.000"    -> "1000"   (se non ci sono decimali, lo tratta come migliaia)
 * Ritorna '' se 'x' o vuoto (readonly / SI).
 */
function normalizza_valore($valore) {
    $valore = trim((string)$valore);

    if ($valore === '' || $valore === 'x') {
        return '';
    }

    // Togli spazi (anche non-breaking) e separatori "strani" usati talvolta come migliaia.
    $valore = str_replace(["\xc2\xa0", ' ', "\t", "\n", "\r", "'"], '', $valore);

    $hasComma = (strpos($valore, ',') !== false);
    $hasDot   = (strpos($valore, '.') !== false);

    // Se ci sono sia ',' che '.', il separatore decimale è quello che compare per ultimo.
    if ($hasComma && $hasDot) {
        $lastComma = strrpos($valore, ',');
        $lastDot   = strrpos($valore, '.');

        $decsep  = ($lastComma > $lastDot) ? ',' : '.';
        $thousep = ($decsep === ',') ? '.' : ',';

        $valore = str_replace($thousep, '', $valore);
        if ($decsep === ',') {
            $valore = str_replace(',', '.', $valore);
        }
    } else if ($hasComma) {
        // Solo virgola: può essere migliaia (EN: 1,000) o decimali (IT: 10,5)
        // Se finisce con ,dd (1-2 cifre) la trattiamo come decimale, altrimenti migliaia.
        if (preg_match('/,\d{1,2}$/', $valore)) {
            $valore = str_replace(',', '.', $valore);
        } else {
            $valore = str_replace(',', '', $valore);
        }
    } else if ($hasDot) {
        // Solo punto: può essere decimale (EN: 10.5) o migliaia (IT: 1.000)
        // Se finisce con .dd (1-2 cifre) lo trattiamo come decimale, altrimenti migliaia.
        if (!preg_match('/\.\d{1,2}$/', $valore)) {
            $valore = str_replace('.', '', $valore);
        }
    }

    // Mantieni solo numeri, segno e punto decimale.
    // (Se ti serve anche il negativo va bene così.)
    if (!preg_match('/^-?\d+(\.\d+)?$/', $valore)) {
        return '';
    }

    return $valore;
}

/**
 * Ordina i mastrini in modo che:
 *  - prima vengano quelli con almeno una riga SOLO DARE
 *  - poi quelli "misti" (sia D che A, o nessuna riga significativa)
 *  - infine quelli con SOLO AVERE
 *
 * Lavora sui dati così come arrivano dal POST (prima della normalizzazione numerica).
 */
function mtc_ordina_mastrini_dare_avere(array $mastrini): array {
    // Normalizza struttura (array indicizzato)
    $mastrini = array_values($mastrini);

    $getkey = function($mastrino) {
        if (is_object($mastrino)) {
            $mastrino = (array)$mastrino;
        }
        $righe = $mastrino['righe'] ?? [];
        if (is_object($righe)) {
            $righe = (array)$righe;
        }
        if (empty($righe)) {
            return 1; // misto / indefinito
        }

        $hasd = false;
        $hasa = false;

        foreach ($righe as $riga) {
            if (is_object($riga)) {
                $riga = (array)$riga;
            }

            $draw = isset($riga['d']) ? trim((string)$riga['d']) : '';
            $araw = isset($riga['a']) ? trim((string)$riga['a']) : '';

            // Salta le celle "x" (readonly/situazione iniziale)
            if ($draw === 'x') {
                $draw = '';
            }
            if ($araw === 'x') {
                $araw = '';
            }

            $dnonempty = ($draw !== '');
            $anonempty = ($araw !== '');

            if ($dnonempty) {
                $hasd = true;
            }
            if ($anonempty) {
                $hasa = true;
            }
        }

        if ($hasd && !$hasa) {
            return 0; // solo DARE
        } else if (!$hasd && $hasa) {
            return 2; // solo AVERE
        } else {
            return 1; // misto/indefinito
        }
    };

    usort($mastrini, function($a, $b) use ($getkey) {
        $ka = $getkey($a);
        $kb = $getkey($b);
        if ($ka === $kb) {
            return 0;
        }
        return ($ka < $kb) ? -1 : 1;
    });

    return $mastrini;
}
