<?php
/**
 * Factur-X / CII / UBL XML generation via horstoeko/zugferd.
 *
 * @package FacturX_For_WooCommerce
 */

defined( 'ABSPATH' ) || exit;

use horstoeko\zugferd\ZugferdDocumentBuilder;
use horstoeko\zugferd\ZugferdProfiles;
use horstoeko\zugferd\codelists\ZugferdInvoiceType;
use horstoeko\zugferd\codelists\ZugferdCurrencyCodes;
use horstoeko\zugferd\codelists\ZugferdUnitCodes;
use horstoeko\zugferd\codelists\ZugferdCountryCodes;
use horstoeko\zugferd\codelists\ZugferdSchemeIdentifiers;
use horstoeko\zugferd\codelists\ZugferdReferenceCodeQualifiers;

class GOFACTOX_XML_Generator {

    /**
     * Generate XML for an invoice.
     *
     * @param array  $data   Invoice data from GOFACTOX_Invoice::get_invoice_data().
     * @param string $format 'facturx', 'cii', or 'ubl'.
     * @return string XML content.
     */
    public static function generate( array $data, string $format = 'facturx' ): string {
        $profile = self::get_profile( $format );
        $builder = ZugferdDocumentBuilder::createNew( $profile );

        self::set_document_info( $builder, $data );
        self::set_seller( $builder, $data['seller'] );
        self::set_buyer( $builder, $data['buyer'] );
        self::set_payment_terms( $builder, $data );
        self::add_line_items( $builder, $data['lines'], $data['currency'] );
        self::set_totals( $builder, $data );

        /**
         * Filter the final XML string before it is returned (or embedded in the PDF).
         *
         * @param string $xml    The generated XML.
         * @param array  $data   The invoice/credit note data.
         * @param string $format Format key ("facturx", "facturx-comfort", "cii", "ubl").
         */
        return apply_filters( 'gofactox_xml_output', $builder->getContent(), $data, $format );
    }

    /**
     * Generate and save XML to a file.
     *
     * @return string File path of the generated XML.
     */
    public static function generate_to_file( array $data, string $format = 'facturx' ): string {
        $xml = self::generate( $data, $format );

        $upload_dir = wp_upload_dir();
        $date       = $data['invoice_date'];
        $year       = substr( $date, 0, 4 );
        $month      = substr( $date, 5, 2 );
        $dir        = $upload_dir['basedir'] . '/wfx-invoices/' . $year . '/' . $month;

        wp_mkdir_p( $dir );

        $filename = 'factur-x-' . sanitize_file_name( $data['invoice_number'] ) . '.xml';
        $filepath = $dir . '/' . $filename;

        file_put_contents( $filepath, $xml );

        return $filepath;
    }

    /**
     * Get the horstoeko profile constant for the format.
     *
     * facturx-comfort upgrades the Basic profile to EN 16931 (Comfort), exposing
     * structured fields (allowances, charges, line-level references) required by
     * sophisticated B2B buyers.
     */
    private static function get_profile( string $format ): int {
        return match ( $format ) {
            'ubl'              => ZugferdProfiles::PROFILE_EN16931,
            'cii'              => ZugferdProfiles::PROFILE_EN16931,
            'facturx-comfort'  => ZugferdProfiles::PROFILE_EN16931,
            default            => ZugferdProfiles::PROFILE_BASIC,
        };
    }

    /**
     * Set document-level information.
     *
     * For credit notes (is_credit_note = true), the document type is set to 381
     * (CREDITNOTE per UN/CEFACT codelist) and a reference to the original invoice
     * is added per EN 16931 BT-25/BT-26.
     */
    private static function set_document_info( ZugferdDocumentBuilder $builder, array $data ): void {
        $is_credit_note = ! empty( $data['is_credit_note'] );
        $type_code      = $is_credit_note ? ZugferdInvoiceType::CREDITNOTE : ZugferdInvoiceType::INVOICE;

        $builder->setDocumentInformation(
            $data['invoice_number'],
            $type_code,
            \DateTime::createFromFormat( 'Y-m-d', $data['invoice_date'] ),
            $data['currency']
        );

        // Cadre de facturation (Chorus Pro invoicing framework), EN 16931 BT-23
        // (Business process type). Defaults to "A1" (standard supplier invoice). It is
        // harmless on ordinary B2B/B2C invoices and is what Chorus Pro expects when an
        // invoice is deposited to a Public Administration (B2G). An empty value (the
        // merchant opted out) leaves BT-23 unset.
        if ( ! empty( $data['cadre_de_facturation'] ) ) {
            $builder->setDocumentBusinessProcess( (string) $data['cadre_de_facturation'] );
        }

        // Operation category (catégorie de l'opération: goods / services / mixed), a new
        // mandatory mention of the French e-invoicing reform. EN 16931 has no dedicated
        // core term for it, so we surface it as a document note (BT-22). The same mention
        // is rendered visibly on the PDF (see the invoice/credit-note templates).
        if ( ! empty( $data['operation_category']['label'] ) ) {
            $builder->addDocumentNote(
                sprintf(
                    /* translators: %s: operation category (goods / services / mixed) */
                    __( 'Operation category: %s', 'gofactox-e-invoicing-for-woocommerce' ),
                    $data['operation_category']['label']
                )
            );
        }

        // VAT-on-debits mention (option pour le paiement de la TVA d'après les débits),
        // a French legal mention when the seller has opted for it. No dedicated EN 16931
        // term exists, so it is carried as a document note (BT-22) and shown on the PDF.
        if ( ! empty( $data['seller']['vat_on_debits'] ) ) {
            $builder->addDocumentNote(
                __( 'VAT paid on the basis of debits (option for payment of VAT on debits).', 'gofactox-e-invoicing-for-woocommerce' )
            );
        }

        if ( ! empty( $data['order_number'] ) ) {
            $builder->setDocumentBuyerOrderReferencedDocument( (string) $data['order_number'] );
        }

        // Buyer reference (EN 16931 BT-10): the customer's PO number, if provided at checkout.
        if ( ! empty( $data['buyer_reference'] ) ) {
            $builder->setDocumentBuyerReference( (string) $data['buyer_reference'] );
        }

        // Credit notes must reference the original invoice (BT-25 + BT-26).
        if ( $is_credit_note && ! empty( $data['original_invoice']['number'] ) ) {
            $original_date = null;
            if ( ! empty( $data['original_invoice']['date'] ) ) {
                $original_date = \DateTime::createFromFormat( 'Y-m-d', $data['original_invoice']['date'] ) ?: null;
            }
            $builder->setDocumentInvoiceReferencedDocument(
                (string) $data['original_invoice']['number'],
                null,
                $original_date
            );
        }
    }

    /**
     * Set seller (vendor) information.
     */
    private static function set_seller( ZugferdDocumentBuilder $builder, array $seller ): void {
        $builder->setDocumentSeller( $seller['company_name'], '' );
        $builder->setDocumentSellerAddress(
            $seller['address_line1'],
            $seller['address_line2'],
            '',
            $seller['postal_code'],
            $seller['city'],
            $seller['country_code']
        );

        // SIRET as seller ID (scheme 0002 = SIRET in France).
        if ( ! empty( $seller['siret'] ) ) {
            $builder->addDocumentSellerGlobalId( $seller['siret'], '0002' );
        }

        // VAT number.
        if ( ! empty( $seller['vat_number'] ) ) {
            $builder->addDocumentSellerTaxRegistration( 'VA', $seller['vat_number'] );
        } elseif ( $seller['vat_exempt'] ) {
            // For VAT-exempt sellers, we still need to indicate this.
            $builder->addDocumentSellerTaxRegistration( 'FC', 'Article 293 B du CGI' );
        }

        // Legal registration (RCS).
        if ( ! empty( $seller['rcs_city'] ) && ! empty( $seller['siret'] ) ) {
            $siren = substr( $seller['siret'], 0, 9 );
            $builder->setDocumentSellerLegalOrganisation( $siren, '0002', $seller['company_name'] );
        }

        // Seller electronic address (EN 16931 BT-34). Required by several PDPs (e.g.
        // Chorus Pro, SuperPDP, Pennylane) to send back invoice receipt acknowledgments
        // and rejection notices.
        if ( ! empty( $seller['email'] ) ) {
            $builder->setDocumentSellerCommunication( 'EM', $seller['email'] );
        }
    }

    /**
     * Set buyer (customer) information.
     */
    private static function set_buyer( ZugferdDocumentBuilder $builder, array $buyer ): void {
        $name = ! empty( $buyer['company'] ) ? $buyer['company'] : $buyer['name'];

        $builder->setDocumentBuyer( $name, '' );
        $builder->setDocumentBuyerAddress(
            $buyer['address_line1'],
            $buyer['address_line2'],
            '',
            $buyer['postal_code'],
            $buyer['city'],
            $buyer['country_code']
        );

        // Buyer VAT number (if B2B).
        if ( ! empty( $buyer['vat_number'] ) ) {
            $builder->addDocumentBuyerTaxRegistration( 'VA', $buyer['vat_number'] );
        }

        // Buyer SIRET.
        if ( ! empty( $buyer['siret'] ) ) {
            $builder->addDocumentBuyerGlobalId( $buyer['siret'], '0002' );
        }

        // Contact info.
        if ( ! empty( $buyer['email'] ) ) {
            $builder->setDocumentBuyerContact(
                $buyer['name'],
                '',
                $buyer['phone'] ?? '',
                '',
                $buyer['email']
            );

            // Buyer electronic address (EN 16931 BT-49). Same purpose as BT-34 but
            // for the buyer side: where the issuing PDP should send notifications.
            $builder->setDocumentBuyerCommunication( 'EM', $buyer['email'] );
        }
    }

    /**
     * Set payment terms, due date, and payment means (IBAN/BIC).
     */
    private static function set_payment_terms( ZugferdDocumentBuilder $builder, array $data ): void {
        // Resolve configurable payment terms (label + due date).
        $terms = GOFACTOX_Payment_Terms::resolve( $data['invoice_date'] );
        $builder->addDocumentPaymentTerm( $terms['label'], $terms['due_date'] );

        // Structured SEPA Credit Transfer payment means in XML (BG-17) is a Pro feature.
        // The plain-text IBAN/BIC block on the PDF remains free; only the structured XML
        // mapping is gated, in line with the competitive landscape (WebToffee gates the
        // XML mapping while keeping the PDF display free).
        $seller = $data['seller'] ?? [];
        if ( ! empty( $seller['iban'] ) && class_exists( 'GOFACTOX_License' ) && GOFACTOX_License::is_pro() ) {
            $iban = preg_replace( '/\s+/', '', (string) $seller['iban'] );
            $bic  = preg_replace( '/\s+/', '', (string) ( $seller['bic'] ?? '' ) );

            $builder->addDocumentPaymentMeanToCreditTransfer(
                $iban,
                $seller['bank_name'] ?: $seller['company_name'] ?? '',
                null,
                $bic ?: null
            );
        }

        // Deliver-to address (EN 16931 BG-15 / BT-70..BT-80). Emitted only when the
        // delivery address differs from the buyer's billing address, which the French
        // e-invoicing reform makes a mandatory mention.
        if ( ! empty( $data['delivery_address'] ) ) {
            $ship = $data['delivery_address'];
            $builder->setDocumentShipTo( (string) ( $ship['name'] ?? '' ), '' );
            $builder->setDocumentShipToAddress(
                (string) ( $ship['address_line1'] ?? '' ),
                (string) ( $ship['address_line2'] ?? '' ),
                '',
                (string) ( $ship['postal_code'] ?? '' ),
                (string) ( $ship['city'] ?? '' ),
                (string) ( $ship['country_code'] ?? '' )
            );
        }

        // Delivery date.
        if ( ! empty( $data['delivery_date'] ) ) {
            $builder->setDocumentSupplyChainEvent(
                \DateTime::createFromFormat( 'Y-m-d', $data['delivery_date'] )
            );
        }

        // Billing period (EN 16931 BT-73/BT-74) for subscription/recurring invoices.
        if ( ! empty( $data['billing_period']['start'] ) && ! empty( $data['billing_period']['end'] ) ) {
            $start = \DateTime::createFromFormat( 'Y-m-d', $data['billing_period']['start'] );
            $end   = \DateTime::createFromFormat( 'Y-m-d', $data['billing_period']['end'] );
            if ( $start && $end ) {
                $builder->setDocumentBillingPeriod( $start, $end, null );
            }
        }
    }

    /**
     * Add line items.
     */
    private static function add_line_items( ZugferdDocumentBuilder $builder, array $lines, string $currency ): void {
        $line_id = 1;

        foreach ( $lines as $line ) {
            $builder->addNewPosition( (string) $line_id );

            $builder->setDocumentPositionProductDetails(
                $line['name'],
                '',
                $line['sku'] ?? ''
            );

            $builder->setDocumentPositionNetPrice(
                round( $line['unit_price'], 2 )
            );

            $builder->setDocumentPositionQuantity(
                $line['quantity'],
                ZugferdUnitCodes::REC20_ONE
            );

            // Tax category.
            $tax_rate = round( $line['tax_rate'], 2 );
            if ( $tax_rate > 0 ) {
                $builder->addDocumentPositionTax( 'S', 'VAT', $tax_rate );
            } else {
                // Zero-rated (exempt, intra-EU, or franchise).
                $builder->addDocumentPositionTax( 'E', 'VAT', 0.0 );
            }

            $builder->setDocumentPositionLineSummation(
                round( $line['total_ht'], 2 )
            );

            $line_id++;
        }
    }

    /**
     * Set document totals, tax summary, and document-level allowances (discounts).
     */
    private static function set_totals( ZugferdDocumentBuilder $builder, array $data ): void {
        // Document-level allowances (coupons/discounts) and charges, before totals.
        // EN 16931 BG-20 (allowances) + BG-21 (charges).
        $allowance_total = 0.0;
        foreach ( $data['allowances'] ?? [] as $allowance ) {
            $amount = (float) ( $allowance['amount'] ?? 0.0 );
            if ( $amount <= 0 ) {
                continue;
            }
            $tax_rate = (float) ( $allowance['tax_rate'] ?? 0.0 );
            $tax_cat  = $tax_rate > 0 ? 'S' : 'E';
            $builder->addDocumentAllowanceCharge(
                round( $amount, 2 ),
                false,            // isCharge: false = allowance (discount)
                $tax_cat,
                'VAT',
                $tax_rate,
                null,
                null,
                null,
                null,
                null,
                '95',             // Reason code 95 = Discount
                (string) ( $allowance['reason'] ?? __( 'Discount', 'gofactox-e-invoicing-for-woocommerce' ) )
            );
            $allowance_total += $amount;
        }

        // Tax breakdown by rate.
        foreach ( $data['tax_totals'] as $tax ) {
            $rate = round( $tax['rate'], 2 );
            if ( $rate > 0 ) {
                $builder->addDocumentTax( 'S', 'VAT', $tax['base'], $tax['amount'], $rate );
            } else {
                $builder->addDocumentTax( 'E', 'VAT', $tax['base'], 0.0, 0.0 );
            }
        }

        // If no tax totals but we have amounts.
        if ( empty( $data['tax_totals'] ) && $data['total_ht'] > 0 ) {
            $builder->addDocumentTax( 'E', 'VAT', $data['total_ht'], 0.0, 0.0 );
        }

        // Document summation.
        // Line total = sum of lines (gross), tax basis = line total - allowances.
        $line_total = (float) $data['total_ht'] + $allowance_total;
        $builder->setDocumentSummation(
            $data['total_ttc'],   // Grand total.
            $data['total_ttc'],   // Due payable amount.
            round( $line_total, 2 ), // Line total amount (pre-allowance).
            0.0,                  // Charge total amount.
            round( $allowance_total, 2 ), // Allowance total amount.
            $data['total_ht'],    // Tax basis total amount (post-allowance).
            $data['total_tax'],   // Tax total amount.
        );
    }

    /**
     * Validate generated XML against XSD schema.
     *
     * @param string $xml The XML content.
     * @return array [ 'valid' => bool, 'errors' => string[] ]
     */
    public static function validate_xml( string $xml ): array {
        $doc = new \DOMDocument();
        $doc->loadXML( $xml );

        // Use libxml internal errors.
        $prev = libxml_use_internal_errors( true );
        libxml_clear_errors();

        // horstoeko/zugferd validates internally, but we double-check.
        $errors = [];
        foreach ( libxml_get_errors() as $error ) {
            $errors[] = trim( $error->message ) . ' (line ' . $error->line . ')';
        }

        libxml_use_internal_errors( $prev );

        return [
            'valid'  => empty( $errors ),
            'errors' => $errors,
        ];
    }
}
