package hr.com.port.ips.eracun.validation;

import org.w3c.dom.Document;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;

public final class XmlUtils {
    private XmlUtils() {}

    private static final Pattern XML_DECL_ENCODING =
            Pattern.compile("<\\?xml[^>]*encoding\\s*=\\s*['\\\"]([A-Za-z0-9_\\-]+)['\\\"][^>]*\\?>");

    /** Rezultat parsiranja s više dijagnostike. */
    public static final class ParseResult {
        public final Document doc;
        public final String encodingUsed;
        public final String diagnostics;

        public ParseResult(Document doc, String encodingUsed, String diagnostics) {
            this.doc = doc;
            this.encodingUsed = encodingUsed;
            this.diagnostics = diagnostics;
        }
    }

    /** Pročitaj prvih ~1 KB kao ISO-8859-1 (byte-preserving) da iščitaš XML deklaraciju. */
    private static String peekHeadAsLatin1(byte[] bytes, int max) {
        int n = Math.min(bytes.length, max);
        // ISO-8859-1 mapira 0x00–0xFF direktno -> ništa se ne gubi
        return new String(bytes, 0, n, StandardCharsets.ISO_8859_1);
    }

    private static String detectDeclaredEncoding(byte[] bytes) {
        String head = peekHeadAsLatin1(bytes, 2048);
        Matcher m = XML_DECL_ENCODING.matcher(head);
        return m.find() ? m.group(1) : null;
    }

    /** Vrati BOM-om sugerirani encoding, ako postoji. */
    private static Charset detectBom(byte[] bytes) {
        if (bytes.length >= 3 &&
            (bytes[0] & 0xFF) == 0xEF &&
            (bytes[1] & 0xFF) == 0xBB &&
            (bytes[2] & 0xFF) == 0xBF) {
            return StandardCharsets.UTF_8; // UTF-8 BOM
        }
        if (bytes.length >= 2) {
            int b0 = bytes[0] & 0xFF, b1 = bytes[1] & 0xFF;
            if (b0 == 0xFE && b1 == 0xFF) return Charset.forName("UTF-16BE");
            if (b0 == 0xFF && b1 == 0xFE) return Charset.forName("UTF-16LE");
        }
        return null;
    }

    /** Pokuša parsirati uz pametnu detekciju encodinga i vrati dijagnostiku. */
    public static ParseResult parse(byte[] xmlBytes) throws Exception {
        if (xmlBytes == null) throw new IllegalArgumentException("XML bytes = null");

        StringBuilder diag = new StringBuilder();
        Charset bom = detectBom(xmlBytes);
        String declared = detectDeclaredEncoding(xmlBytes);
        Charset chosen;

        if (bom != null) {
            chosen = bom;
            diag.append("BOM detected: ").append(bom.name()).append(". ");
        } else if (declared != null) {
            try {
                chosen = Charset.forName(declared);
            } catch (Exception e) {
                chosen = StandardCharsets.UTF_8; // fallback
                diag.append("Unknown declared encoding '").append(declared).append("', fallback UTF-8. ");
            }
            diag.append("Declared encoding: ").append(declared).append(". ");
        } else {
            chosen = StandardCharsets.UTF_8;
            diag.append("No BOM/declared encoding, assuming UTF-8. ");
        }

        try {
            // Parse preko InputStreamReader-a s *odabranim* charsetom.
            InputStream is = new ByteArrayInputStream(xmlBytes);
            Reader reader = new InputStreamReader(is, chosen);
            InputSource src = new InputSource(reader);

            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            // Sigurnosne postavke (XXE off)
            try {
                dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
                dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
                dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            } catch (ParserConfigurationException ignored) {}

            DocumentBuilder db = dbf.newDocumentBuilder();

            // ErrorHandler za hvatanje red/stupac poruka
            final StringBuilder saxDiag = new StringBuilder();
            db.setErrorHandler(new DefaultHandler() {
                private void append(String level, SAXParseException e) {
                    if (saxDiag.length() > 0) saxDiag.append(" | ");
                    saxDiag.append(level)
                           .append(" at line ").append(e.getLineNumber())
                           .append(", col ").append(e.getColumnNumber())
                           .append(": ").append(e.getMessage());
                }
                @Override public void error(SAXParseException e) { append("ERROR", e); }
                @Override public void fatalError(SAXParseException e) { append("FATAL", e); }
                @Override public void warning(SAXParseException e) { append("WARN", e); }
            });

            Document doc = db.parse(src);
            diag.append("Parsed OK with ").append(chosen.name()).append(".");
            if (saxDiag.length() > 0) diag.append(" SAX: ").append(saxDiag);
            return new ParseResult(doc, chosen.name(), diag.toString());

        } catch (SAXParseException e) {
            // Precizna lokacija
            diag.append(" SAX at line ").append(e.getLineNumber())
                .append(", col ").append(e.getColumnNumber())
                .append(": ").append(e.getMessage());
            throw new Exception(diag.toString(), e);
        } catch (Exception e) {
            // Dodatna heuristika: brzo skeniraj prvi “nevažeći” UTF-8 bajt i prijavi offset
            int badAt = firstInvalidUtf8Offset(xmlBytes);
            if (badAt >= 0 && (declared == null || "UTF-8".equalsIgnoreCase(declared))) {
                diag.append(" UTF-8 invalid sequence at byte offset ").append(badAt)
                    .append(" (hint: datoteka nije stvarno UTF-8).");
            }
            throw new Exception(diag.toString() + " Root: " + e.getMessage(), e);
        }
    }

    /** Vrati indeks prvog bajta gdje UTF-8 validacija pada, ili -1 ako sve izgleda ok (heuristika). */
    private static int firstInvalidUtf8Offset(byte[] a) {
        int i = 0, n = a.length;
        while (i < n) {
            int b = a[i] & 0xFF;
            if (b < 0x80) { i++; continue; }
            int need;
            if ((b >> 5) == 0b110) { need = 1; }
            else if ((b >> 4) == 0b1110) { need = 2; }
            else if ((b >> 3) == 0b11110) { need = 3; }
            else { return i; }
            if (i + need >= n) return i;
            for (int k = 1; k <= need; k++) {
                int c = a[i + k] & 0xFF;
                if ((c & 0xC0) != 0x80) return i + k; // continuation byte invalid
            }
            i += need + 1;
        }
        return -1;
    }
}