<?php

declare( strict_types=1 );

namespace Brightfern\Inbound;

use DOMDocument;
use DOMXPath;

/**
 * Parses an inbound Peppol BIS UBL Invoice or CreditNote into an
 * {@see InboundSummary}. Pure (no WordPress) so it is unit-testable and reusable.
 *
 * Inbound documents come from arbitrary senders' systems, so this is forgiving:
 * it reads the well-known BT-* locations and leaves fields blank when absent
 * rather than failing. It handles both the Invoice-2 and CreditNote-2 roots.
 */
final class UblInboundParser {

	private const NS_CAC = 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2';
	private const NS_CBC = 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2';

	public function parse( string $xml ): InboundSummary {
		$summary = new InboundSummary();

		if ( '' === trim( $xml ) ) {
			// DOMDocument::loadXML('') throws a ValueError on PHP 8 rather than
			// returning false — the `! $ok` guard below never runs for this
			// specific input, so an empty payload used to propagate as an
			// uncaught exception instead of the graceful "empty summary" every
			// other malformed document gets. Reachable in practice: MockProvider
			// (the DEFAULT provider) accepts any string, including '', as a
			// document's xml; WebhookController catches \Throwable and returns
			// 500 either way, but 500 contradicts its own "always 200 for
			// accepted-but-ignored events so the provider doesn't retry forever"
			// design intent — an empty/malformed payload should be ignored, not
			// treated as a reason for the sender to retry indefinitely.
			return $summary;
		}

		$doc  = new DOMDocument();
		$prev = libxml_use_internal_errors( true );
		// LIBXML_NONET: this XML comes from arbitrary senders' systems, so forbid
		// any network fetch (external DTD/entity) during parsing outright instead
		// of relying on libxml's version-dependent defaults.
		$ok = $doc->loadXML( $xml, LIBXML_NONET );
		libxml_clear_errors();
		libxml_use_internal_errors( $prev );
		if ( ! $ok || null === $doc->documentElement ) {
			return $summary;
		}

		$root                  = $doc->documentElement->localName;
		$summary->documentType = ( 'CreditNote' === $root ) ? 'creditnote' : 'invoice';

		$xp = new DOMXPath( $doc );
		$xp->registerNamespace( 'cac', self::NS_CAC );
		$xp->registerNamespace( 'cbc', self::NS_CBC );
		// Register the default (root) namespace under "d" so we can query the root's children.
		$rootNs = $doc->documentElement->namespaceURI;
		if ( is_string( $rootNs ) ) {
			$xp->registerNamespace( 'd', $rootNs );
		}

		$summary->invoiceNumber  = $this->first( $xp, '/d:*/cbc:ID' );
		$summary->issueDate      = $this->first( $xp, '/d:*/cbc:IssueDate' );
		$summary->currency       = $this->first( $xp, '/d:*/cbc:DocumentCurrencyCode' );
		$summary->buyerReference = $this->first( $xp, '/d:*/cbc:BuyerReference' );
		$summary->orderReference = $this->first( $xp, '/d:*/cac:OrderReference/cbc:ID' );

		$summary->payableAmount = (float) $this->first( $xp, '/d:*/cac:LegalMonetaryTotal/cbc:PayableAmount' );

		$summary->supplierName     = $this->first( $xp, '/d:*/cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName' );
		$summary->supplierVat      = $this->first( $xp, '/d:*/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID' );
		$summary->supplierEndpoint = $this->first( $xp, '/d:*/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID' );

		$summary->buyerName = $this->first( $xp, '/d:*/cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName' );
		$summary->buyerVat  = $this->first( $xp, '/d:*/cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID' );

		if ( '' === $summary->supplierName ) {
			$summary->supplierName = $this->first( $xp, '/d:*/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name' );
		}

		return $summary;
	}

	private function first( DOMXPath $xp, string $query ): string {
		$nodes = $xp->query( $query );
		return ( $nodes && $nodes->length ) ? trim( $nodes->item( 0 )->textContent ) : '';
	}
}
