import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

// Importy OpenCSV
import com.opencsv.CSVParser;
import com.opencsv.CSVParserBuilder;
import com.opencsv.CSVReader;
import com.opencsv.CSVReaderBuilder;
import com.opencsv.exceptions.CsvValidationException;

// Importy XML
import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

public class OptimaConverterApp extends JFrame {

    // --- Klasa wyjątku wewnętrznego ---
    public static class ValidationException extends Exception {
        public ValidationException(String message) { super(message); }
    }

    // Pola interfejsu
    private JTextField txtKontrahenci;
    private JTextField txtRejestr;
    private JTextArea txtLogi;
    private JButton btnKonwertuj;

    // Zmienne na ścieżki
    private File fileKontrahenci;
    private File fileRejestr;

    // Stałe Optima
    private static final String WERSJA_STRUKTURY = "2.00";
    private static final String BAZA_ZRD_ID = "CSV_IMPORT";
    private static final String BAZA_DOC_ID = "KSIEG"; 

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            try {
                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            } catch (Exception e) {
                // ignorujemy błędy wyglądu
            }
            new OptimaConverterApp().setVisible(true);
        });
    }

    public OptimaConverterApp() {
        super("David Ali - s26567@st.pans.nysa.pl - Konwerter CSV do Optimy");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(800, 650);
        setLocationRelativeTo(null);
        setLayout(new BorderLayout(10, 10));

        // Panel górny
        JPanel pnlInput = new JPanel(new GridBagLayout());
        pnlInput.setBorder(BorderFactory.createTitledBorder("Krok 1: Wybierz pliki"));
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.insets = new Insets(5, 5, 5, 5);

        // Wiersz 1: Kontrahenci
        gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 0.1;
        pnlInput.add(new JLabel("Kontrahenci (CSV):"), gbc);

        txtKontrahenci = new JTextField();
        txtKontrahenci.setEditable(false);
        gbc.gridx = 1; gbc.weightx = 0.8;
        pnlInput.add(txtKontrahenci, gbc);

        JButton btnWybierzKontr = new JButton("Wybierz...");
        gbc.gridx = 2; gbc.weightx = 0.1;
        pnlInput.add(btnWybierzKontr, gbc);

        // Wiersz 2: Rejestr
        gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0.1;
        pnlInput.add(new JLabel("Rejestr VAT (CSV):"), gbc);

        txtRejestr = new JTextField();
        txtRejestr.setEditable(false);
        gbc.gridx = 1; gbc.weightx = 0.8;
        pnlInput.add(txtRejestr, gbc);

        JButton btnWybierzRejestr = new JButton("Wybierz...");
        gbc.gridx = 2; gbc.weightx = 0.1;
        pnlInput.add(btnWybierzRejestr, gbc);

        add(pnlInput, BorderLayout.NORTH);

        // Panel środkowy
        txtLogi = new JTextArea();
        txtLogi.setEditable(false);
        txtLogi.setFont(new Font("Monospaced", Font.PLAIN, 12));
        JScrollPane scrollPane = new JScrollPane(txtLogi);
        scrollPane.setBorder(BorderFactory.createTitledBorder("Logi przetwarzania"));
        add(scrollPane, BorderLayout.CENTER);

        // Panel dolny
        JPanel pnlButton = new JPanel(new FlowLayout(FlowLayout.CENTER));
        btnKonwertuj = new JButton("KROK 2: GENERUJ XML");
        btnKonwertuj.setFont(new Font("Arial", Font.BOLD, 14));
        btnKonwertuj.setBackground(new Color(200, 240, 200));
        btnKonwertuj.setEnabled(false);
        pnlButton.add(btnKonwertuj);
        add(pnlButton, BorderLayout.SOUTH);

        // --- AKCJE ---

        btnWybierzKontr.addActionListener(e -> {
            File f = chooseFile("Plik Kontrahentów");
            if (f != null) {
                fileKontrahenci = f;
                txtKontrahenci.setText(f.getAbsolutePath());
                log("Kontrahenci: " + f.getName());
                checkReady();
            }
        });

        btnWybierzRejestr.addActionListener(e -> {
            File f = chooseFile("Plik Rejestru VAT");
            if (f != null) {
                fileRejestr = f;
                txtRejestr.setText(f.getAbsolutePath());
                log("Rejestr VAT: " + f.getName());
                checkReady();
            }
        });

        btnKonwertuj.addActionListener(e -> {
            txtLogi.setText("");
            runConversion();
        });

        log("Witaj. Wybierz pliki CSV.");
    }

    private void checkReady() {
        if (fileKontrahenci != null || fileRejestr != null) {
            btnKonwertuj.setEnabled(true);
        }
    }

    private File chooseFile(String title) {
        JFileChooser fc = new JFileChooser(new File(System.getProperty("user.dir")));
        fc.setDialogTitle(title);
        fc.setFileFilter(new FileNameExtensionFilter("Pliki CSV", "csv"));
        if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
            return fc.getSelectedFile();
        }
        return null;
    }

    private void log(String msg) {
        txtLogi.append(msg + "\n");
        txtLogi.setCaretPosition(txtLogi.getDocument().getLength());
    }

    // --- GŁÓWNA LOGIKA KONWERSJI ---

    private void runConversion() {
        JFileChooser fc = new JFileChooser(new File(System.getProperty("user.dir")));
        fc.setDialogTitle("Zapisz plik XML");
        fc.setSelectedFile(new File("optima_import.xml"));
        fc.setFileFilter(new FileNameExtensionFilter("Plik XML", "xml"));

        if (fc.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return;
        File outputFile = fc.getSelectedFile();

        new Thread(() -> {
            try {
                SwingUtilities.invokeLater(() -> btnKonwertuj.setEnabled(false));
                log(">>> Start konwersji...");

                DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
                DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
                Document doc = docBuilder.newDocument();

                Element rootElement = doc.createElement("ROOT");
                rootElement.setAttribute("xmlns", "http://www.comarch.pl/cdn/optima/offline");
                doc.appendChild(rootElement);

                // 1. Kontrahenci
                if (fileKontrahenci != null) {
                    processKontrahenci(doc, rootElement, fileKontrahenci);
                }

                // 2. Rejestr VAT
                if (fileRejestr != null) {
                    processRejestrSprzedazy(doc, rootElement, fileRejestr);
                }

                writeXml(doc, outputFile);

                log(">>> SUKCES! Plik zapisany: " + outputFile.getName());
                JOptionPane.showMessageDialog(this, "Gotowe!", "Sukces", JOptionPane.INFORMATION_MESSAGE);

            } catch (Exception e) {
                log("!!! BŁĄD: " + e.getMessage());
                e.printStackTrace();
                JOptionPane.showMessageDialog(this, "Wystąpił błąd:\n" + e.getMessage(), "Błąd", JOptionPane.ERROR_MESSAGE);
            } finally {
                SwingUtilities.invokeLater(() -> btnKonwertuj.setEnabled(true));
            }
        }).start();
    }

    // --- CZYTANIE CSV ---
    private List<Map<String, String>> readCsvFile(File file) throws IOException, CsvValidationException, ValidationException {
        List<Map<String, String>> records = new ArrayList<>();
        
        CSVParser parser = new CSVParserBuilder().withSeparator(',').build();
        
        try (CSVReader reader = new CSVReaderBuilder(new FileReader(file, StandardCharsets.UTF_8))
                .withCSVParser(parser)
                .build()) {
            
            String[] header = reader.readNext();
            if (header == null) return records;

            // Usuwanie BOM
            if (header.length > 0 && header[0].startsWith("\uFEFF")) {
                header[0] = header[0].substring(1);
            }

            int lineNumber = 1; // Licznik linii (1 to nagłówek)
            String[] line;
            while ((line = reader.readNext()) != null) {
                lineNumber++;
                
                // WALIDACJA LICZBY KOLUMN
                if (line.length != header.length) {
                    throw new ValidationException(
                        "Błąd krytyczny w wierszu " + lineNumber + ": " +
                        "Nieprawidłowa liczba kolumn. Oczekiwano: " + header.length + 
                        ", otrzymano: " + line.length + ". Sprawdź separatory w pliku CSV."
                    );
                }

                Map<String, String> row = new HashMap<>();
                for (int i = 0; i < header.length; i++) {
                    row.put(header[i].trim(), line[i].trim());
                }
                records.add(row);
            }
        }
        return records;
    }

    // --- PROCESOWANIE KONTRAHENTÓW ---
   private void processKontrahenci(Document doc, Element rootElement, File csvFile) throws Exception {
        log("Przetwarzam kontrahentów...");
        Element kontrahenciNode = doc.createElement("KONTRAHENCI");
        addRequiredHeaderTags(doc, kontrahenciNode);

        List<Map<String, String>> rows = readCsvFile(csvFile);

        int count = 0;
        for (Map<String, String> row : rows) {
            count++;
            String nazwa = row.get("ADRES_NAZWA1");
            log("Przetwarzam: " + nazwa);

            Element kontrahent = doc.createElement("KONTRAHENT");
            
            createCdataElement(doc, kontrahent, "ID_ZRODLA", row.get("ID_ZRODLA"));
            createCdataElement(doc, kontrahent, "AKRONIM", row.get("AKRONIM"));
            createCdataElement(doc, kontrahent, "FINALNY", row.get("FINALNY"));
            createCdataElement(doc, kontrahent, "OPIS", row.get("OPIS"));

            Element adresy = doc.createElement("ADRESY");
            Element adres = doc.createElement("ADRES");

            createCdataElement(doc, adres, "STATUS", row.get("ADRES_STATUS"));
            createCdataElement(doc, adres, "NAZWA1", row.get("ADRES_NAZWA1"));
            createCdataElement(doc, adres, "NAZWA2", row.get("ADRES_NAZWA2"));
            createCdataElement(doc, adres, "KRAJ", row.get("ADRES_KRAJ"));
            createCdataElement(doc, adres, "MIASTO", row.get("ADRES_MIASTO"));
            createCdataElement(doc, adres, "ULICA", row.get("ADRES_ULICA"));
            createCdataElement(doc, adres, "NR_DOMU", row.get("ADRES_NR_DOMU"));
            createCdataElement(doc, adres, "NR_LOKALU", row.get("ADRES_NR_LOKALU"));
            createCdataElement(doc, adres, "KOD_POCZTOWY", row.get("ADRES_KOD_POCZTOWY"));
            createCdataElement(doc, adres, "POCZTA", row.get("ADRES_POCZTA"));
            createCdataElement(doc, adres, "NIP", row.get("ADRES_NIP"));
            createCdataElement(doc, adres, "TELEFON1", row.get("ADRES_TELEFON1"));
            createCdataElement(doc, adres, "EMAIL", row.get("ADRES_EMAIL"));

            adresy.appendChild(adres);
            kontrahent.appendChild(adresy);
            kontrahenciNode.appendChild(kontrahent);
        }

        if (kontrahenciNode.hasChildNodes()) {
            rootElement.appendChild(kontrahenciNode);
        }
        log("-> Kontrahenci OK: " + rows.size() + " szt.");
    }

    // --- PROCESOWANIE FAKTUR ---
    private void processRejestrSprzedazy(Document doc, Element rootElement, File csvFile) throws Exception {
        Element rejestryNode = doc.createElement("REJESTRY_SPRZEDAZY_VAT");
        addRequiredHeaderTags(doc, rejestryNode);

        // Tu wywoła się nasz nowy, bezpieczny readCsvFile
        List<Map<String, String>> rows = readCsvFile(csvFile);
        
        if (rows.isEmpty()) {
            log("!!! UWAGA: Pusty plik rejestru lub zły separator.");
        }

        int count = 0;
        for (Map<String, String> row : rows) {
            count++; // To jest numer wiersza danych (nie linii w pliku)
            String numerFaktury = row.get("NUMER");
            
            // --- SEKCJA WALIDACJI (Nowość) ---
            
            // 1. Sprawdzenie czy kluczowe pola istnieją
            if (getSafe(row, "NUMER", "").isEmpty()) {
                throw new ValidationException("Błąd w wierszu " + count + ": Brak numeru faktury (kolumna NUMER).");
            }
            
            // 2. Walidacja dat [cite: 12, 55]
            validateDate(row.get("DATA_WYSTAWIENIA"), "DATA_WYSTAWIENIA", count);
            validateDate(row.get("DATA_SPRZEDAZY"), "DATA_SPRZEDAZY", count);
            validateDate(row.get("TERMIN"), "TERMIN", count);

            // 3. Walidacja liczb w pozycjach (obsługa znaku "|") [cite: 88, 89, 90]
            validateSplitNumeric(row.get("POZYCJE_NETTO"), "POZYCJE_NETTO", count);
            validateSplitNumeric(row.get("POZYCJE_VAT"), "POZYCJE_VAT", count);
            //validateSplitNumeric(row.get("POZYCJE_STAWKA"), "POZYCJE_STAWKA", count);
            validateSplitVatRates(row.get("POZYCJE_STAWKA"), count);
            
            // 4. Walidacja płatności [cite: 126]
            validateSplitNumeric(row.get("PLATNOSCI_KWOTA"), "PLATNOSCI_KWOTA", count);

            // --- KONIEC WALIDACJI ---

            log("Przetwarzam: " + numerFaktury);

            Element rejestr = doc.createElement("REJESTR_SPRZEDAZY_VAT");

            // ... (reszta kodu bez zmian: tworzenie XML) ...
            // Skopiuj tutaj resztę oryginalnego kodu z tej metody, 
            // zaczynając od: createCdataElement(doc, rejestr, "ID_ZRODLA", ...
            
            createCdataElement(doc, rejestr, "ID_ZRODLA", UUID.randomUUID().toString());
            createCdataElement(doc, rejestr, "MODUL", "Rejestr Vat");
            createCdataElement(doc, rejestr, "TYP", "Rejestr sprzedazy");
            createCdataElement(doc, rejestr, "REJESTR", getSafe(row, "REJESTR", "SPRZEDAŻ"));
            
            createCdataElement(doc, rejestr, "DATA_WYSTAWIENIA", row.get("DATA_WYSTAWIENIA"));
            createCdataElement(doc, rejestr, "DATA_SPRZEDAZY", row.get("DATA_SPRZEDAZY"));
            createCdataElement(doc, rejestr, "TERMIN", row.get("TERMIN"));
            createCdataElement(doc, rejestr, "DATA_DATAOBOWIAZKUPODATKOWEGO", row.get("DATA_SPRZEDAZY"));
            createCdataElement(doc, rejestr, "NUMER", numerFaktury);
            
            createCdataElement(doc, rejestr, "KOREKTA", "Nie");
            createCdataElement(doc, rejestr, "FINALNY", "Tak");
            createCdataElement(doc, rejestr, "PODATNIK_CZYNNY", "Tak");
            createCdataElement(doc, rejestr, "TYP_PODMIOTU", "kontrahent");
            
            createCdataElement(doc, rejestr, "PODMIOT", row.get("KONTRAHENT_KOD")); 
            createCdataElement(doc, rejestr, "PODMIOT_NIP", row.get("KONTRAHENT_NIP"));
            createCdataElement(doc, rejestr, "NAZWA1", row.get("KONTRAHENT_NAZWA"));
            createCdataElement(doc, rejestr, "NIP", row.get("KONTRAHENT_NIP"));
            createCdataElement(doc, rejestr, "MIASTO", row.get("ADRES_MIASTO"));
            createCdataElement(doc, rejestr, "ULICA", row.get("ADRES_ULICA"));

            createCdataElement(doc, rejestr, "TYP_PLATNIKA", "kontrahent");
            createCdataElement(doc, rejestr, "PLATNIK", row.get("KONTRAHENT_KOD"));

            createCdataElement(doc, rejestr, "WALUTA", "PLN"); 
            createCdataElement(doc, rejestr, "KURS_WALUTY", "NBP");
            createCdataElement(doc, rejestr, "NOTOWANIE_WALUTY_ILE", "1");
            createCdataElement(doc, rejestr, "NOTOWANIE_WALUTY_ZA_ILE", "1");
            createCdataElement(doc, rejestr, "DATA_KURSU", row.get("DATA_WYSTAWIENIA"));

            // --- POZYCJE (SPLIT) ---
            Element pozycjeNode = doc.createElement("POZYCJE");
            
            String sNettoAll = getSafe(row, "POZYCJE_NETTO", "0");
            String sVatAll = getSafe(row, "POZYCJE_VAT", "0");
            String sStawkaAll = getSafe(row, "POZYCJE_STAWKA", "23");
            String sRodzajAll = getSafe(row, "POZYCJE_RODZAJ", "towary");
            String sKolumnaKpr = getSafe(row, "KOLUMNA_KPR", "Sprzedaż");

            List<String> listNetto = splitValues(sNettoAll);
            List<String> listVat = splitValues(sVatAll);
            List<String> listStawka = splitValues(sStawkaAll);
            List<String> listRodzaj = splitValues(sRodzajAll);

            int countPos = listNetto.size();

            for (int i = 0; i < countPos; i++) {
                Element poz = doc.createElement("POZYCJA");
                
                String valNetto = getListVal(listNetto, i);
                String valVat = getListVal(listVat, i);
                String valStawkaRaw = getListVal(listStawka, i); // Oryginalna wartość z CSV
                String valRodzaj = getListVal(listRodzaj, i);

                createCdataElement(doc, poz, "LP", String.valueOf(i + 1));

                // --- LOGIKA OBSŁUGI ZW / NP ---
                String stawkaDoXml = valStawkaRaw;
                String statusVat = "opodatkowana";

                if ("zw".equalsIgnoreCase(valStawkaRaw)) {
                    // Dla "zw": Stawka w XML to 0.00, Status to "zwolniona" 
                    stawkaDoXml = "0.00";
                    statusVat = "zwolniona";
                } else if ("np".equalsIgnoreCase(valStawkaRaw)) {
                    // Dla "np": Stawka w XML to 0.00, Status to "nie podlega" 
                    stawkaDoXml = "0.00";
                    statusVat = "nie podlega";
                }
                
                // Zapisujemy przetworzone wartości
                createCdataElement(doc, poz, "STAWKA_VAT", stawkaDoXml); // 
                createCdataElement(doc, poz, "STATUS_VAT", statusVat);   // 
                // --------------------------------

                createCdataElement(doc, poz, "NETTO", valNetto);
                createCdataElement(doc, poz, "VAT", valVat);
                createCdataElement(doc, poz, "NETTO_SYS", valNetto);
                createCdataElement(doc, poz, "VAT_SYS", valVat);
                createCdataElement(doc, poz, "NETTO_SYS2", valNetto);
                createCdataElement(doc, poz, "VAT_SYS2", valVat);
                createCdataElement(doc, poz, "RODZAJ_SPRZEDAZY", valRodzaj);
                createCdataElement(doc, poz, "UWZ_W_PROPORCJI", "tak");
                createCdataElement(doc, poz, "KOLUMNA_KPR", sKolumnaKpr);

                pozycjeNode.appendChild(poz);
            }
            rejestr.appendChild(pozycjeNode);

            // --- PŁATNOŚCI (SPLIT) ---
            Element platnosciNode = doc.createElement("PLATNOSCI");
            
            String sPlatForma = getSafe(row, "PLATNOSCI_FORMA", "gotówka");
            String sPlatKwota = getSafe(row, "PLATNOSCI_KWOTA", "0");
            String sPlatTermin = getSafe(row, "PLATNOSCI_TERMIN", row.get("TERMIN"));

            List<String> listPlatForma = splitValues(sPlatForma);
            List<String> listPlatKwota = splitValues(sPlatKwota);
            List<String> listPlatTermin = splitValues(sPlatTermin);

            int countPlat = listPlatKwota.size();

            for (int j = 0; j < countPlat; j++) {
                Element plat = doc.createElement("PLATNOSC");

                String valForma = getListVal(listPlatForma, j);
                String valKwota = getListVal(listPlatKwota, j);
                String valTermin = getListVal(listPlatTermin, j);

                createCdataElement(doc, plat, "TERMIN_PLAT", valTermin);
                createCdataElement(doc, plat, "FORMA_PLATNOSCI_PLAT", valForma);
                createCdataElement(doc, plat, "KWOTA_PLAT", valKwota);
                createCdataElement(doc, plat, "WALUTA_PLAT", "PLN");
                createCdataElement(doc, plat, "WALUTA_DOK", "PLN");
                createCdataElement(doc, plat, "KURS_WALUTY_PLAT", "NBP");
                createCdataElement(doc, plat, "NOTOWANIE_WALUTY_ILE_PLAT", "1");
                createCdataElement(doc, plat, "NOTOWANIE_WALUTY_ZA_ILE_PLAT", "1");
                createCdataElement(doc, plat, "KWOTA_PLN_PLAT", valKwota);
                createCdataElement(doc, plat, "KIERUNEK", "przychód");
                createCdataElement(doc, plat, "PODLEGA_ROZLICZENIU", "tak");
                
                createCdataElement(doc, plat, "PLATNOSC_TYP_PODMIOTU", "kontrahent");
                createCdataElement(doc, plat, "PLATNOSC_PODMIOT", row.get("KONTRAHENT_KOD"));

                platnosciNode.appendChild(plat);
            }
            rejestr.appendChild(platnosciNode);
            rejestryNode.appendChild(rejestr);
        }

        rootElement.appendChild(rejestryNode);
        log("-> Rejestr VAT OK: " + count + " dok.");
    }

    // --- METODY POMOCNICZE ---

    private List<String> splitValues(String raw) {
        if (raw == null || raw.isEmpty()) return new ArrayList<>();
        String[] parts = raw.split("\\|");
        List<String> result = new ArrayList<>();
        for (String p : parts) {
            result.add(p.trim());
        }
        return result;
    }

    private void validateDate(String dateStr, String fieldName, int rowNum) throws ValidationException {
        if (dateStr == null || dateStr.trim().isEmpty()) return; // Puste pola pomijamy (chyba że są wymagane, to sprawdzamy osobno)
        
        // Wymagany format RRRR-MM-DD
        if (!dateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
            throw new ValidationException(
                "Błąd w wierszu " + rowNum + ", kolumna '" + fieldName + "': " +
                "Nieprawidłowy format daty ('" + dateStr + "'). Wymagane: RRRR-MM-DD."
            );
        }
    }

    private void validateNumeric(String numStr, String fieldName, int rowNum) throws ValidationException {
        if (numStr == null || numStr.trim().isEmpty()) return;
        
        try {
            // Optima oczekuje kropki jako separatora dziesiętnego w XML, ale w CSV użytkownik może mieć przecinek
            String safeNum = numStr.replace(",", ".");
            Double.parseDouble(safeNum);
        } catch (NumberFormatException e) {
            throw new ValidationException(
                "Błąd w wierszu " + rowNum + ", kolumna '" + fieldName + "': " +
                "Wartość '" + numStr + "' nie jest liczbą."
            );
        }
    }

    // Do walidacji pól, które są rozdzielane znakiem pipe "|" (np. netto|netto|netto)
    private void validateSplitNumeric(String rawValue, String fieldName, int rowNum) throws ValidationException {
        List<String> parts = splitValues(rawValue);
        for (String part : parts) {
            validateNumeric(part, fieldName, rowNum);
        }
    }

    // Specjalna walidacja dla stawek VAT (akceptuje liczby oraz "zw" i "np")
    private void validateSplitVatRates(String rawValue, int rowNum) throws ValidationException {
        List<String> parts = splitValues(rawValue);
        for (String part : parts) {
            String val = part.toLowerCase();
            // Jeśli to "zw" lub "np" - jest OK, idziemy dalej
            if (val.equals("zw") || val.equals("np")) {
                continue;
            }
            // W przeciwnym razie musi to być liczba
            try {
                Double.parseDouble(val.replace(",", "."));
            } catch (NumberFormatException e) {
                throw new ValidationException(
                    "Błąd w wierszu " + rowNum + ", kolumna 'POZYCJE_STAWKA': " +
                    "Wartość '" + part + "' jest nieprawidłowa. Dozwolone: liczby (np. 23), 'zw' lub 'np'."
                );
            }
        }
    }

    private String getListVal(List<String> list, int index) {
        if (list == null || list.isEmpty()) return "";
        if (index >= list.size()) return list.get(list.size() - 1);
        return list.get(index);
    }

    private String getSafe(Map<String, String> row, String key, String defaultVal) {
        String val = row.get(key);
        if (val == null || val.trim().isEmpty()) return defaultVal;
        return val.trim();
    }

    private void addRequiredHeaderTags(Document doc, Element parentNode) {
        Element wersja = doc.createElement("WERSJA");
        wersja.setTextContent(WERSJA_STRUKTURY);
        parentNode.appendChild(wersja);

        Element bazaZrd = doc.createElement("BAZA_ZRD_ID");
        bazaZrd.setTextContent(BAZA_ZRD_ID);
        parentNode.appendChild(bazaZrd);

        Element bazaDoc = doc.createElement("BAZA_DOC_ID");
        bazaDoc.setTextContent(BAZA_DOC_ID);
        parentNode.appendChild(bazaDoc);
    }

    private void createCdataElement(Document doc, Element parent, String tagName, String text) {
        Element element = doc.createElement(tagName);
        if (text != null && !text.isEmpty()) {
            CDATASection cdata = doc.createCDATASection(text);
            element.appendChild(cdata);
        }
        parent.appendChild(element);
    }

    private void writeXml(Document doc, File file) throws Exception {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");

        DOMSource source = new DOMSource(doc);
        StreamResult result = new StreamResult(file);
        transformer.transform(source, result);
    }
}