// 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 SAL Attivo (Stato Avanzamento Lavori).
    /// Regex calibrate su DUE template reali con layout differenti:
    ///   - Figline e Incisa Valdarno (PriMus): RIEPILOGO FINALE dettagliato per
    ///     misura/economia, label "LAVORI A MISURA euro" / "PER LAVORI A MISURA euro" /
    ///     "COSTI SICUREZZA (SPECIALI) euro" / "T O T A L E euro" / "RIBASSO del X%".
    ///   - Villa Huffer (Banca d'Italia): "PER LAVORI A MISURA" / "T O T A L E euro" /
    ///     "TOTALE GENERALE euro" / "ribasso contrattuale (-X%)" / "Oneri per la sicurezza" /
    ///     "Lavori in economia (come da liste)". Espone anche CIG/CUP.
    ///
    /// La strategia è multi-pattern: ogni campo prova prima il pattern Figline, poi
    /// quello Villa Huffer, poi fallback generici. Numeri italiani con vari separatori
    /// migliaia (`.`, ` `, `'`, `´` da pdftotext corrotto, `’`).
    /// </summary>
    public class SalPdfHandler
    {
        private readonly bool _debug;

        private const string THSEP = @"[\.\s'´’]";
        private const string NUM_IT = @"-?[0-9]{1,3}(?:" + THSEP + @"[0-9]{3})*,[0-9]{2}";

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

        public bool IsSalPdf(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("sal") ||
                fileName.Contains("stato avanzamento") ||
                fileName.Contains("stato_avanzamento");
            if (!nameMatch) return false;

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

                int hits = 0;
                if (Regex.IsMatch(text, @"STATO\s+AVANZAMENTO\s+LAVORI", RegexOptions.IgnoreCase)) hits++;
                if (Regex.IsMatch(text, @"RIEPILOGO\s+FINALE",          RegexOptions.IgnoreCase)) hits++;
                if (Regex.IsMatch(text, @"LAVORI\s+A\s+MISURA",         RegexOptions.IgnoreCase)) hits++;
                if (Regex.IsMatch(text, @"(COSTI\s+SICUREZZA|Oneri\s+per\s+la\s+sicurezza)", RegexOptions.IgnoreCase)) hits++;
                return hits >= 2;
            }
            catch
            {
                return false;
            }
        }

        public string ExtractAndConvertToJson(FileInfo file, string existingJsonFields)
        {
            if (_debug) Console.WriteLine($"[SAL] Inizio estrazione: {file.Name}");
            string text = ExtractPdfText(file.FullName);

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

            if (_debug)
            {
                Console.WriteLine($"[SAL] Campi estratti: {extracted.Count}");
                foreach (var kv in extracted) Console.WriteLine($"[SAL]   {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"] = "SalPdfHandler";
            jsonObj["extraction_timestamp"] = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
            return jsonObj.ToString();
        }

        private Dictionary<string, string> ExtractSalFields(string text)
        {
            var d = new Dictionary<string, string>();
            if (string.IsNullOrWhiteSpace(text)) return d;

            // === CIG === (presente in alcuni template tipo Villa Huffer, assente in Figline)
            var mCig = Regex.Match(text, @"\bCIG\s*[:.\-]?\s*([0-9A-Z]{10})\b", RegexOptions.IgnoreCase);
            if (mCig.Success) d["cig"] = mCig.Groups[1].Value.ToUpperInvariant();

            // === CUP === (15 caratteri)
            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();

            // === Progressivo SAL + Data ===
            var mProgData = Regex.Match(text,
                @"STATO\s+AVANZAMENTO\s+LAVORI\s+N\.?\s*([0-9]+\s*(?:bis|tris|quater)?)\s+A\s+TUTTO\s+IL\s+([0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4})",
                RegexOptions.IgnoreCase);
            if (mProgData.Success)
            {
                d["progressivo"] = mProgData.Groups[1].Value.Replace(" ", "");
                d["data_fine"] = NormalizeDateIt(mProgData.Groups[2].Value);
            }

            // === TOTALE PROGRESSIVO (netto finale del SAL) ===
            // Villa Huffer ha "TOTALE GENERALE euro" come netto finale (distinto da "T O T A L E euro").
            // Figline ha "T O T A L E euro" come totale.
            // Strategia: cerco prima "TOTALE GENERALE" (più specifico), poi fallback su "T O T A L E".
            d["importo_totale_progressivo"] = FindNumberAfter(text,
                @"TOTALE\s+GENERALE\s+euro", windowSize: 400, skipNegative: true);
            if (string.IsNullOrWhiteSpace(d["importo_totale_progressivo"]))
            {
                d["importo_totale_progressivo"] = FindNumberAfter(text,
                    @"T\s*O\s*T\s*A\s*L\s*E\s+euro(?!\s+A\s+RIPORTARE)(?!\s+GENERALE)",
                    windowSize: 600, skipNegative: true);
            }

            // === Lavori a misura LORDO ===
            // Figline: "LAVORI A MISURA euro RIBASSO del 9,52% da applicare su euro 1´346´608,28"
            // Villa Huffer: solo "PER LAVORI A MISURA" (no distinzione lordo/netto in modo esplicito)
            d["lavori_misura_lordo"] = FindNumberAfter(text,
                @"LAVORI\s+A\s+MISURA\s+euro\s+RIBASSO\s+del", windowSize: 400, skipNegative: true);
            if (string.IsNullOrWhiteSpace(d["lavori_misura_lordo"]))
                d["lavori_misura_lordo"] = FindNumberAfter(text, @"applicare\s+su\s+euro", windowSize: 80, skipNegative: true);

            // === Lavori a misura NETTO ===
            d["lavori_misura_netto"] = FindNumberAfter(text,
                @"PER\s+LAVORI\s+A\s+MISURA\s+euro", windowSize: 400, skipNegative: true);

            // === Lavori in economia LORDO ===
            d["lavori_economia_lordo"] = FindNumberAfter(text,
                @"LAVORI\s+IN\s+ECONOMIA\s+euro", windowSize: 400, skipNegative: true,
                excludePrefix: "PER LAVORI IN ECONOMIA");
            if (string.IsNullOrWhiteSpace(d["lavori_economia_lordo"]))
            {
                // Villa Huffer: "Lavori in economia (come da liste)" → numero in tabella
                d["lavori_economia_lordo"] = FindNumberAfter(text,
                    @"Lavori\s+in\s+economia\s*\(\s*come\s+da\s+liste\s*\)", windowSize: 200, skipNegative: true);
            }

            // === Lavori in economia NETTO ===
            d["lavori_economia_netto"] = FindNumberAfter(text,
                @"PER\s+LAVORI\s+IN\s+ECONOMIA\s+euro", windowSize: 400, skipNegative: true);

            // === Costi sicurezza speciali ===
            d["costi_sicurezza_speciali"] = FindNumberAfter(text,
                @"COSTI\s+SICUREZZA\s*\(\s*SPECIALI\s*\)\s+euro", windowSize: 200, skipNegative: true);
            if (string.IsNullOrWhiteSpace(d["costi_sicurezza_speciali"]))
            {
                // Variante Figline header: "Imp orto della Sicurezza ... NON soggetto a Ribasso euro <num>"
                d["costi_sicurezza_speciali"] = FindNumberAfter(text,
                    @"Imp\s*orto\s+della\s+Sicurezza[^\r\n]*?NON\s+sogget\w*\s+a\s+Ribasso\s+euro",
                    windowSize: 80);
            }
            if (string.IsNullOrWhiteSpace(d["costi_sicurezza_speciali"]))
            {
                // Variante Villa Huffer: "Oneri per la sicurezza" → numero in tabella
                d["costi_sicurezza_speciali"] = FindNumberAfter(text,
                    @"Oneri\s+per\s+la\s+sicurezza", windowSize: 200, skipNegative: true);
            }

            // === Ribasso percentuale ===
            // Figline: "RIBASSO del 9,52%"
            var mRibPerc = Regex.Match(text, @"RIBASSO\s+del\s+([0-9]+(?:,[0-9]+)?)\s*%", RegexOptions.IgnoreCase);
            if (mRibPerc.Success) d["ribasso_percentuale"] = mRibPerc.Groups[1].Value;
            else
            {
                // Villa Huffer: "ribasso contrattuale (-36,077%)" — il segno meno indica detrazione
                var mRibPerc2 = Regex.Match(text, @"ribasso\s+contrattuale\s*\(\s*-?([0-9]+(?:,[0-9]+)?)\s*%\s*\)", RegexOptions.IgnoreCase);
                if (mRibPerc2.Success) d["ribasso_percentuale"] = mRibPerc2.Groups[1].Value;
            }

            // === Ribasso importo === (primo importo negativo dopo riga ribasso)
            var mRibImp = Regex.Match(text,
                @"RIBASSO\s+del\s+[0-9,.]+\s*%[^=]*=\s*euro[\s\r\n]+(" + NUM_IT + @")",
                RegexOptions.IgnoreCase);
            if (mRibImp.Success) d["ribasso_importo"] = mRibImp.Groups[1].Value;
            else
            {
                // Villa Huffer: subito dopo "a detrarre ribasso contrattuale" — cerco il primo importo negativo
                var mRibImp2 = Regex.Match(text,
                    @"(?:a\s+detrarre\s+)?ribasso\s+contrattuale[^\r\n]*?[\r\n][\s\S]{0,200}?(-" + NUM_IT.Substring(2) + @")",
                    RegexOptions.IgnoreCase);
                if (mRibImp2.Success) d["ribasso_importo"] = mRibImp2.Groups[1].Value;
            }

            // === Ritenute / Acconti === (raramente nel SAL, più nei CdP)
            string rit = FindNumberAfter(text, @"Ritenut[ae]\s+(?:di\s+Garanzia|0[,.]5\s*%|a\s+Garanzia|infortuni)", 100);
            if (!string.IsNullOrWhiteSpace(rit)) d["ritenute"] = rit;
            string acc = FindNumberAfter(text, @"Accont[oi]", 80);
            if (!string.IsNullOrWhiteSpace(acc)) d["acconti"] = acc;

            // Cleanup vuoti
            foreach (var k in d.Where(kv => string.IsNullOrWhiteSpace(kv.Value)).Select(kv => kv.Key).ToList())
                d.Remove(k);

            // Normalizza i numeri (rimuovi separatori migliaia, mantieni virgola)
            foreach (var key in d.Keys.ToList())
            {
                if (key == "progressivo" || key == "data_inizio" || key == "data_fine" ||
                    key == "cig" || key == "cup" || key == "ribasso_percentuale")
                    continue;
                d[key] = NormalizeNumber(d[key]);
            }

            return d;
        }

        /// <summary>
        /// Cerca il primo numero italiano in una finestra di N caratteri dopo l'ancora.
        /// </summary>
        private string FindNumberAfter(string text, string anchorPattern, int windowSize,
                                        bool takeAbs = false, bool skipNegative = false,
                                        string excludePrefix = null)
        {
            foreach (Match anchor in Regex.Matches(text, anchorPattern, RegexOptions.IgnoreCase))
            {
                if (!string.IsNullOrEmpty(excludePrefix))
                {
                    int start = Math.Max(0, anchor.Index - excludePrefix.Length - 5);
                    string before = text.Substring(start, anchor.Index - start);
                    if (before.IndexOf(excludePrefix, StringComparison.OrdinalIgnoreCase) >= 0) continue;
                }

                int from = anchor.Index + anchor.Length;
                int to = Math.Min(text.Length, from + windowSize);
                string slice = text.Substring(from, to - from);

                foreach (Match numMatch in Regex.Matches(slice, NUM_IT))
                {
                    string v = numMatch.Value;
                    if (skipNegative && v.TrimStart().StartsWith("-")) continue;
                    return takeAbs && v.StartsWith("-") ? v.Substring(1) : v;
                }
            }
            return null;
        }

        /// <summary>Normalizza numero italiano: rimuove separatori migliaia (qualsiasi encoding), virgola resta decimale.</summary>
        private string NormalizeNumber(string v)
        {
            if (string.IsNullOrWhiteSpace(v)) return v;
            return Regex.Replace(v, THSEP, "").Trim();
        }

        /// <summary>Data dd/MM/yyyy → yyyy-MM-dd.</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();
        }
    }
}
