// Questo file fa parte di MyTown.
// MyTown è software libero: puoi ridistribuirlo e/o modificarlo secondo
// i termini della GNU Affero General Public License come pubblicata dalla
// Free Software Foundation, versione 3.
// Distribuito senza ALCUNA GARANZIA; vedi la GNU AGPL v3 per i dettagli.
// Copyright (C) Kondor Consulting & Services S.r.l.
// SPDX-License-Identifier: AGPL-3.0-or-later

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using iTextSharp.text.pdf;
using iTextSharp.text.pdf.parser;
using Newtonsoft.Json.Linq;

namespace KondorCS.ScheduleLibrary.AIFileProcessor
{
    /// <summary>
    /// Handler dedicato per PDF di Certificato di Pagamento.
    /// Regex calibrate sui template reali dei CertPag (Figline e Incisa Valdarno),
    /// tipici di committenti pubblici italiani che usano la modulistica standard.
    /// Se l'estrazione locale ha successo, evita la chiamata all'AI esterna.
    /// </summary>
    public class CertPagPdfHandler
    {
        private readonly bool _debug;

        public CertPagPdfHandler(bool debug = false)
        {
            _debug = debug;
        }

        /// <summary>
        /// Riconosce un PDF Certificato di Pagamento. NB: deve essere chiamato PRIMA
        /// di SalPdfHandler perché un CertPag contiene "SAL" come riferimento testuale.
        /// </summary>
        public bool IsCertPagPdf(FileInfo file)
        {
            if (file == null || !file.Exists) return false;
            if (!file.Extension.Equals(".pdf", StringComparison.OrdinalIgnoreCase)) return false;

            string fileName = file.Name.ToLower();
            bool nameMatch =
                fileName.Contains("certificato di pagamento") ||
                fileName.Contains("certificato_pagamento") ||
                fileName.Contains("cert_pagamento") ||
                fileName.Contains("cdp") ||
                fileName.Contains("cert_pag");

            try
            {
                string text = ExtractPdfText(file.FullName);
                if (string.IsNullOrWhiteSpace(text) || text.Length < 50) return nameMatch;

                // Riconoscimento del template ufficiale CertPag:
                //   - presenza di "Certificato di Pagamento" (o variante con typo "Ceritificato"
                //     che compare nei template ufficiali)
                //   - sezione "Pagamenti Autorizzati" o "IMPORTI CONTABILIZZATI"
                //   - "SI AUTORIZZA IL PAGAMENTO" (frase tipica della pagina firma)
                bool contentMatch =
                    Regex.IsMatch(text, @"\bCe?ri?tificato\s+di\s+Pagamento\b", RegexOptions.IgnoreCase) ||
                    Regex.IsMatch(text, @"\bSI\s+AUTORIZZA\s+IL\s+PAGAMENTO\b", RegexOptions.IgnoreCase) ||
                    Regex.IsMatch(text, @"\bIMPORTI\s+CONTABILIZZATI\b", RegexOptions.IgnoreCase);

                return nameMatch || contentMatch;
            }
            catch
            {
                return nameMatch;
            }
        }

        /// <summary>
        /// Estrae i dati dal PDF CertPag e popola fu_ai_response sui campi vuoti.
        /// Preserva i valori già compilati (idempotente).
        /// </summary>
        public string ExtractAndConvertToJson(FileInfo file, string existingJsonFields)
        {
            if (_debug) Console.WriteLine($"[CERTPAG] Inizio estrazione: {file.Name}");
            string text = ExtractPdfText(file.FullName);

            Dictionary<string, string> extracted = ExtractCertPagFields(text);

            if (_debug)
            {
                Console.WriteLine($"[CERTPAG] Campi estratti: {extracted.Count}");
                foreach (var kv in extracted) Console.WriteLine($"[CERTPAG]   {kv.Key} = {kv.Value}");
            }

            JObject jsonObj = string.IsNullOrWhiteSpace(existingJsonFields)
                ? new JObject { ["fields"] = new JArray() }
                : JObject.Parse(existingJsonFields);
            if (jsonObj["fields"] == null) jsonObj["fields"] = new JArray();

            JArray fieldsArray = (JArray)jsonObj["fields"];
            foreach (JObject f in fieldsArray.ToList())
            {
                string name = f["name"]?.ToString();
                string current = f["value"]?.ToString();
                if (string.IsNullOrWhiteSpace(name)) continue;
                if (!string.IsNullOrWhiteSpace(current)) continue;
                if (extracted.TryGetValue(name, out string v) && !string.IsNullOrWhiteSpace(v))
                {
                    f["value"] = v;
                }
            }

            jsonObj["extracted_by"] = "CertPagPdfHandler";
            jsonObj["extraction_timestamp"] = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
            return jsonObj.ToString();
        }

        /// <summary>
        /// Estrazione campi dal testo del PDF. Regex calibrate sui CertPag reali.
        /// Le chiavi corrispondono ai "name" dei campi nel ft_form di "Certificato di Pagamento".
        /// </summary>
        private Dictionary<string, string> ExtractCertPagFields(string text)
        {
            var d = new Dictionary<string, string>();
            if (string.IsNullOrWhiteSpace(text)) return d;

            // === CIG ===
            // Pattern 1 (preferito): "CIG. MASTER N. <code>"
            var mCigMaster = Regex.Match(text, @"CIG\.?\s+MASTER\s+N\.?\s*([0-9A-Z]{10})\b", RegexOptions.IgnoreCase);
            if (mCigMaster.Success) d["cig"] = mCigMaster.Groups[1].Value.ToUpperInvariant();
            else
            {
                // Fallback: "CIG N. <code>" o "CIG: <code>" o "CIG <code>"
                var mCig = Regex.Match(text, @"\bCIG\.?\s*(?:N\.?\s*)?[:.\-]?\s*([0-9A-Z]{10})\b", RegexOptions.IgnoreCase);
                if (mCig.Success) d["cig"] = mCig.Groups[1].Value.ToUpperInvariant();
            }

            // === CUP === (15 caratteri alfanumerici standard italiano)
            var mCup = Regex.Match(text, @"\bCUP\s*[:.\-]?\s*([0-9A-Z]{15})\b", RegexOptions.IgnoreCase);
            if (mCup.Success) d["cup"] = mCup.Groups[1].Value.ToUpperInvariant();

            // === Numero CertPag ===
            // Template italiano: "Ceritificato di Pagamento" (typo nel modulo ufficiale) seguito
            // da un numero su righe successive. Catturo il primo numero dopo l'ancora.
            var mNCert = Regex.Match(text,
                @"Ce?ri?tificato\s+di\s+Pagamento[^\d]{0,80}?(\d{1,3})\b",
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (mNCert.Success) d["numero_certpag"] = mNCert.Groups[1].Value;

            // === Numero SAL ===
            // ATTENZIONE: la tabella "Pagamenti Autorizzati" del template Figline
            //   "1 ACC 18/10/2023  775.890,85"
            //   "2 SAL01 31/05/2024  600.947,75"
            // riporta CODICI dei pagamenti precedenti (es. "SAL01" è il codice della
            // riga, NON il numero del SAL di riferimento del CertPag corrente).
            // Per convenzione: SAL di riferimento = Numero CertPag (X = X).
            // Il fallback è gestito lato SP cert_payment_save_from_ai_upload (V16):
            // se numero_sal è vuoto, usa numero_certpag per il match.
            // Estraggo numero_sal SOLO se trovo un'ancora narrativa esplicita
            // (es. "relativo al SAL N.X" — non presente nei template Figline ma in altri sì).
            var mSalNarrative = Regex.Match(text,
                @"(?:relativo|in\s+riferimento|riferito)\s+al\s+SAL\s+N\.?\s*([0-9]+\s*(?:bis|tris|quater)?)",
                RegexOptions.IgnoreCase);
            if (mSalNarrative.Success)
            {
                d["numero_sal"] = mSalNarrative.Groups[1].Value.Replace(" ", "");
            }
            // altrimenti lascio vuoto → la SP fa fallback su numero_certpag

            // === Importo === (rata autorizzata al pagamento — è l'importo netto del CertPag)
            var mImp = Regex.Match(text,
                @"SI\s+AUTORIZZA\s+IL\s+PAGAMENTO\s+DELLA\s+RATA\s+DI[:\s€]*([0-9]{1,3}(?:[.\s'][0-9]{3})*,[0-9]{2})",
                RegexOptions.IgnoreCase);
            if (mImp.Success) d["importo"] = mImp.Groups[1].Value.Replace(" ", "");

            // === Data === (ultima data nel documento — tipicamente la data emissione/firma)
            // Pattern italiano dd/MM/yyyy o dd/MM/yy
            var dateMatches = Regex.Matches(text, @"\b([0-3]?\d/[01]?\d/(?:\d{4}|\d{2}))\b");
            if (dateMatches.Count > 0)
            {
                string lastDate = dateMatches[dateMatches.Count - 1].Groups[1].Value;
                d["data"] = NormalizeDateIt(lastDate);
            }

            // === Tipologia ===
            // I CertPag standard sono "S.A.L." (= tipologia 1 CERTIFICATO).
            // Casi particolari: "svincolo ritenute" → 3 SVINCOLO; "acconto" → 4 ACCONTO; "nota" → 2 NOTA.
            string upper = text.ToUpperInvariant();
            if (Regex.IsMatch(upper, @"\bNOTA\s+DI\s+CREDITO\b") || Regex.IsMatch(upper, @"\bNOTA\s+CREDITO\b"))
                d["tipologia"] = "2";
            else if (Regex.IsMatch(upper, @"\bSVINCOLO\s+RITENUTE\b"))
                d["tipologia"] = "3";
            else if (Regex.IsMatch(upper, @"\bS\.?A\.?L\.?\s*\(\s*ACCONTO\s*\)\s*[\r\n]\s*Ce?ri"))
                d["tipologia"] = "4";  // raro: checkbox S.A.L. acconto attivo
            else
                d["tipologia"] = "1";  // default: CERTIFICATO standard

            // === Firma RUP ===
            // Convenzione pattern UNILAV: combo value="present" se trovo riferimento testuale.
            bool hasRup =
                Regex.IsMatch(text, @"\bFirma\s+del\s+RUP\b", RegexOptions.IgnoreCase) ||
                Regex.IsMatch(text, @"\bIl\s+R\.?\s*U\.?\s*P\.?\b", RegexOptions.IgnoreCase) ||
                Regex.IsMatch(text, @"\bResponsabile\s+(?:Unico\s+)?del\s+Procedimento\b", RegexOptions.IgnoreCase);
            if (hasRup) d["firma_rup"] = "present";

            // === Breakdown CdP per pre-compilazione dettaglio Fattura Attiva ===
            // Tre voci progressive a tutto il CdP, usate per ripartire proporzionalmente
            // la rata netta nelle righe del dettaglio fattura.
            //
            // PriMus standard (es. CdP Figline):
            //   "Opere contrattuali (appalto iniziale) €. 1.332.270,50"
            //   "Opere integrative (extra/NP) €. 377.241,98"       → sommato a contrattuali
            //   "Opere in economia €. 17.400,93"
            //   (oneri sicurezza NON separati: restano NULL → fattura senza riga sicurezza)
            //
            // BdI custom (es. CdP Villa Huffer):
            //   "Importo lavori soggetti a ribasso € 1.271.129,96"
            //   "Ribasso contrattuale € -458.585,56"
            //   "Prestazioni specialistiche € 0,00"
            //   "Importo economie € 42.081,58"
            //   "Ribasso su economie € -3.180,36"
            //   "Oneri relativi alla sicurezza € 112.947,77"
            //   → contrattuali = lavori_ribasso + ribasso + prestazioni  (NETTO)
            //   → economia     = imp_economie + ribasso_economie         (NETTO)
            //   → sicurezza    = oneri_sicurezza
            ExtractBreakdownCdp(text, d);

            // Rimuovi chiavi con valore vuoto
            foreach (var k in d.Where(kv => string.IsNullOrWhiteSpace(kv.Value)).Select(kv => kv.Key).ToList())
                d.Remove(k);

            return d;
        }

        /// <summary>
        /// Estrae il breakdown del CdP (opere_contrattuali, opere_economia, oneri_sicurezza).
        /// Riconosce sia il template PriMus che il template BdI/Villa Huffer.
        /// </summary>
        private void ExtractBreakdownCdp(string text, Dictionary<string, string> d)
        {
            // Pattern PriMus: "<voce> €. <numero>" oppure "<voce> € <numero>"
            // I numeri possono usare ' o ´ o . come separatore migliaia (formato italiano)
            const string NUM = @"([+\-]?\s*[0-9]{1,3}(?:[.\s'´][0-9]{3})*,\s?[0-9]{2})";

            // === PriMus: opere contrattuali + integrative (sommate) ===
            decimal? oc = null;
            var mOcPrimus = Regex.Match(text,
                @"Opere\s+contrattuali[^€]*€\.?\s*" + NUM,
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (mOcPrimus.Success)
            {
                oc = ParseItNumber(mOcPrimus.Groups[1].Value);
            }
            var mOiPrimus = Regex.Match(text,
                @"Opere\s+integrative[^€]*€\.?\s*" + NUM,
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (mOiPrimus.Success)
            {
                var oi = ParseItNumber(mOiPrimus.Groups[1].Value);
                if (oi.HasValue) oc = (oc ?? 0m) + oi.Value;
            }

            // === PriMus: opere in economia ===
            decimal? oe = null;
            var mOePrimus = Regex.Match(text,
                @"Opere\s+in\s+economia[^€]*€\.?\s*" + NUM,
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (mOePrimus.Success)
            {
                oe = ParseItNumber(mOePrimus.Groups[1].Value);
            }

            // === BdI: se nessun risultato PriMus, prova il layout BdI ===
            decimal? os = null;
            if (!oc.HasValue && !oe.HasValue)
            {
                // Lavori soggetti a ribasso + ribasso + prestazioni specialistiche → contrattuali NETTO
                var mLavori = Regex.Match(text,
                    @"Importo\s+lavori\s+soggetti\s+a\s+ribasso[^€]*€\s*" + NUM,
                    RegexOptions.IgnoreCase | RegexOptions.Singleline);
                var mRibasso = Regex.Match(text,
                    @"Ribasso\s+contrattuale[^€]*€\s*" + NUM,
                    RegexOptions.IgnoreCase | RegexOptions.Singleline);
                var mPrest = Regex.Match(text,
                    @"Prestazioni\s+specialistiche[^€]*€\s*" + NUM,
                    RegexOptions.IgnoreCase | RegexOptions.Singleline);
                if (mLavori.Success)
                {
                    decimal acc = ParseItNumber(mLavori.Groups[1].Value) ?? 0m;
                    if (mRibasso.Success) acc += ParseItNumber(mRibasso.Groups[1].Value) ?? 0m;  // già negativo
                    if (mPrest.Success)   acc += ParseItNumber(mPrest.Groups[1].Value) ?? 0m;
                    oc = acc;
                }

                // Importo economie + ribasso su economie → economia NETTO
                var mEcon = Regex.Match(text,
                    @"Importo\s+economie[^€]*€\s*" + NUM,
                    RegexOptions.IgnoreCase | RegexOptions.Singleline);
                var mRibEcon = Regex.Match(text,
                    @"Ribasso\s+su\s+economie[^€]*€\s*" + NUM,
                    RegexOptions.IgnoreCase | RegexOptions.Singleline);
                if (mEcon.Success)
                {
                    decimal acc = ParseItNumber(mEcon.Groups[1].Value) ?? 0m;
                    if (mRibEcon.Success) acc += ParseItNumber(mRibEcon.Groups[1].Value) ?? 0m;
                    oe = acc;
                }
            }

            // === Oneri sicurezza === (esposto separatamente solo in BdI / alcuni template)
            // Uso ancoraggio inizio riga (Multiline) per evitare il match con la frase
            // narrativa "...per oneri della sicurezza" che precede la tabella in BdI.
            var mSic = Regex.Match(text,
                @"^\s*Oneri\s+(?:relativi\s+alla\s+|della\s+)?sicurezza[^€\n]{0,30}€\.?\s*" + NUM,
                RegexOptions.IgnoreCase | RegexOptions.Multiline);
            if (mSic.Success)
            {
                os = ParseItNumber(mSic.Groups[1].Value);
            }

            // === Lavori soggetti a ribasso === (BdI)
            decimal? lsr = null;
            var mLsr = Regex.Match(text,
                @"Importo\s+lavori\s+soggetti\s+a\s+ribasso[^€]{0,20}€\s*" + NUM,
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (mLsr.Success) lsr = ParseItNumber(mLsr.Groups[1].Value);

            // === Ribasso contrattuale === (BdI: negativo)
            decimal? rib = null;
            var mRib = Regex.Match(text,
                @"Ribasso\s+contrattuale[^€]{0,20}€\s*" + NUM,
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (mRib.Success) rib = ParseItNumber(mRib.Groups[1].Value);

            // === TOTALE PRODUZIONE (A) ===
            // PriMus: "TOTALE PRODUZIONE €. 1.349.671,43"
            // BdI: "Per lavori e somministrazioni € 1.426.159,31"
            // BdI alt: "SOMMANO € 964.393,40" (netto)
            decimal? tp = null;
            var mTpPrimus = Regex.Match(text,
                @"TOTALE\s+PRODUZIONE[^€]{0,30}€\.?\s*" + NUM,
                RegexOptions.IgnoreCase);
            if (mTpPrimus.Success) tp = ParseItNumber(mTpPrimus.Groups[1].Value);
            else
            {
                var mTpBdi = Regex.Match(text,
                    @"Per\s+lavori\s+e\s+somministrazioni[^€]{0,20}€\s*" + NUM,
                    RegexOptions.IgnoreCase);
                if (mTpBdi.Success) tp = ParseItNumber(mTpBdi.Groups[1].Value);
            }

            // === PARZIALE PRESENTE CERTIFICATO / Importo lavori CdP corrente ===
            // È IL valore che entra in fattura come riga POSITIVA del CdP corrente:
            //   - PriMus: "PARZIALE PRESENTE CERTIFICATO" (es. Figline 589.745,88)
            //   - BdI con SAL n>1: "Maggiore importo (90% differenza)" (es. VH CdP 2 = 293.808,53)
            //   - BdI primo CdP: "Importo netto per lavori e somministrazioni SAL N da prezzi
            //     contrattuali" (es. VH CdP 1 = 964.393,40)
            decimal? pc = null;
            var mPc = Regex.Match(text,
                @"PARZIALE\s+PRESENTE\s+CERTIFICATO[^€]{0,50}€\.?\s*" + NUM,
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (mPc.Success) pc = ParseItNumber(mPc.Groups[1].Value);
            else
            {
                // BdI: "Maggiore importo (90% differenza)" — il delta del CdP corrente
                var mMagg = Regex.Match(text,
                    @"Maggiore\s+importo[^€]{0,50}€\s*" + NUM,
                    RegexOptions.IgnoreCase | RegexOptions.Singleline);
                if (mMagg.Success) pc = ParseItNumber(mMagg.Groups[1].Value);
                else
                {
                    // BdI primo CdP: "Importo netto per lavori e somministrazioni SAL N da prezzi contrattuali"
                    var mSal = Regex.Match(text,
                        @"Importo\s+netto\s+per\s+lavori\s+e\s+somministrazioni\s+SAL\s+\d+[^€]{0,80}€\s*" + NUM,
                        RegexOptions.IgnoreCase | RegexOptions.Singleline);
                    if (mSal.Success) pc = ParseItNumber(mSal.Groups[1].Value);
                }
            }

            // === (B) Ritenute totali ===
            // PriMus: "TOTALE RITENUTE CONTRATTUALI" — spesso il valore è sulla riga successiva.
            // BdI: "TOTALE DEDUZIONI € 4.821,97"
            decimal? rt = null;
            var mRtPrimus = Regex.Match(text,
                @"TOTALE\s+RITENUTE\s+CONTRATTUALI[^€]{0,80}€\.?\s*" + NUM,
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (mRtPrimus.Success) rt = ParseItNumber(mRtPrimus.Groups[1].Value);
            else
            {
                var mRtBdi = Regex.Match(text,
                    @"TOTALE\s+DEDUZIONI[^€]{0,30}€\s*" + NUM,
                    RegexOptions.IgnoreCase);
                if (mRtBdi.Success) rt = ParseItNumber(mRtBdi.Groups[1].Value);
            }

            // === Ritenute correnti (del SOLO CdP corrente) ===
            // BdI: "La ritenuta dello 0,5% per infortuni € 1.469,04" (es. VH CdP 2)
            // BdI alt: la riga negli "Da cui detraendo" che precede "TOTALE DEDUZIONI"
            // PriMus: somma di "Ritenute a garanzia" + "Ritenute infortuni" (per ora prendo solo infortuni)
            decimal? rc = null;
            var mRc = Regex.Match(text,
                @"(?:La\s+)?ritenuta(?:\s+(?:dello|del)\s+0[,.]5%)?\s+(?:per\s+)?infortuni[^€]{0,30}€\.?\s*" + NUM,
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (mRc.Success) rc = ParseItNumber(mRc.Groups[1].Value);
            else
            {
                // Fallback BdI: "TOTALE DEDUZIONI" come ritenute correnti (se l'unica deduzione è la ritenuta)
                var mTd = Regex.Match(text,
                    @"TOTALE\s+DEDUZIONI[^€]{0,30}€\s*" + NUM,
                    RegexOptions.IgnoreCase);
                if (mTd.Success) rc = ParseItNumber(mTd.Groups[1].Value);
            }

            // === (C) Recupero anticipazione corrente ===
            // PriMus: "Quota recup.ant./ne %  34,79%  €. 269.934,29" (quota corrente)
            // BdI: "Per recupero anticipazione € 0,00"
            decimal? ra = null;
            var mRaPrimus = Regex.Match(text,
                @"Quota\s+recup\.?\s*ant\.?[^€]{0,50}€\.?\s*" + NUM,
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (mRaPrimus.Success) ra = ParseItNumber(mRaPrimus.Groups[1].Value);
            else
            {
                var mRaBdi = Regex.Match(text,
                    @"Per\s+recupero\s+anticipazione[^€]{0,30}€\s*" + NUM,
                    RegexOptions.IgnoreCase);
                if (mRaBdi.Success) ra = ParseItNumber(mRaBdi.Groups[1].Value);
            }

            // === (D) Pagamenti precedentemente autorizzati ===
            // PriMus: "PAGAMENTI PRECEDENTEMENTE AUTORIZZATI" + somma colonna pagamenti
            //   Spesso esposto come "TOTALE € <somma>" della tabella pagamenti a sinistra.
            // BdI: "Ammontare certificati precedenti € 0,00"
            decimal? pp = null;
            var mPpPrimus = Regex.Match(text,
                @"PAGAMENTI\s+PRECEDENTEMENTE\s+AUTORIZZATI[^€]{0,80}€\.?\s*" + NUM,
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (mPpPrimus.Success) pp = ParseItNumber(mPpPrimus.Groups[1].Value);
            else
            {
                var mPpBdi = Regex.Match(text,
                    @"Ammontare\s+certificati\s+precedenti[^€]{0,30}€\s*" + NUM,
                    RegexOptions.IgnoreCase);
                if (mPpBdi.Success) pp = ParseItNumber(mPpBdi.Groups[1].Value);
            }

            // Scrivi nel dizionario in formato italiano (per parse_it_numeric della SP)
            if (oc.HasValue)  d["opere_contrattuali"]      = FormatItNumber(oc.Value);
            if (oe.HasValue)  d["opere_economia"]          = FormatItNumber(oe.Value);
            if (os.HasValue)  d["oneri_sicurezza"]         = FormatItNumber(os.Value);
            if (lsr.HasValue) d["lavori_soggetti_ribasso"] = FormatItNumber(lsr.Value);
            if (rib.HasValue) d["ribasso_contrattuale"]    = FormatItNumber(rib.Value);
            if (tp.HasValue)  d["totale_produzione"]       = FormatItNumber(tp.Value);
            if (pc.HasValue)  d["parziale_certificato"]    = FormatItNumber(pc.Value);
            if (rt.HasValue)  d["ritenute_totali"]         = FormatItNumber(rt.Value);
            if (rc.HasValue)  d["ritenute_correnti"]       = FormatItNumber(rc.Value);
            if (ra.HasValue)  d["recupero_anticipazione"]  = FormatItNumber(ra.Value);
            if (pp.HasValue)  d["pagamenti_precedenti"]    = FormatItNumber(pp.Value);
        }

        /// <summary>Converte numero formato italiano (1´234,56 / 1.234,56 / 1 234,56) → decimal.</summary>
        private decimal? ParseItNumber(string s)
        {
            if (string.IsNullOrWhiteSpace(s)) return null;
            string cleaned = s.Replace("´", "").Replace("'", "").Replace(".", "").Replace(" ", "").Replace(",", ".");
            decimal result;
            if (decimal.TryParse(cleaned, System.Globalization.NumberStyles.Any,
                                 System.Globalization.CultureInfo.InvariantCulture, out result))
                return result;
            return null;
        }

        /// <summary>Formatta decimal → "1.234,56" (formato italiano, parsabile da parse_it_numeric).</summary>
        private string FormatItNumber(decimal v)
        {
            return v.ToString("N2", System.Globalization.CultureInfo.GetCultureInfo("it-IT"));
        }

        /// <summary>Normalizza date italiane dd/MM/yyyy → yyyy-MM-dd (per type=2 nel ft_form).</summary>
        private string NormalizeDateIt(string value)
        {
            if (string.IsNullOrWhiteSpace(value)) return value;
            var m = Regex.Match(value.Trim(), @"^([0-9]{1,2})[/\-\.]([0-9]{1,2})[/\-\.]([0-9]{2,4})$");
            if (!m.Success) return value;
            string day = m.Groups[1].Value.PadLeft(2, '0');
            string month = m.Groups[2].Value.PadLeft(2, '0');
            string year = m.Groups[3].Value;
            if (year.Length == 2) year = "20" + year;
            return $"{year}-{month}-{day}";
        }

        private string ExtractPdfText(string pdfPath)
        {
            var sb = new StringBuilder();
            using (var reader = new PdfReader(pdfPath))
            {
                for (int p = 1; p <= reader.NumberOfPages; p++)
                {
                    var strategy = new SimpleTextExtractionStrategy();
                    sb.AppendLine(PdfTextExtractor.GetTextFromPage(reader, p, strategy));
                }
            }
            return sb.ToString();
        }
    }
}
