// 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 iTextSharp.text;
using iTextSharp.text.pdf;
using KondorCS.Interface;
using KondorCS.ScheduleServices.Interface;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace KondorCS.ScheduleLibrary.AIFileProcessor
{
    /// <summary>
    /// Scheduler per l'elaborazione di file con chiamate AI
    /// Equivalente del metodo Java processFileWithAICall con pattern ILibraryProcess
    /// </summary>
    public class AIFileProcessor : ILibraryProcess
    {
        public string Description
        {
            get { return "Schedule elaborazione file con AI"; }
        }

        public string Name
        {
            get { return "KondorCS.ScheduleLibrary.AIFileProcessor AIFileProcessor"; }
        }

        public event CurrentOperation OnCurrentOperation;

        private bool mStopPorcess = false;
        public bool StopProcess
        {
            get { return mStopPorcess; }
            set { mStopPorcess = value; }
        }

        private bool mDebug = false;
        private System.Timers.Timer mTimer;
        private bool mIsWork = false;
        private int mActiveTasksCount = 0;
        private readonly object mActiveTasksLock = new object();
        private const int MAX_PARALLEL_TASKS = 10;
        private DBController mConn;
        private string mAiUrl;
        private int mPt_id;
        private int mUser_id;
        private bool mEnableStartThread;
        private MediaDownloadService mMediaDownloadService;
        private bool mUseRemoteFiles = false;

        public KCSResponse Execute(KCSRequest request, Guid InstanceProcess)
        {
            KCSResponse response = null;
            try
            {
                // Parametro debug
                if (request.Params["debug"] != null)
                    mDebug = Convert.ToBoolean(request.Params["debug"]);

                // Connection string database
                string ConnDB = request.Params["ConnDB"].ToString();

                // URL servizio AI (parametro obbligatorio)
                if (!request.Params.ContainsKey("ai_url"))
                {
                    response = new KCSResponse(-1);
                    response.CommandResponse = "KO";
                    new KCSException("Parametro 'ai_url' mancante", "KondorCS.ScheduleLibrary.AIFileProcessor.AIFileProcessor");
                    return response;
                }
                mAiUrl = request.Params["ai_url"].ToString();

                // Parametri opzionali pt_id e user_id
                mPt_id = 0;
                if (request.Params.ContainsKey("pt_id"))
                    mPt_id = Convert.ToInt32(request.Params["pt_id"]);

                mUser_id = 0;
                if (request.Params.ContainsKey("user_id"))
                    mUser_id = Convert.ToInt32(request.Params["user_id"]);

                // Parametro per gestire la partenza del thread
                mEnableStartThread = true;
                if (request.Params.ContainsKey("enableStartThread"))
                {
                    mEnableStartThread = Convert.ToBoolean(request.Params["enableStartThread"]);
                }

                // Parametro opzionale "local" - Se impostato a "true" forza l'uso del file locale
                // ignorando eventuali configurazioni FTP
                bool forceLocal = false;
                if (request.Params.ContainsKey("local"))
                {
                    forceLocal = request.Params["local"].ToString().Equals("true", StringComparison.OrdinalIgnoreCase);
                    if (forceLocal && mDebug)
                        Console.WriteLine("[AI] Parametro 'local=true' rilevato: forzato accesso diretto ai file locali");
                }

                // Parametri opzionali per il download remoto dei file tramite FTP
                // Se presenti, i file verranno scaricati tramite FTP invece di accesso diretto
                // NOTA: Se il parametro "local" è "true", l'FTP viene ignorato
                if (!forceLocal &&
                    request.Params.ContainsKey("ftp_host") &&
                    request.Params.ContainsKey("ftp_username") &&
                    request.Params.ContainsKey("ftp_password") &&
                    request.Params.ContainsKey("ftp_temp_folder"))
                {
                    mUseRemoteFiles = true;
                    mMediaDownloadService = new MediaDownloadService(mDebug);

                    string ftpHost = request.Params["ftp_host"].ToString();
                    string ftpUsername = request.Params["ftp_username"].ToString();
                    string ftpPassword = request.Params["ftp_password"].ToString();
                    string tempFolder = request.Params["ftp_temp_folder"].ToString();

                    // Parametro opzionale: base path da rimuovere dal path completo
                    // Utile quando l'home directory FTP corrisponde a un path base
                    // Es: home FTP = /var/www/html/assets, path DB = /var/www/html/assets/temp/file.pdf
                    string ftpBasePath = null;
                    if (request.Params.ContainsKey("ftp_base_path"))
                    {
                        ftpBasePath = request.Params["ftp_base_path"].ToString();
                    }

                    mMediaDownloadService.Initialize(ftpHost, ftpUsername, ftpPassword, tempFolder, ftpBasePath);

                    if (mDebug)
                        Console.WriteLine("[AI] Modalità download remoto FTP abilitata");
                }
                else
                {
                    if (mDebug)
                        Console.WriteLine("[AI] Modalità accesso diretto ai file (locale)");
                }

                // Data stop (default: domani -1 minuto)
                DateTime DataStop = DateTime.Now;
                DataStop = DataStop.AddDays(1);
                DataStop = DataStop.AddMinutes(-1);

                if (mDebug)
                    Console.WriteLine(string.Format("[AI] InitParam OK. DataStop: {0}", DataStop));
                if (OnCurrentOperation != null)
                {
                    ScheduleCurrentOperation curop = new ScheduleCurrentOperation(
                        string.Format("DataStop: {0}", DataStop),
                        0,
                        InstanceProcess,
                        "InitParam",
                        "<data></data>");
                    OnCurrentOperation(curop);
                }

                // Inizializza connessione database
                mConn = new DBController(ConnDB);

                // Avvia timer ogni 5 secondi
                mTimer = new System.Timers.Timer(5000);
                mTimer.Elapsed += async (sender, e) => await HandleTimerAsync(InstanceProcess, DataStop);
                mTimer.Start();

                response = new KCSResponse(0);
                response.CommandResponse = "OK";
            }
            catch (Exception ex)
            {
                response = new KCSResponse(-1, ex);
                response.CommandResponse = "KO";

                if (mDebug)
                    Console.WriteLine(string.Format("[AI ERROR] Message:{0} - StackTrace:{1}", ex.Message, ex.StackTrace));
                if (OnCurrentOperation != null)
                {
                    ScheduleCurrentOperation curop = new ScheduleCurrentOperation(
                        ex.Message,
                        -1,
                        InstanceProcess,
                        "Execute Error",
                        string.Format("<data><stacktrace>{0}</stackTrace></data>", ex.StackTrace));
                    OnCurrentOperation(curop);
                }

                new KCSException(string.Format("Message: {0}. StackTrace: {1}", ex.Message, ex.StackTrace),
                    "KondorCS.ScheduleLibrary.AIFileProcessor.AIFileProcessor");
            }
            return response;
        }

        /// <summary>
        /// Gestisce il timer per elaborare i file in coda
        /// </summary>
        private Task HandleTimerAsync(Guid InstanceProcess, DateTime DataStop)
        {
            try
            {
                // Verifica se è ora di fermare il processo
                if (DateTime.Now >= DataStop)
                {
                    mIsWork = true;
                    mTimer.Stop();
                    mTimer.Dispose();

                    if (mDebug)
                        Console.WriteLine(string.Format("[AI] Exit HandleTimerAsync: {0}", DateTime.Now));
                    if (OnCurrentOperation != null)
                    {
                        ScheduleCurrentOperation curop = new ScheduleCurrentOperation(
                            string.Format("Exit HandleTimerAsync: {0}", DateTime.Now),
                            0,
                            InstanceProcess,
                            "EndProcess",
                            "<data></data>");
                        OnCurrentOperation(curop);
                    }

                    return Task.FromResult<object>(null);
                }

                // Controlla continuamente se ci sono slot disponibili per elaborare nuovi file
                if (mEnableStartThread)
                {
                    // Verifica quanti task sono attivi
                    int activeTasks;
                    lock (mActiveTasksLock)
                    {
                        activeTasks = mActiveTasksCount;
                    }

                    if (mDebug && !mIsWork)
                        Console.WriteLine($"[AI] Interval Called {DateTime.Now:HH:mm:ss} - Task attivi: {activeTasks}/{MAX_PARALLEL_TASKS}");

                    // Pulizia periodica dei file temporanei vecchi (ogni 24 ore)
                    if (!mIsWork && mUseRemoteFiles && mMediaDownloadService != null)
                    {
                        mMediaDownloadService.CleanupOldTempFiles(24);
                    }

                    // Se ci sono slot disponibili, recupera nuovi file
                    if (activeTasks < MAX_PARALLEL_TASKS)
                    {
                        // Recupera i file da elaborare (status = 0 = in attesa)
                        // Limita il numero di file da recuperare agli slot disponibili
                        int availableSlots = MAX_PARALLEL_TASKS - activeTasks;
                        List<FileUpload> lstFiles = mConn.GetFilesToProcess();

                        if (lstFiles.Count > 0)
                        {
                            // Prendi solo i file che possono essere elaborati in parallelo
                            int filesToProcess = Math.Min(lstFiles.Count, availableSlots);
                            var filesToProcessNow = lstFiles.Take(filesToProcess).ToList();

                            if (mDebug)
                                Console.WriteLine($"[AI] Trovati {lstFiles.Count} file in coda, avvio elaborazione di {filesToProcessNow.Count} file");

                            if (OnCurrentOperation != null)
                            {
                                ScheduleCurrentOperation curop = new ScheduleCurrentOperation(
                                    $"File in coda: {lstFiles.Count}, Avvio: {filesToProcessNow.Count}",
                                    0,
                                    InstanceProcess,
                                    "GetFilesToProcess",
                                    "<data></data>");
                                OnCurrentOperation(curop);
                            }

                            // Avvia l'elaborazione di ogni file in modo asincrono
                            foreach (var file in filesToProcessNow)
                            {
                                // Incrementa il contatore dei task attivi
                                lock (mActiveTasksLock)
                                {
                                    mActiveTasksCount++;
                                }

                                // Avvia il task in background senza aspettare
                                Task.Run(async () =>
                                {
                                    try
                                    {
                                        if (mDebug)
                                            Console.WriteLine($"[AI] Inizio elaborazione file fu_id={file.Fu_id}");

                                        // Elabora il file con AI
                                        await Task.Run(() => ProcessFileWithAICall(mPt_id, mUser_id, file.Fu_id));

                                        if (mDebug)
                                            Console.WriteLine($"[AI] File fu_id={file.Fu_id} elaborato con successo");
                                    }
                                    catch (Exception exFile)
                                    {
                                        Console.WriteLine($"[AI ERROR] Errore elaborazione fu_id={file.Fu_id}: {exFile.Message}");

                                        if (OnCurrentOperation != null)
                                        {
                                            ScheduleCurrentOperation curop = new ScheduleCurrentOperation(
                                                exFile.Message,
                                                -1,
                                                InstanceProcess,
                                                "ProcessFile Error",
                                                $"<data><fu_id>{file.Fu_id}</fu_id><stacktrace>{exFile.StackTrace}</stackTrace></data>");
                                            OnCurrentOperation(curop);
                                        }
                                    }
                                    finally
                                    {
                                        // Decrementa il contatore dei task attivi
                                        lock (mActiveTasksLock)
                                        {
                                            mActiveTasksCount--;
                                            if (mDebug)
                                                Console.WriteLine($"[AI] Task completato. Task attivi rimanenti: {mActiveTasksCount}");
                                        }
                                    }
                                });
                            }
                        }
                    }

                    mIsWork = true; // Flag per indicare che il timer è stato eseguito almeno una volta
                }
            }
            catch (Exception ex)
            {
                if (mDebug)
                    Console.WriteLine(string.Format("[AI ERROR] HandleTimerAsync! Message:{0} - StackTrace:{1}",
                        ex.Message, ex.StackTrace));
                if (OnCurrentOperation != null)
                {
                    ScheduleCurrentOperation curop = new ScheduleCurrentOperation(
                        ex.Message,
                        -1,
                        InstanceProcess,
                        "HandleTimerAsync Error",
                        string.Format("<data><stacktrace>{0}</stackTrace></data>", ex.StackTrace));
                    OnCurrentOperation(curop);
                }
            }
            return Task.FromResult<object>(null);
        }

        /// <summary>
        /// Elabora un singolo file con chiamata AI
        /// Equivalente di: processFileWithAICall(int pt_id, int user_id, int fu_id)
        /// </summary>
        private TypeFile ProcessFileWithAICall(int pt_id, int user_id, int fu_id)
        {
            FileUpload file = null;
            TypeFile typeFile = null;
            FileInfo physicalFile = null;
            bool isTemporaryFile = false;

            try
            {
                // Recupera il file dal database
                file = mConn.GetFileUploadById(fu_id);
                if (file == null)
                {
                    mConn.UpdateScheduleProcessDetailStatus(fu_id, "errore", "File non trovato nel database");
                    throw new ArgumentException("File non trovato");
                }

                // Recupera il TypeFile
                typeFile = mConn.GetTypeFileByFuId(pt_id, user_id, fu_id);

                // Determina i campi JSON da usare
                string jsonFields = file.Fu_ai_response;
                if (string.IsNullOrEmpty(jsonFields))
                    jsonFields = typeFile.Ft_form;

                // Ottieni il file fisico (download FTP o accesso locale).
                // Se il file non c'è (cancellato, ripristino non andato a buon fine, etc.)
                // marchiamo il job come "errore" e usciamo SENZA throw: l'eccezione
                // bloccherebbe solo questo singolo task ma sporcherebbe i log; il loop
                // esterno seleziona già solo i job con sp_status_detail='in attesa...'
                // quindi una volta marcato "errore" il file non viene più ripreso.
                if (mUseRemoteFiles)
                {
                    // Scarica il file dal server FTP
                    Console.WriteLine($"[AI] Download FTP del file fu_id={fu_id} da: {file.Fu_path}");
                    try
                    {
                        physicalFile = mMediaDownloadService.DownloadFile(file.Fu_path, fu_id);
                    }
                    catch (Exception exDl)
                    {
                        Console.WriteLine($"[AI WARN] Download FTP fallito per fu_id={fu_id} ({file.Fu_path}): {exDl.Message}. Skip e prosegui col prossimo file.");
                        mConn.UpdateScheduleProcessDetailStatus(fu_id, "errore", $"Download FTP fallito: {exDl.Message}");
                        return null;
                    }
                    isTemporaryFile = true;
                }
                else
                {
                    // Accesso diretto al file locale
                    physicalFile = new FileInfo(file.Fu_path);
                    if (!physicalFile.Exists)
                    {
                        Console.WriteLine($"[AI WARN] File fisico non trovato per fu_id={fu_id} ({file.Fu_path}). Skip e prosegui col prossimo file.");
                        mConn.UpdateScheduleProcessDetailStatus(fu_id, "errore", $"File fisico non trovato: {file.Fu_path}");
                        return null;
                    }
                }

                Console.WriteLine($"[AI] Inizio elaborazione file fu_id={fu_id}");

                // Imposta stato "in elaborazione" prima dell'elaborazione
                mConn.UpdateScheduleProcessDetailStatus(fu_id, "in elaborazione");

                string responseString;

                // === Routing per NOME FILE TYPE (ft_name) ===
                // NESSUN auto-riconoscimento del contenuto: si instrada SOLO per nome del
                // file_type. E' piu' preciso del gruppo (fg) perche' una stessa cartella
                // puo' contenere ft di tipologie diverse (es. "SAL Principale" + "Allegato
                // SAL": solo il principale va estratto come SAL). I nomi ft sono identici
                // tra collaudo e produzione (gli id no).
                //   ft "Certificato di Pagamento" -> estrazione locale CertPag
                //   ft "SAL Principale"            -> estrazione locale SAL
                //   ogni altro ft (es. "Contratto Appalto", "Allegato SAL") -> AI / generico
                const string FT_CERTPAG = "Certificato di Pagamento";
                const string FT_SAL = "SAL Principale";
                string ftName = (typeFile?.Ft_name ?? "").Trim();

                CertPagPdfHandler certPagHandler = new CertPagPdfHandler(mDebug);
                SalPdfHandler salHandler = new SalPdfHandler(mDebug);

                if (string.Equals(ftName, FT_CERTPAG, StringComparison.OrdinalIgnoreCase))
                {
                    Console.WriteLine($"[AI] File type '{ftName}', estrazione locale CertPag");
                    try
                    {
                        responseString = certPagHandler.ExtractAndConvertToJson(physicalFile, jsonFields);
                        Console.WriteLine($"[AI] Estrazione CertPag completata per fu_id={fu_id} (nessun costo AI)");
                        responseString = FillMissingFieldsViaAiFirstPage(responseString, physicalFile, typeFile.Ft_id, new[] { "cig", "cup" });
                    }
                    catch (Exception exCp)
                    {
                        Console.WriteLine($"[AI] Errore estrazione CertPag: {exCp.Message}. Fallback su AI...");
                        responseString = CallAIServiceWithPageFallback(physicalFile, jsonFields, typeFile.Ft_id);
                    }
                }
                else if (string.Equals(ftName, FT_SAL, StringComparison.OrdinalIgnoreCase))
                {
                    Console.WriteLine($"[AI] File type '{ftName}', estrazione locale SAL");
                    try
                    {
                        responseString = salHandler.ExtractAndConvertToJson(physicalFile, jsonFields);
                        Console.WriteLine($"[AI] Estrazione SAL completata per fu_id={fu_id} (nessun costo AI)");
                        responseString = FillMissingFieldsViaAiFirstPage(responseString, physicalFile, typeFile.Ft_id, new[] { "cig", "cup" });
                    }
                    catch (Exception exSal)
                    {
                        Console.WriteLine($"[AI] Errore estrazione SAL: {exSal.Message}. Fallback su AI...");
                        responseString = CallAIServiceWithPageFallback(physicalFile, jsonFields, typeFile.Ft_id);
                    }
                }
                else
                {
                    // Verifica se è un PDF generico con testo processabile
                    GenericPdfHandler genericPdfHandler = new GenericPdfHandler(mDebug);
                    if (genericPdfHandler.IsProcessablePdf(physicalFile))
                    {
                        Console.WriteLine($"[AI] File riconosciuto come PDF generico con testo, tento estrazione con matching");

                        try
                        {
                            // Tenta l'estrazione e il matching dei campi
                            PdfExtractionResult extractionResult = genericPdfHandler.TryExtractAndMatch(physicalFile, jsonFields);

                            if (extractionResult.IsFullMatch)
                            {
                                // Match completo: usa solo i dati estratti senza chiamare l'AI
                                Console.WriteLine($"[AI] Match completo ({extractionResult.MatchPercentage:F1}%): {extractionResult.MatchedFieldsCount}/{extractionResult.TotalFieldsCount} campi trovati");
                                Console.WriteLine($"[AI] Uso dati estratti senza chiamare AI (nessun costo AI)");
                                responseString = extractionResult.ExtractedData.ToString();
                            }
                            else if (extractionResult.AllRequiredFieldsFound)
                            {
                                // Tutti i campi obbligatori presenti: usa dati estratti senza AI (anche se mancano campi opzionali)
                                Console.WriteLine($"[AI] Tutti i campi obbligatori presenti ({extractionResult.MatchPercentage:F1}%): {extractionResult.MatchedFieldsCount}/{extractionResult.TotalFieldsCount} campi trovati");
                                if (extractionResult.MissingFields != null && extractionResult.MissingFields.Count > 0)
                                {
                                    Console.WriteLine($"[AI] Campi opzionali mancanti: {string.Join(", ", extractionResult.MissingFields)}");
                                }
                                Console.WriteLine($"[AI] Uso dati estratti senza chiamare AI (nessun costo AI)");
                                responseString = extractionResult.ExtractedData.ToString();
                            }
                            else if (extractionResult.IsPartialMatch)
                            {
                                // Match parziale con campi obbligatori mancanti: l'estrazione non è affidabile, cancella i dati estratti e usa solo l'AI
                                Console.WriteLine($"[AI] Match parziale ({extractionResult.MatchPercentage:F1}%): {extractionResult.MatchedFieldsCount}/{extractionResult.TotalFieldsCount} campi trovati");
                                Console.WriteLine($"[AI] Campi mancanti: {string.Join(", ", extractionResult.MissingFields)}");
                                if (extractionResult.MissingRequiredFields != null && extractionResult.MissingRequiredFields.Count > 0)
                                {
                                    Console.WriteLine($"[AI] Campi obbligatori mancanti: {string.Join(", ", extractionResult.MissingRequiredFields)}");
                                }
                                Console.WriteLine($"[AI] Match incompleto (mancano campi obbligatori), cancello i dati estratti e chiamo AI per tutti i campi");

                                // Chiama l'AI per TUTTI i campi (non solo quelli mancanti)
                                // I valori estratti parzialmente vengono scartati perché potrebbero essere inaffidabili
                                responseString = CallAIServiceWithPageFallback(physicalFile, jsonFields, typeFile.Ft_id);
                                Console.WriteLine($"[AI] Uso solo la risposta AI (dati estratti scartati)");
                            }
                            else
                            {
                                // Nessun match: delega completamente all'AI
                                Console.WriteLine($"[AI] Nessun match trovato (0%), delego completamente all'AI");
                                responseString = CallAIServiceWithPageFallback(physicalFile, jsonFields, typeFile.Ft_id);
                            }
                        }
                        catch (Exception exGenericPdf)
                        {
                            Console.WriteLine($"[AI] Errore estrazione PDF generico: {exGenericPdf.Message}. Fallback su AI...");
                            // Se l'estrazione generica fallisce, usa l'AI come fallback
                            responseString = CallAIServiceWithPageFallback(physicalFile, jsonFields, typeFile.Ft_id);
                        }
                    }
                    else
                    {
                        // File non processabile come PDF: usa la chiamata AI normale
                        Console.WriteLine($"[AI] File non riconosciuto come PDF processabile, uso chiamata AI");
                        responseString = CallAIServiceWithPageFallback(physicalFile, jsonFields, typeFile.Ft_id);
                    }
                }

                // Verifica che la risposta non sia un errore
                if (responseString.Contains("\"status\":\"failure\""))
                {
                    Console.WriteLine($"[AI ERROR] AI ha restituito failure per fu_id={fu_id}");
                    mConn.UpdateScheduleProcessDetailStatus(fu_id, "errore", "AI ha restituito status failure");
                    throw new Exception($"L'AI ha restituito un errore: {responseString}");
                }

                // Merge intelligente tra il JSON esistente e quello dell'AI
                // I valori già presenti nel DB non vengono sovrascritti
                string mergedJson = MergeAIResponseWithExisting(file.Fu_ai_response, responseString);

                // Aggiorna il database con la risposta merged
                file.Fu_ai_response = mergedJson;
                mConn.UpdateFileForm(file);

                // Imposta stato "elaborato" su schedule_process
                mConn.UpdateScheduleProcessDetailStatus(fu_id, "elaborato");
                Console.WriteLine($"[AI] Elaborazione completata per fu_id={fu_id}");

                // Pulisci il file temporaneo se necessario
                if (isTemporaryFile && mMediaDownloadService != null)
                {
                    mMediaDownloadService.DeleteTempFile(physicalFile);
                }

                // Ricarica il TypeFile aggiornato
                return mConn.GetTypeFileByFuId(pt_id, user_id, fu_id);
            }
            catch (IOException ioEx)
            {
                // Errore I/O (download, lettura file): logga WARN, marca il job come errore
                // nel DB (così non viene ripreso al prossimo poll) e termina senza
                // propagare l'eccezione, in modo che il loop esterno non debba gestirla.
                Console.WriteLine($"[AI WARN] Errore I/O per fu_id={fu_id}: {ioEx.Message}");
                mConn.UpdateScheduleProcessDetailStatus(fu_id, "errore", $"Errore I/O: {ioEx.Message}");

                // Pulisci il file temporaneo anche in caso di errore
                if (isTemporaryFile && mMediaDownloadService != null && physicalFile != null)
                {
                    mMediaDownloadService.DeleteTempFile(physicalFile);
                }
                return null;
            }
            catch (Exception ex)
            {
                // Errore generico (es. AI service ha risposto 500 con "Result does not match
                // the required structure"): non bloccante. Logga, marca il job come errore
                // nel DB e termina senza propagare. Lo stack trace è scritto solo se utile
                // alla diagnosi (debug mode).
                Console.WriteLine($"[AI WARN] Elaborazione AI fallita per fu_id={fu_id}: {ex.Message}");
                if (mDebug && ex.StackTrace != null) Console.WriteLine(ex.StackTrace);
                mConn.UpdateScheduleProcessDetailStatus(fu_id, "errore", ex.Message);

                // Pulisci il file temporaneo anche in caso di errore
                if (isTemporaryFile && mMediaDownloadService != null && physicalFile != null)
                {
                    mMediaDownloadService.DeleteTempFile(physicalFile);
                }
                return null;
            }
        }

        /// <summary>
        /// Effettua la chiamata al servizio AI con timeout estesi
        /// Equivalente della logica HttpClient Java con multipart/form-data
        /// </summary>
        private string CallAIService(FileInfo physicalFile, string jsonFields, int documentoTipo)
        {
            // Configura timeout estesi per chiamate AI lunghe (5 minuti)
            using (HttpClient httpClient = new HttpClient())
            {
                httpClient.Timeout = TimeSpan.FromMinutes(5);

                try
                {
                    // Legge il file in byte array
                    byte[] imageBytes = File.ReadAllBytes(physicalFile.FullName);

                    // Determina il ContentType in base all'estensione
                    string fileName = physicalFile.Name.ToLower();
                    string mediaType = "image/jpeg";

                    if (fileName.EndsWith(".png"))
                        mediaType = "image/png";
                    else if (fileName.EndsWith(".pdf"))
                        mediaType = "application/pdf";
                    else if (fileName.EndsWith(".jpg") || fileName.EndsWith(".jpeg"))
                        mediaType = "image/jpeg";

                    // Costruisce la richiesta multipart
                    using (var content = new MultipartFormDataContent())
                    {
                        // Aggiunge il campo JSON
                        var jsonContent = new StringContent(jsonFields, Encoding.UTF8, "application/json");
                        content.Add(jsonContent, "json");

                        // Aggiunge il file immagine/documento
                        var fileContent = new ByteArrayContent(imageBytes);
                        fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mediaType);
                        content.Add(fileContent, "image", physicalFile.Name);

                        // Aggiunge il tipo documento
                        var tipoContent = new StringContent(documentoTipo.ToString(), Encoding.UTF8, "text/plain");
                        content.Add(tipoContent, "documento_tipo");

                        Console.WriteLine($"[AI] Chiamata a AI service per file: {physicalFile.Name}");

                        // Esegue la richiesta POST
                        HttpResponseMessage response = httpClient.PostAsync(mAiUrl, content).Result;

                       if (response.IsSuccessStatusCode)
                        {
                            string responseString = response.Content.ReadAsStringAsync().Result;
                            Console.WriteLine($"[AI] Risposta ricevuta per file: {physicalFile.Name}");
                            return responseString;
                        }
                        else
                        {
                            string errorContent = response.Content.ReadAsStringAsync().Result;
                            throw new Exception($"Errore nella chiamata AI: {response.StatusCode} - {errorContent}");
                        }
                    }
                }
                catch (AggregateException ae)
                {
                    Exception innerException = ae.InnerException ?? ae;
                    throw new IOException($"Errore nella comunicazione con AI service: {innerException.Message}", innerException);
                }
                catch (HttpRequestException httpEx)
                {
                    throw new IOException($"Errore HTTP nella chiamata AI: {httpEx.Message}", httpEx);
                }
            }
        }

        /// <summary>
        /// Wrapper di CallAIService con strategia di riduzione progressiva per PDF lunghi.
        /// Logica suggerita: le info essenziali di un SAL/contratto stanno in apertura e
        /// nei totali finali, quindi proviamo prima un subset minimo (prima+ultima) e
        /// allarghiamo solo se serve. Per non-PDF o PDF già piccoli si chiama direttamente
        /// CallAIService sul file originale.
        ///
        /// Strategie tentate in ordine, fino al primo successo:
        ///   1. solo pagina 1 + ultima                (se #pagine > 4)
        ///   2. prime 3 + ultime 3                    (se #pagine > 6)
        ///   3. PDF completo                          (ultima risorsa)
        ///
        /// Se CallAIService fallisce su una strategia (context_length_exceeded o altro)
        /// passa alla successiva. L'eccezione dell'ultimo tentativo viene rilanciata.
        /// </summary>
        private string CallAIServiceWithPageFallback(FileInfo physicalFile, string jsonFields, int documentoTipo)
        {
            bool isPdf = physicalFile.Extension != null && physicalFile.Extension.ToLower() == ".pdf";
            if (!isPdf)
            {
                return CallAIService(physicalFile, jsonFields, documentoTipo);
            }

            int pageCount;
            try { pageCount = GetPdfPageCount(physicalFile); }
            catch (Exception exPc)
            {
                Console.WriteLine($"[AI] Impossibile contare le pagine del PDF ({exPc.Message}), invio file intero.");
                return CallAIService(physicalFile, jsonFields, documentoTipo);
            }

            if (pageCount <= 4)
            {
                return CallAIService(physicalFile, jsonFields, documentoTipo);
            }

            // Costruisce le strategie progressive (skip quelle ridondanti rispetto al pageCount)
            var strategies = new List<int[]>();
            strategies.Add(new[] { 1, pageCount });
            if (pageCount >= 6)
            {
                strategies.Add(BuildFirstNLastN(pageCount, 3, 3));
            }

            Exception lastEx = null;
            for (int i = 0; i < strategies.Count; i++)
            {
                int[] pages = strategies[i];
                FileInfo subset = null;
                try
                {
                    subset = ExtractPagesToTempPdf(physicalFile, pages);
                    Console.WriteLine($"[AI] Tentativo riduzione PDF (strategia {i + 1}/{strategies.Count + 1}): pagine [{string.Join(",", pages)}] su {pageCount}");
                    return CallAIService(subset, jsonFields, documentoTipo);
                }
                catch (Exception exTry)
                {
                    lastEx = exTry;
                    Console.WriteLine($"[AI] Riduzione strategia {i + 1} fallita: {exTry.Message}. Provo strategia successiva.");
                }
                finally
                {
                    if (subset != null)
                    {
                        try { subset.Delete(); } catch { /* ignore */ }
                    }
                }
            }

            // Ultima risorsa: PDF completo. Se anche questo fallisce, propaga l'eccezione
            // (il catch in ProcessFileWithAICall lo trasformerà in "errore" sul job).
            try
            {
                Console.WriteLine($"[AI] Strategie ridotte esaurite, tento PDF completo ({pageCount} pagine).");
                return CallAIService(physicalFile, jsonFields, documentoTipo);
            }
            catch (Exception exFull)
            {
                throw lastEx != null
                    ? new Exception($"Tutti i tentativi AI falliti. Ultimo: {exFull.Message}", exFull)
                    : exFull;
            }
        }

        /// <summary>
        /// Dopo un'estrazione LOCALE, se alcuni campi chiave (es. CIG/CUP, necessari per
        /// l'aggancio automatico del cantiere) sono ancora vuoti, chiede all'AI di leggerli
        /// inviando SOLO la prima pagina (dove stanno quelle informazioni): veloce ed economico.
        /// I valori AI riempiono solo i campi rimasti vuoti (l'estrazione locale ha priorità).
        /// </summary>
        private string FillMissingFieldsViaAiFirstPage(string localJson, FileInfo physicalFile,
                                                       int ftId, string[] requiredKeys)
        {
            try
            {
                var local = JObject.Parse(localJson);
                var fields = local["fields"] as JArray;
                if (fields == null) return localJson;

                bool missing = false;
                foreach (var k in requiredKeys)
                {
                    var f = fields.OfType<JObject>().FirstOrDefault(
                        x => string.Equals(x["name"]?.ToString(), k, StringComparison.OrdinalIgnoreCase));
                    if (f == null || string.IsNullOrWhiteSpace(f["value"]?.ToString())) { missing = true; break; }
                }
                // Trigger anche se il committente (combo option_search ~ "committenti") è vuoto:
                // l'estrattore locale legge CIG/CUP ma NON il committente, quindi senza questo
                // controllo il CdP verrebbe salvato senza committente e la fattura non lo erediterebbe.
                if (!missing)
                {
                    var commit = fields.OfType<JObject>().FirstOrDefault(
                        x => (x["option_search"]?.ToString() ?? "").IndexOf("committ", StringComparison.OrdinalIgnoreCase) >= 0);
                    if (commit != null && string.IsNullOrWhiteSpace(commit["value"]?.ToString())) missing = true;
                }
                if (!missing) return localJson;

                Console.WriteLine($"[AI] Campi chiave mancanti dopo estrazione locale ({string.Join(",", requiredKeys)}): chiedo all'AI solo la 1a pagina.");

                FileInfo firstPage = null;
                try
                {
                    firstPage = ExtractPagesToTempPdf(physicalFile, new[] { 1 });
                    string aiJson = CallAIService(firstPage, localJson, ftId);
                    var aiFields = JObject.Parse(aiJson)["fields"] as JArray;
                    if (aiFields != null)
                    {
                        foreach (JObject lf in fields.OfType<JObject>())
                        {
                            string name = lf["name"]?.ToString();
                            if (string.IsNullOrWhiteSpace(name)) continue;
                            if (!string.IsNullOrWhiteSpace(lf["value"]?.ToString())) continue; // locale ha priorità
                            var af = aiFields.OfType<JObject>().FirstOrDefault(
                                x => string.Equals(x["name"]?.ToString(), name, StringComparison.OrdinalIgnoreCase));
                            string av = af?["value"]?.ToString();
                            if (!string.IsNullOrWhiteSpace(av)) lf["value"] = av;
                        }
                    }
                    Console.WriteLine($"[AI] Fallback 1a pagina completato: campi chiave integrati.");
                    return local.ToString();
                }
                finally
                {
                    if (firstPage != null) { try { firstPage.Delete(); } catch { /* ignore */ } }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[AI] Fallback 1a pagina fallito: {ex.Message}. Uso solo l'estrazione locale.");
                return localJson;
            }
        }

        /// <summary>Ritorna il numero di pagine di un PDF usando iTextSharp.</summary>
        private int GetPdfPageCount(FileInfo pdf)
        {
            using (var reader = new PdfReader(pdf.FullName))
            {
                return reader.NumberOfPages;
            }
        }

        /// <summary>
        /// Costruisce un array ordinato e univoco di numeri di pagina prendendo le prime N
        /// e le ultime N di un PDF di totalPages pagine. Se le finestre si sovrappongono,
        /// le pagine non si duplicano.
        /// </summary>
        private int[] BuildFirstNLastN(int totalPages, int firstN, int lastN)
        {
            var set = new SortedSet<int>();
            for (int i = 1; i <= Math.Min(firstN, totalPages); i++) set.Add(i);
            for (int i = Math.Max(1, totalPages - lastN + 1); i <= totalPages; i++) set.Add(i);
            return set.ToArray();
        }

        /// <summary>
        /// Estrae le pagine indicate da source in un nuovo PDF temporaneo e ritorna il FileInfo.
        /// I numeri di pagina sono 1-based. Pagine invalide (fuori range) vengono ignorate.
        /// Caller responsabile della delete del file temporaneo.
        /// </summary>
        private FileInfo ExtractPagesToTempPdf(FileInfo source, int[] pageNumbers)
        {
            string outPath = Path.Combine(
                Path.GetDirectoryName(source.FullName) ?? Path.GetTempPath(),
                Path.GetFileNameWithoutExtension(source.Name) + "_pages_" + Guid.NewGuid().ToString("N").Substring(0, 8) + ".pdf");

            using (var reader = new PdfReader(source.FullName))
            using (var fs = new FileStream(outPath, FileMode.Create, FileAccess.Write))
            {
                var doc = new Document();
                var copy = new PdfCopy(doc, fs);
                doc.Open();
                int total = reader.NumberOfPages;
                foreach (var pn in pageNumbers)
                {
                    if (pn >= 1 && pn <= total)
                    {
                        var imported = copy.GetImportedPage(reader, pn);
                        copy.AddPage(imported);
                    }
                }
                doc.Close();
            }

            return new FileInfo(outPath);
        }

        /// <summary>
        /// Merge intelligente tra il JSON esistente nel database e quello restituito dall'AI.
        /// I valori già presenti nel DB non vengono sovrascritti, solo quelli vuoti vengono aggiornati con i dati dell'AI.
        /// </summary>
        /// <param name="existingJson">JSON esistente nel database (fu_ai_response)</param>
        /// <param name="aiResponseJson">JSON restituito dall'AI</param>
        /// <returns>JSON merged</returns>
        private string MergeAIResponseWithExisting(string existingJson, string aiResponseJson)
        {
            try
            {
                // Se non c'è JSON esistente, restituisce direttamente la risposta dell'AI
                if (string.IsNullOrWhiteSpace(existingJson))
                    return aiResponseJson;

                // Parse dei JSON
                JObject existingObj = JObject.Parse(existingJson);
                JObject aiResponseObj = JObject.Parse(aiResponseJson);

                // Verifica che entrambi abbiano la struttura con "fields"
                if (existingObj["fields"] == null || aiResponseObj["fields"] == null)
                    return aiResponseJson;

                JArray existingFields = (JArray)existingObj["fields"];
                JArray aiFields = (JArray)aiResponseObj["fields"];

                // Crea un dizionario dei campi esistenti per accesso rapido
                Dictionary<string, JObject> existingFieldsMap = new Dictionary<string, JObject>();
                foreach (JObject field in existingFields)
                {
                    string name = field["name"]?.ToString();
                    if (!string.IsNullOrEmpty(name))
                        existingFieldsMap[name] = field;
                }

                // Aggiorna i campi dell'AI solo se il valore nel DB è vuoto
                foreach (JObject aiField in aiFields)
                {
                    string fieldName = aiField["name"]?.ToString();
                    if (string.IsNullOrEmpty(fieldName))
                        continue;

                    // Se il campo esiste già nel DB
                    if (existingFieldsMap.ContainsKey(fieldName))
                    {
                        JObject existingField = existingFieldsMap[fieldName];
                        string existingValue = existingField["value"]?.ToString();

                        // Se il valore esistente è vuoto o null, aggiorna con quello dell'AI
                        if (string.IsNullOrWhiteSpace(existingValue))
                        {
                            string aiValue = aiField["value"]?.ToString();
                            if (!string.IsNullOrWhiteSpace(aiValue))
                            {
                                existingField["value"] = aiField["value"];
                                Console.WriteLine($"[AI] Campo '{fieldName}' aggiornato con valore dall'AI: {aiValue}");
                            }
                        }
                        else
                        {
                            Console.WriteLine($"[AI] Campo '{fieldName}' già valorizzato nel DB, mantengo il valore esistente: {existingValue}");
                        }
                    }
                    // Se il campo non esiste nel DB, lo aggiungiamo
                    else
                    {
                        existingFields.Add(aiField);
                        Console.WriteLine($"[AI] Campo '{fieldName}' aggiunto dall'AI");
                    }
                }

                // Restituisce il JSON merged
                return existingObj.ToString();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[AI ERROR] Errore nel merge dei JSON: {ex.Message}. Uso la risposta dell'AI.");
                // In caso di errore nel parsing, restituisce la risposta dell'AI
                return aiResponseJson;
            }
        }

        /// <summary>
        /// Crea un JSON contenente solo i campi mancanti da passare all'AI.
        /// </summary>
        /// <param name="originalJsonFields">JSON originale con tutti i campi</param>
        /// <param name="missingFieldNames">Lista dei nomi dei campi mancanti</param>
        /// <returns>JSON con solo i campi mancanti</returns>
        private string CreateJsonForMissingFields(string originalJsonFields, List<string> missingFieldNames)
        {
            try
            {
                if (missingFieldNames == null || missingFieldNames.Count == 0)
                    return originalJsonFields;

                JObject originalObj = JObject.Parse(originalJsonFields);
                JArray originalFields = originalObj["fields"] as JArray;

                if (originalFields == null)
                    return originalJsonFields;

                // Filtra solo i campi mancanti
                JArray missingFields = new JArray();
                foreach (JObject field in originalFields)
                {
                    string fieldName = field["name"]?.ToString();
                    if (!string.IsNullOrEmpty(fieldName) && missingFieldNames.Contains(fieldName))
                    {
                        missingFields.Add(field);
                    }
                }

                // Crea il nuovo JSON con solo i campi mancanti
                JObject result = new JObject();
                result["fields"] = missingFields;

                Console.WriteLine($"[AI] Creato JSON con {missingFields.Count} campi mancanti per l'AI");

                return result.ToString();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[AI ERROR] Errore nella creazione del JSON per campi mancanti: {ex.Message}");
                return originalJsonFields;
            }
        }

        /// <summary>
        /// Merge tra dati estratti dal PDF generico e dati restituiti dall'AI.
        /// </summary>
        /// <param name="extractedJson">JSON con i dati estratti dal PDF</param>
        /// <param name="aiResponseJson">JSON con i dati restituiti dall'AI per i campi mancanti</param>
        /// <returns>JSON merged con tutti i dati</returns>
        private string MergeExtractedAndAIData(string extractedJson, string aiResponseJson)
        {
            try
            {
                JObject extractedObj = JObject.Parse(extractedJson);
                JObject aiResponseObj = JObject.Parse(aiResponseJson);

                JArray extractedFields = extractedObj["fields"] as JArray;
                JArray aiFields = aiResponseObj["fields"] as JArray;

                if (extractedFields == null)
                    return aiResponseJson;

                if (aiFields == null || aiFields.Count == 0)
                    return extractedJson;

                // Crea un dizionario dei campi estratti per accesso rapido
                Dictionary<string, JObject> extractedFieldsMap = new Dictionary<string, JObject>();
                foreach (JObject field in extractedFields)
                {
                    string name = field["name"]?.ToString();
                    if (!string.IsNullOrEmpty(name))
                        extractedFieldsMap[name] = field;
                }

                // Aggiungi o aggiorna i campi dall'AI
                foreach (JObject aiField in aiFields)
                {
                    string fieldName = aiField["name"]?.ToString();
                    if (string.IsNullOrEmpty(fieldName))
                        continue;

                    // Se il campo esiste già nei dati estratti
                    if (extractedFieldsMap.ContainsKey(fieldName))
                    {
                        JObject extractedField = extractedFieldsMap[fieldName];
                        string extractedValue = extractedField["value"]?.ToString();

                        // Se il valore estratto è vuoto, aggiorna con quello dell'AI
                        if (string.IsNullOrWhiteSpace(extractedValue))
                        {
                            string aiValue = aiField["value"]?.ToString();
                            if (!string.IsNullOrWhiteSpace(aiValue))
                            {
                                extractedField["value"] = aiField["value"];
                                Console.WriteLine($"[AI] Campo '{fieldName}' aggiornato con valore dall'AI: {aiValue}");
                            }
                        }
                    }
                    // Se il campo non esiste nei dati estratti, lo aggiungiamo
                    else
                    {
                        extractedFields.Add(aiField);
                        Console.WriteLine($"[AI] Campo '{fieldName}' aggiunto dall'AI");
                    }
                }

                // Restituisce il JSON merged
                return extractedObj.ToString();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[AI ERROR] Errore nel merge tra dati estratti e AI: {ex.Message}");
                // In caso di errore, restituisce i dati dall'AI
                return aiResponseJson;
            }
        }
    }
}
