package de.assecutor.pdftool.zugferd;

import de.assecutor.pdftool.invoice.InvoiceMetadata;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.mustangproject.BankDetails;
import org.mustangproject.Contact;
import org.mustangproject.Invoice;
import org.mustangproject.Item;
import org.mustangproject.Product;
import org.mustangproject.SchemedID;
import org.mustangproject.TradeParty;
import org.mustangproject.ZUGFeRD.IZUGFeRDExporter;
import org.mustangproject.ZUGFeRD.Profiles;
import org.mustangproject.ZUGFeRD.ZUGFeRD2PullProvider;
import org.mustangproject.ZUGFeRD.ZUGFeRDExporterFromA1;
import org.mustangproject.ZUGFeRD.ZUGFeRDExporterFromPDFA;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Wandelt ein herkömmliches Rechnungs-PDF in eine rechtskonforme ZUGFeRD-Rechnung
 * (PDF/A-3 mit eingebettetem EN16931-XML, Profil EN 16931) um, validiert das
 * Ergebnis mit dem Mustang-Validator und verpackt PDF, Factur-X-XML und
 * Prüfbericht in eine ZIP-Datei.
 */
@Service
public class ZugferdService {

    private static final Logger log = LoggerFactory.getLogger(ZugferdService.class);

    private static final String PROFILE = "EN16931";
    private static final String PRODUCER = "Assecutor Data Service GmbH Invoice Tool";

    /** Business Process (BT-23), von PEPPOL-EN16931-R001 gefordert. */
    private static final String BUSINESS_PROCESS = "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0";

    /** EAS-Schema "EM" = E-Mail-Adresse für die elektronische Adresse (BT-34/BT-49). */
    private static final String EAS_EMAIL = "EM";

    private final ZugferdValidationService validationService;

    public ZugferdService(ZugferdValidationService validationService) {
        this.validationService = validationService;
    }

    public ZugferdResult createZugferdZip(byte[] sourcePdf, InvoiceMetadata metadata) {
        validatePdf(sourcePdf);
        sourcePdf = ensurePageResources(sourcePdf);
        Invoice invoice = buildInvoice(metadata);

        byte[] zugferdPdf = embedXmlIntoPdf(sourcePdf, invoice);
        byte[] facturXml = generateXml(invoice);

        String baseName = sanitizeFileName(metadata.invoiceNumber());
        String pdfName = baseName + "-zugferd.pdf";
        ZugferdValidationService.ValidationResult validation = validationService.validate(zugferdPdf, pdfName);

        byte[] zip = zip(
                pdfName, zugferdPdf,
                "factur-x.xml", facturXml,
                "validation-report.xml", validation.reportXml().getBytes(StandardCharsets.UTF_8));
        return new ZugferdResult(zip, baseName + ".zip", validation.valid(), validation.reportXml());
    }

    private void validatePdf(byte[] pdf) {
        if (pdf == null || pdf.length < 5
                || !"%PDF-".equals(new String(pdf, 0, 5, StandardCharsets.US_ASCII))) {
            throw new ZugferdConversionException("Die hochgeladene Datei ist kein gültiges PDF.");
        }
    }

    /**
     * Mustang setzt bei jeder Seite ein Resources-Dictionary voraus und stürzt
     * sonst mit einer NullPointerException ab. Seiten ohne Resources (z.B. aus
     * manchen PDF-Generatoren) erhalten deshalb vorab ein leeres Dictionary.
     */
    private byte[] ensurePageResources(byte[] pdf) {
        try (PDDocument doc = Loader.loadPDF(pdf)) {
            boolean changed = false;
            for (PDPage page : doc.getPages()) {
                if (page.getResources() == null) {
                    page.setResources(new PDResources());
                    changed = true;
                }
            }
            if (!changed) {
                return pdf;
            }
            try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                doc.save(out);
                return out.toByteArray();
            }
        } catch (IOException e) {
            throw new ZugferdConversionException("Das PDF konnte nicht gelesen werden: " + e.getMessage(), e);
        }
    }

    private Invoice buildInvoice(InvoiceMetadata m) {
        LocalDate delivery = m.deliveryDate() != null ? m.deliveryDate() : m.issueDate();
        LocalDate due = m.dueDate() != null ? m.dueDate() : m.issueDate().plusDays(14);
        String currency = m.currency() != null && !m.currency().isBlank() ? m.currency() : "EUR";

        Invoice invoice = new Invoice()
                .setDocumentName("Rechnung")
                .setBusinessProcessId(BUSINESS_PROCESS)
                .setNumber(m.invoiceNumber())
                .setIssueDate(toDate(m.issueDate()))
                .setDeliveryDate(toDate(delivery))
                .setDueDate(toDate(due))
                .setCurrency(currency)
                .setSender(toSender(m))
                .setRecipient(toTradeParty(m.recipient()));

        if (m.paymentTerms() != null && !m.paymentTerms().isBlank()) {
            invoice.setPaymentTermDescription(m.paymentTerms());
        }
        if (m.buyerReference() != null && !m.buyerReference().isBlank()) {
            // Käuferreferenz / Leitweg-ID (BT-10)
            invoice.setReferenceNumber(m.buyerReference().trim());
        }

        for (InvoiceMetadata.LineItem li : m.items()) {
            // Einheit "C62" = Stück (UN/ECE Recommendation 20)
            Product product = new Product(li.description(), "", "C62", li.vatPercent());
            invoice.addItem(new Item(product, li.unitPriceNet(), li.quantity()));
        }
        return invoice;
    }

    /** Rechnungssteller inkl. Verkäufer-Kontakt (BG-6) und Zahlungsverbindung (BG-16). */
    private TradeParty toSender(InvoiceMetadata m) {
        InvoiceMetadata.Party p = m.sender();
        TradeParty party = toTradeParty(p);
        party.setContact(new Contact(p.name(), p.phone(), p.email()));
        if (m.iban() != null && !m.iban().isBlank()) {
            String iban = m.iban().replaceAll("\\s", "").toUpperCase();
            party.addBankDetails(m.bic() != null && !m.bic().isBlank()
                    ? new BankDetails(iban, m.bic().trim().toUpperCase())
                    : new BankDetails(iban));
        }
        return party;
    }

    private TradeParty toTradeParty(InvoiceMetadata.Party p) {
        TradeParty party = new TradeParty(p.name(), p.street(), p.zip(), p.city(), p.countryCode());
        if (p.vatId() != null && !p.vatId().isBlank()) {
            party.addVATID(p.vatId());
        }
        if (p.email() != null && !p.email().isBlank()) {
            party.setEmail(p.email());
            party.addUriUniversalCommunicationID(new SchemedID(EAS_EMAIL, p.email()));
        }
        return party;
    }

    private byte[] embedXmlIntoPdf(byte[] sourcePdf, Invoice invoice) {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream();
             IZUGFeRDExporter exporter = loadExporter(sourcePdf)) {
            exporter.setProducer(PRODUCER)
                    .setCreator(PRODUCER)
                    .setProfile(Profiles.getByName(PROFILE))
                    .setTransaction(invoice);
            exporter.export(out);
            return out.toByteArray();
        } catch (IOException | RuntimeException e) {
            // Mustang wirft bei problematischen PDFs auch unchecked Exceptions —
            // ohne diesen Catch würde daraus ein HTTP 500 statt einer Fehlermeldung.
            throw new ZugferdConversionException(
                    "Das PDF konnte nicht in eine ZUGFeRD-Rechnung umgewandelt werden: " + e.getMessage(), e);
        }
    }

    /**
     * Versucht zuerst die automatische PDF/A-Erkennung; ist das PDF kein PDF/A
     * (der Normalfall bei ERP-Ausdrucken), wird es tolerant als PDF/A-1
     * interpretiert und nach PDF/A-3 konvertiert.
     */
    private IZUGFeRDExporter loadExporter(byte[] sourcePdf) throws IOException {
        ZUGFeRDExporterFromPDFA exporter = new ZUGFeRDExporterFromPDFA();
        try {
            exporter.load(new ByteArrayInputStream(sourcePdf));
            return exporter;
        } catch (IOException | IllegalArgumentException e) {
            // Nicht exporter.close(): vor erfolgreichem load() hält er keine Ressourcen
            // und close() würde eine RuntimeException werfen.
            log.info("Eingabe ist kein PDF/A, konvertiere tolerant: {}", e.getMessage());
            ZUGFeRDExporterFromA1 fallback = new ZUGFeRDExporterFromA1();
            fallback.ignorePDFAErrors();
            fallback.load(new ByteArrayInputStream(sourcePdf));
            return fallback;
        }
    }

    private byte[] generateXml(Invoice invoice) {
        ZUGFeRD2PullProvider provider = new ZUGFeRD2PullProvider();
        provider.setProfile(Profiles.getByName(PROFILE));
        provider.generateXML(invoice);
        return provider.getXML();
    }

    /**
     * Packt die übergebenen Einträge (abwechselnd Dateiname als String und Inhalt
     * als byte[]) in eine ZIP-Datei.
     */
    private byte[] zip(Object... namesAndContents) {
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
             ZipOutputStream zos = new ZipOutputStream(bos)) {
            for (int i = 0; i < namesAndContents.length; i += 2) {
                zos.putNextEntry(new ZipEntry((String) namesAndContents[i]));
                zos.write((byte[]) namesAndContents[i + 1]);
                zos.closeEntry();
            }
            zos.finish();
            return bos.toByteArray();
        } catch (IOException e) {
            throw new ZugferdConversionException("ZIP-Datei konnte nicht erstellt werden.", e);
        }
    }

    private static String sanitizeFileName(String name) {
        String cleaned = name.replaceAll("[^A-Za-z0-9._-]", "_");
        return cleaned.isBlank() ? "rechnung" : cleaned;
    }

    private static Date toDate(LocalDate date) {
        return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
}
