<?php
/**
 * Parser XML FA(3) → tablica zgodna z payload_json używanym wewnątrz pluginu.
 *
 * Minimum: wyciąga Naglowek (numer/daty), Podmiot1/2 (sprzedawca/nabywca),
 * pozycje FaWiersz[], podsumowanie VAT, płatność i markery (Adnotacje).
 *
 * Nie waliduje XSD — przyjmuje, że MF zwrócił poprawny dokument.
 *
 * @package KSEF\WordPress\V2
 */

declare(strict_types=1);

namespace KSEF\WordPress\V2;

class Fa3_Parser {

	private const FA_NS = 'http://crd.gov.pl/wzor/2025/06/25/13775/';

	/**
	 * @return array<string,mixed>|null  null gdy XML nie jest fakturą FA(3)
	 */
	public static function parse( string $xml ): ?array {
		$xml = trim( $xml );
		if ( $xml === '' ) {
			return null;
		}
		libxml_use_internal_errors( true );
		$dom = new \DOMDocument();
		if ( ! $dom->loadXML( $xml ) ) {
			return null;
		}
		$root = $dom->documentElement;
		if ( ! $root || 'Faktura' !== $root->localName ) {
			return null;
		}
		$xp = new \DOMXPath( $dom );
		$xp->registerNamespace( 'fa', self::FA_NS );

		$out = [
			'invoice_number'   => self::str( $xp, '//fa:Fa/fa:P_2' ),
			'issue_date'       => self::str( $xp, '//fa:Fa/fa:P_1' ),
			'sale_date'        => self::str( $xp, '//fa:Fa/fa:P_6' ) ?: self::str( $xp, '//fa:Fa/fa:OkresFa/fa:P_6_Do' ),
			'period_from'      => self::str( $xp, '//fa:Fa/fa:OkresFa/fa:P_6_Od' ),
			'period_to'        => self::str( $xp, '//fa:Fa/fa:OkresFa/fa:P_6_Do' ),
			'currency'         => self::str( $xp, '//fa:Fa/fa:KodWaluty' ) ?: 'PLN',
			'currency_payment' => self::str( $xp, '//fa:Fa/fa:KodWalutyPlatnosci' ),
			'kurs_waluty'      => self::str( $xp, '//fa:Fa/fa:KursWaluty' ),
			'issue_place'      => self::str( $xp, '//fa:Fa/fa:P_1M' ),
			'rodzaj_faktury'   => self::str( $xp, '//fa:Fa/fa:RodzajFaktury' ) ?: 'VAT',
			'data_wplywu'      => self::str( $xp, '//fa:Fa/fa:DataWplywuFaktury' ),
			'p_4a'             => self::str( $xp, '//fa:Fa/fa:P_4A' ),
			'p_4b'             => self::str( $xp, '//fa:Fa/fa:P_4B' ),
			'adnotacja_cit'    => self::str( $xp, '//fa:Fa/fa:AdnotacjaPodlegaCit' ),
			'total_net'        => self::num( $xp, '//fa:Fa/fa:P_13_1' ) + self::num( $xp, '//fa:Fa/fa:P_13_2' ) + self::num( $xp, '//fa:Fa/fa:P_13_3' ) + self::num( $xp, '//fa:Fa/fa:P_13_5' ) + self::num( $xp, '//fa:Fa/fa:P_13_6_1' ) + self::num( $xp, '//fa:Fa/fa:P_13_6_2' ) + self::num( $xp, '//fa:Fa/fa:P_13_7' ),
			'total_vat'        => self::num( $xp, '//fa:Fa/fa:P_14_1' ) + self::num( $xp, '//fa:Fa/fa:P_14_2' ) + self::num( $xp, '//fa:Fa/fa:P_14_3' ) + self::num( $xp, '//fa:Fa/fa:P_14_4' ) + self::num( $xp, '//fa:Fa/fa:P_14_5' ),
			'total_gross'      => self::num( $xp, '//fa:Fa/fa:P_15' ),
		];

		// Podmiot1 (sprzedawca)
		$out['seller'] = self::parse_party( $xp, '//fa:Podmiot1' );
		// Podmiot2 (nabywca)
		$buyer = self::parse_party( $xp, '//fa:Podmiot2' );
		// is_b2c gdy BrakID
		$out['is_b2c'] = ! empty( $buyer['brak_id'] );
		$out['buyer']  = $buyer;

		// Pozycje — pełen zestaw FA(3) FaWiersz
		$items = [];
		foreach ( $xp->query( '//fa:Fa/fa:FaWiersz' ) ?: [] as $w ) {
			$net   = (float) ( self::child( $xp, $w, 'fa:P_11' ) ?: 0 );
			$gross = (float) ( self::child( $xp, $w, 'fa:P_11A' ) ?: 0 );
			// Zbierz WSZYSTKIE GTU/IDZewnetrzny per wiersz
			$gtus = [];
			foreach ( $xp->query( './/fa:GTU', $w ) ?: [] as $gn ) {
				$gtus[] = trim( $gn->textContent );
			}
			$items[] = [
				'lp'               => (int) ( self::child( $xp, $w, 'fa:NrWierszaFa' ) ?: 0 ),
				'uu_id'            => self::child( $xp, $w, 'fa:UU_ID' ),
				'delivery_date'    => self::child( $xp, $w, 'fa:P_6A' ),
				'description'      => self::child( $xp, $w, 'fa:P_7' ),
				'pkwiu'            => self::child( $xp, $w, 'fa:P_7A' ),
				'cn'               => self::child( $xp, $w, 'fa:P_7B' ),
				'pkob'             => self::child( $xp, $w, 'fa:P_7C' ),
				'p_7d'             => self::child( $xp, $w, 'fa:P_7D' ),
				'unit'             => self::child( $xp, $w, 'fa:P_8A' ) ?: 'szt.',
				'quantity'         => (float) ( self::child( $xp, $w, 'fa:P_8B' ) ?: 1 ),
				'unit_price_net'   => (float) ( self::child( $xp, $w, 'fa:P_9A' ) ?: 0 ),
				'unit_price_gross' => (float) ( self::child( $xp, $w, 'fa:P_9B' ) ?: 0 ),
				'discount'         => (float) ( self::child( $xp, $w, 'fa:P_10' ) ?: 0 ),
				'line_net'         => $net,
				'line_gross'       => $gross ?: $net,
				'line_correction_ref' => self::child( $xp, $w, 'fa:P_11Nr' ),
				'vat_rate'         => self::vat_label_to_value( self::child( $xp, $w, 'fa:P_12' ) ),
				'vat_label'        => self::child( $xp, $w, 'fa:P_12' ),
				'vat_kind'         => self::child( $xp, $w, 'fa:P_12_XII' ),
				'vat_procedura'    => self::child( $xp, $w, 'fa:P_12_Procedura' ),
				'gtu'              => implode( ', ', array_filter( $gtus ) ),
				'gtu_codes'        => $gtus,
				'discount_pct'     => 0.0,
				'kurs_waluty'      => (float) ( self::child( $xp, $w, 'fa:KursWaluty' ) ?: 0 ),
				'id_zewnetrzny'    => self::child( $xp, $w, 'fa:IDZewnetrzny' ),
			];
		}
		$out['items'] = $items;

		// VAT summary
		$out['vat_summary'] = self::build_vat_summary( $xp );

		// Adnotacje → markers
		$out['markers']  = self::parse_markers( $xp );
		// adnotacje raw values (P_16..P_23 jako '1' lub '2')
		$out['adnotacje'] = self::parse_adnotacje_raw( $xp );

		// Płatność — pełen zestaw z FA(3): Zaplacono (1=tak całość, 2=nie),
		// ZaplaconoData, KwotaZaplacona, NaleznoscPozostala (częściowo).
		$zaplacono_flag  = self::str( $xp, '//fa:Fa/fa:Platnosc/fa:Zaplacono' );
		$zaplacono_data  = self::str( $xp, '//fa:Fa/fa:Platnosc/fa:ZaplaconoData' );
		$kwota_zaplacona = (float) ( self::str( $xp, '//fa:Fa/fa:Platnosc/fa:KwotaZaplacona' ) ?: 0 );
		$naleznosc_poz   = (float) ( self::str( $xp, '//fa:Fa/fa:Platnosc/fa:NaleznoscPozostala' ) ?: 0 );

		$paid_amount = 0.0;
		$payment_status = 'unpaid';
		if ( '1' === $zaplacono_flag ) {
			$paid_amount    = (float) $out['total_gross'];
			$payment_status = 'paid';
		} elseif ( $kwota_zaplacona > 0 ) {
			$paid_amount    = $kwota_zaplacona;
			$payment_status = $kwota_zaplacona >= (float) $out['total_gross'] ? 'paid' : 'partial';
		}

		$out['payment'] = [
			'form'                  => self::str( $xp, '//fa:Fa/fa:Platnosc/fa:FormaPlatnosci' ) ?: '6',
			'due_date'              => self::str( $xp, '//fa:Fa/fa:Platnosc/fa:TerminPlatnosci/fa:Termin' ),
			'due_date_desc'         => self::str( $xp, '//fa:Fa/fa:Platnosc/fa:TerminPlatnosci/fa:TerminOpis' ),
			'zaplacono_flag'        => $zaplacono_flag,
			'zaplacono_data'        => $zaplacono_data,
			'kwota_zaplacona'       => $kwota_zaplacona,
			'naleznosc_pozostala'   => $naleznosc_poz,
			'paid'                  => $paid_amount,
			'payment_status'        => $payment_status,
			'skonto'                => self::str( $xp, '//fa:Fa/fa:Platnosc/fa:SkontoUmowne' ),
			'skonto_opis'           => self::str( $xp, '//fa:Fa/fa:Platnosc/fa:SkontoUmowneOpis' ),
		];

		// Wszystkie rachunki bankowe z Platnosc/RachunekBankowy (może być >1)
		$banks_out = [];
		foreach ( $xp->query( '//fa:Fa/fa:Platnosc/fa:RachunekBankowy' ) ?: [] as $rbn ) {
			$banks_out[] = [
				'account_number'     => self::child( $xp, $rbn, 'fa:NrRB' ),
				'bank_name'          => self::child( $xp, $rbn, 'fa:NazwaBanku' ),
				'swift'              => self::child( $xp, $rbn, 'fa:SWIFT' ),
				'description'        => self::child( $xp, $rbn, 'fa:OpisRachunku' ),
				'foreign'            => self::child( $xp, $rbn, 'fa:OdRachunkuZagranicznego' ),
				'currency'           => $out['currency'],
			];
		}
		$out['bank_accounts'] = $banks_out;
		if ( ! empty( $banks_out ) ) {
			$out['bank_account'] = $banks_out[0];   // backward-compat z poprzednią wersją
		}

		// DodatkowyOpis → ksef_custom_notes (klucz/wartość)
		$kn = [];
		foreach ( $xp->query( '//fa:Fa/fa:DodatkowyOpis' ) ?: [] as $do ) {
			$kn[] = [
				'key'   => self::child( $xp, $do, 'fa:Klucz' ),
				'value' => self::child( $xp, $do, 'fa:Wartosc' ),
			];
		}
		$out['ksef_custom_notes'] = $kn;

		// DokumentyPowiazane
		$rel = [];
		$ord = $xp->query( '//fa:Fa/fa:DokumentyPowiazane/fa:ZamowienieZakupu' );
		if ( $ord && $ord->length > 0 ) {
			$on = $ord->item( 0 );
			$rel['order'] = [
				'number' => self::child( $xp, $on, 'fa:NrZamowieniaZakupu' ),
				'date'   => self::child( $xp, $on, 'fa:DataZamowieniaZakupu' ),
			];
		}
		$wz = $xp->query( '//fa:Fa/fa:DokumentyPowiazane/fa:DokumentMagazynowy' );
		if ( $wz && $wz->length > 0 ) {
			$rel['wz'] = [ 'number' => self::child( $xp, $wz->item( 0 ), 'fa:NumerDokumentu' ) ];
		}
		$out['related_docs'] = $rel;

		// Extra: customer_no, issue_place
		$out['extra_data'] = [
			'customer_no' => self::str( $xp, '//fa:Fa/fa:NumerKlienta/fa:NrKlienta' ),
			'issue_place' => $out['issue_place'],
		];

		// Podmiot3 (odbiorca = adres dostawy)
		$delivery = self::parse_delivery_party( $xp );
		if ( $delivery ) {
			$out['delivery'] = $delivery;
			// Backward-compat
			$out['delivery_address'] = [
				'name'        => $delivery['name'] ?? '',
				'street'      => $delivery['address']['street'] ?? '',
				'postal_code' => $delivery['address']['postal_code'] ?? '',
				'city'        => $delivery['address']['city'] ?? '',
				'country'     => $delivery['address']['country'] ?? 'PL',
			];
		}

		// PodmiotUpoważniony (biuro rachunkowe)
		$auth = self::parse_authorized_party( $xp );
		if ( $auth ) {
			$out['authorized'] = $auth;
		}

		// Korekta — gdy faktura jest korygująca
		$correction = self::parse_correction( $xp );
		if ( $correction ) {
			$out['correction'] = $correction;
		}

		// Faktura zaliczkowa — P_5A/P_5B
		$advance = self::parse_advance( $xp );
		if ( $advance ) {
			$out['advance'] = $advance;
		}

		// Stopka (Wstęp/Zakończenie/Rejestracja)
		$footer = self::parse_footer( $xp );
		if ( $footer ) {
			$out['footer'] = $footer;
		}

		// Naglowek systemowy KSeF
		$kf_node = $xp->query( '//fa:Naglowek/fa:KodFormularza' )->item( 0 );
		$out['system_info'] = [
			'kod_formularza'   => self::str( $xp, '//fa:Naglowek/fa:KodFormularza' ),
			'kod_systemowy'    => $kf_node ? trim( (string) $kf_node->getAttribute( 'kodSystemowy' ) ) : '',
			'wersja_schemy'    => $kf_node ? trim( (string) $kf_node->getAttribute( 'wersjaSchemy' ) ) : '',
			'wariant'          => self::str( $xp, '//fa:Naglowek/fa:WariantFormularza' ),
			'data_wytworzenia' => self::str( $xp, '//fa:Naglowek/fa:DataWytworzeniaFa' ),
			'system_info'      => self::str( $xp, '//fa:Naglowek/fa:SystemInfo' ),
			'kod_urzedu'       => self::str( $xp, '//fa:Naglowek/fa:KodUrzedu' ),
		];

		// WarunkiTransakcji (transport, koszt opakowań, dostawa)
		$wt = self::parse_transport_conditions( $xp );
		if ( $wt ) {
			$out['transport_conditions'] = $wt;
		}

		// Zamowienie z wierszami
		$zam = self::parse_zamowienie_details( $xp );
		if ( $zam ) {
			$out['order_details'] = $zam;
		}

		// Wszystkie faktury zaliczkowe (multi)
		$zaliczki = self::parse_advance_invoices( $xp );
		if ( ! empty( $zaliczki ) ) {
			$out['advance_invoices'] = $zaliczki;
		}

		// DostawcaNieobowiazkowe / NabywcaNieobowiazkowe (opcjonalne dane)
		$optdesc = self::parse_optional_party_descriptions( $xp );
		if ( ! empty( array_filter( $optdesc ) ) ) {
			$out['optional_descriptions'] = $optdesc;
		}

		// Multiple Podmiot1k (dodatkowi sprzedawcy — wspólnota)
		$p1k = [];
		foreach ( $xp->query( '//fa:Podmiot1k' ) ?: [] as $i => $node ) {
			$p1k[] = self::parse_party( $xp, '(//fa:Podmiot1k)[' . ( $i + 1 ) . ']' );
		}
		if ( ! empty( $p1k ) ) {
			$out['sellers_additional'] = $p1k;
		}

		// Multiple Podmiot2k (dodatkowi nabywcy)
		$p2k = [];
		foreach ( $xp->query( '//fa:Podmiot2k' ) ?: [] as $i => $node ) {
			$p2k[] = self::parse_party( $xp, '(//fa:Podmiot2k)[' . ( $i + 1 ) . ']' );
		}
		if ( ! empty( $p2k ) ) {
			$out['buyers_additional'] = $p2k;
		}

		// PrzeterminePlatnosci (odsetki za zwłokę)
		$out['payment']['przetermine'] = self::str( $xp, '//fa:Fa/fa:Platnosc/fa:PrzeterminePlatnosci' );

		return $out;
	}

	private static function parse_party( \DOMXPath $xp, string $xpath ): array {
		$node = $xp->query( $xpath )->item( 0 );
		if ( ! $node ) {
			return [];
		}
		// Adres szczegółowy FA(3): KodKraju + KodPocztowy + Miejscowosc + Ulica + NrDomu + NrLokalu
		// + Wojewodztwo + Powiat + Gmina + Dzielnica. Fallback do AdresL1/L2 gdy szczegółów brak.
		$street_no = self::child( $xp, $node, 'fa:Adres/fa:NrDomu' );
		$apt_no    = self::child( $xp, $node, 'fa:Adres/fa:NrLokalu' );
		$street    = trim( self::child( $xp, $node, 'fa:Adres/fa:Ulica' ) . ' ' . $street_no . ( $apt_no !== '' ? '/' . $apt_no : '' ) );
		if ( '' === $street ) {
			$street = self::child( $xp, $node, 'fa:Adres/fa:AdresL1' );
		}
		$address = [
			'country'     => self::child( $xp, $node, 'fa:Adres/fa:KodKraju' ),
			'postal_code' => self::child( $xp, $node, 'fa:Adres/fa:KodPocztowy' ),
			'city'        => self::child( $xp, $node, 'fa:Adres/fa:Miejscowosc' ),
			'street'      => $street,
			'street_only' => self::child( $xp, $node, 'fa:Adres/fa:Ulica' ),
			'street_no'   => $street_no,
			'apt_no'      => $apt_no,
			'voivodeship' => self::child( $xp, $node, 'fa:Adres/fa:Wojewodztwo' ),
			'county'      => self::child( $xp, $node, 'fa:Adres/fa:Powiat' ),
			'commune'     => self::child( $xp, $node, 'fa:Adres/fa:Gmina' ),
			'district'    => self::child( $xp, $node, 'fa:Adres/fa:Dzielnica' ),
			'line1'       => self::child( $xp, $node, 'fa:Adres/fa:AdresL1' ),
			'line2'       => self::child( $xp, $node, 'fa:Adres/fa:AdresL2' ),
		];
		// Adres dwuliniowy (AdresL1/AdresL2) bez pól strukturalnych — rozbij AdresL2
		// ("00-373 Warszawa") na kod pocztowy + miejscowość, aby pełny adres był widoczny.
		if ( '' === $address['city'] && '' === $address['postal_code'] ) {
			$l2 = trim( (string) $address['line2'] );
			if ( '' !== $l2 ) {
				if ( preg_match( '/^(\d{2}-\d{3})\s+(.+)$/u', $l2, $m ) ) {
					$address['postal_code'] = $m[1];
					$address['city']        = trim( $m[2] );
				} else {
					$address['city'] = $l2;
				}
			}
		}
		// Gdy brak strukturalnej ulicy, ale jest AdresL1 — użyj go jako ulicy.
		if ( '' === $address['street'] && '' !== trim( (string) $address['line1'] ) ) {
			$address['street'] = trim( (string) $address['line1'] );
		}
		// OsobaFizyczna1 — gdy sprzedawcą jest osoba fizyczna
		$osoba = $xp->query( './fa:DaneIdentyfikacyjne/fa:OsobaFizyczna1', $node )->item( 0 );
		$osoba_fiz = null;
		if ( $osoba ) {
			$osoba_fiz = [
				'imie'     => self::child( $xp, $osoba, 'fa:ImiePierwsze' ),
				'nazwisko' => self::child( $xp, $osoba, 'fa:Nazwisko' ),
				'id_ns'    => self::child( $xp, $osoba, 'fa:IDNS' ),
			];
		}

		// AdresKoresp (adres korespondencyjny, oddzielny)
		$adk = $xp->query( './fa:AdresKoresp', $node )->item( 0 );
		$adres_koresp = null;
		if ( $adk ) {
			$adres_koresp = [
				'country'     => self::child( $xp, $adk, 'fa:KodKraju' ),
				'postal_code' => self::child( $xp, $adk, 'fa:KodPocztowy' ),
				'city'        => self::child( $xp, $adk, 'fa:Miejscowosc' ),
				'street'      => self::child( $xp, $adk, 'fa:Ulica' ),
				'street_no'   => self::child( $xp, $adk, 'fa:NrDomu' ),
				'apt_no'      => self::child( $xp, $adk, 'fa:NrLokalu' ),
				'line1'       => self::child( $xp, $adk, 'fa:AdresL1' ),
				'line2'       => self::child( $xp, $adk, 'fa:AdresL2' ),
			];
		}

		// Telefony i Emaile mogą być multi
		$emails = []; $phones = [];
		foreach ( $xp->query( './fa:DaneKontaktowe/fa:Email', $node ) ?: [] as $em ) $emails[] = trim( $em->textContent );
		foreach ( $xp->query( './fa:DaneKontaktowe/fa:Telefon', $node ) ?: [] as $tp ) $phones[] = trim( $tp->textContent );

		return [
			'nip'           => self::child( $xp, $node, 'fa:DaneIdentyfikacyjne/fa:NIP' ),
			'vat_ue'        => self::child( $xp, $node, 'fa:DaneIdentyfikacyjne/fa:NrVatUE' ),
			'country_ue'    => self::child( $xp, $node, 'fa:DaneIdentyfikacyjne/fa:KodUE' ),
			'country_code'  => self::child( $xp, $node, 'fa:DaneIdentyfikacyjne/fa:KodKraju' ),
			'nr_id'         => self::child( $xp, $node, 'fa:DaneIdentyfikacyjne/fa:NrID' ),
			'brak_id'       => self::child( $xp, $node, 'fa:DaneIdentyfikacyjne/fa:BrakID' ),
			'name'          => self::child( $xp, $node, 'fa:DaneIdentyfikacyjne/fa:Nazwa' ) ?: self::child( $xp, $node, 'fa:DaneIdentyfikacyjne/fa:PelnaNazwa' ),
			'regon'         => self::child( $xp, $node, 'fa:DaneIdentyfikacyjne/fa:REGON' ),
			'krs'           => self::child( $xp, $node, 'fa:DaneIdentyfikacyjne/fa:KRS' ),
			'pesel'         => self::child( $xp, $node, 'fa:DaneIdentyfikacyjne/fa:PESEL' ),
			'osoba_fiz'     => $osoba_fiz,
			'email'         => $emails[0] ?? '',
			'phone'         => $phones[0] ?? '',
			'emails'        => $emails,
			'phones'        => $phones,
			'eori'          => self::child( $xp, $node, 'fa:NrEORI' ),
			'prefiks'       => self::child( $xp, $node, 'fa:PrefiksPodatnika' ),
			'status_info'   => self::child( $xp, $node, 'fa:StatusInfoPodatnika' ),
			'nr_klienta'    => self::child( $xp, $node, 'fa:NrKlienta' ),
			'id_nabywcy'    => self::child( $xp, $node, 'fa:IDNabywcy' ),
			'jst'           => self::child( $xp, $node, 'fa:JST' ),
			'gv'            => self::child( $xp, $node, 'fa:GV' ),
			'address'       => $address,
			'address_koresp'=> $adres_koresp,
		];
	}

	private static function parse_markers( \DOMXPath $xp ): array {
		$markers = [];
		foreach ( [ 'P_16', 'P_17', 'P_18', 'P_18A', 'P_23' ] as $code ) {
			if ( '1' === self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:' . $code ) ) {
				$markers[ $code ] = '1';
			}
		}
		if ( '1' === self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:Zwolnienie/fa:P_19N' ) ) {
			// P_19N=1 oznacza "nie dotyczy"; gdy w XML obecna jakaś podstawa zwolnienia → P_19
			$has_zw = $xp->query( '//fa:Fa/fa:Adnotacje/fa:Zwolnienie/fa:P_19A' );
			if ( ! $has_zw || $has_zw->length === 0 ) {
				// fallback: jeśli jest tylko P_19N=2 (nie zwolnione) — nic nie dodawaj
			}
		}
		// PMarzy
		if ( '1' === self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:PMarzy/fa:P_PMarzyN' ) ) {
			// 1 = nie dotyczy — pomiń
		} else {
			$pm_nodes = $xp->query( '//fa:Fa/fa:Adnotacje/fa:PMarzy/*' );
			if ( $pm_nodes && $pm_nodes->length > 0 ) {
				$markers['PMarzy'] = '1';
			}
		}
		// Faktura za okres
		if ( $xp->query( '//fa:Fa/fa:OkresFa' )->length > 0 ) {
			$markers['fa_za_okres'] = '1';
		}
		// FP
		if ( 'FP' === self::str( $xp, '//fa:Fa/fa:RodzajFaktury' ) ) {
			$markers['FP'] = '1';
		}
		return $markers;
	}

	private static function parse_adnotacje_raw( \DOMXPath $xp ): array {
		$out = [];
		foreach ( [ 'P_16', 'P_17', 'P_18', 'P_18A', 'P_21', 'P_22', 'P_23' ] as $code ) {
			$v = self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:' . $code );
			$out[ $code ] = '' === $v ? '2' : $v;
		}

		// Zwolnienie szczegół: P_19 + PrzyczynaZwolnienia (P_19A art., P_19B ustawa, P_19C dyrektywa)
		$out['Zwolnienie'] = [
			'P_19N' => self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:Zwolnienie/fa:P_19N' ),
			'P_19'  => self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:Zwolnienie/fa:P_19' ),
			'P_19A' => self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:Zwolnienie/fa:P_19A' ),
			'P_19B' => self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:Zwolnienie/fa:P_19B' ),
			'P_19C' => self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:Zwolnienie/fa:P_19C' ),
		];

		// NoweSrodkiTransportu szczegół: P_22A (data) + P_22B (przebieg/godziny)
		$out['NoweSrodkiTransportu'] = [
			'P_22'  => self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:NoweSrodkiTransportu/fa:P_22' ),
			'P_22A' => self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:NoweSrodkiTransportu/fa:P_22A' ),
			'P_22B' => self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:NoweSrodkiTransportu/fa:P_22B' ),
		];

		// PMarzy szczegół
		$out['PMarzy'] = [
			'P_PMarzyN' => self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:PMarzy/fa:P_PMarzyN' ),
			'P_PMarzy_2' => self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:PMarzy/fa:P_PMarzy_2' ),
			'P_PMarzy_3' => self::str( $xp, '//fa:Fa/fa:Adnotacje/fa:PMarzy/fa:P_PMarzy_3' ),
		];

		return $out;
	}

	private static function build_vat_summary( \DOMXPath $xp ): array {
		// Pełna lista stawek z FA(3) — P_13_X (suma netto) + P_14_X (suma VAT)
		$mapping = [
			[ 'P_13_1',   'P_14_1', 23 ],
			[ 'P_13_2',   'P_14_2', 8 ],
			[ 'P_13_3',   'P_14_3', 5 ],
			[ 'P_13_4',   'P_14_4', 'inne' ],
			[ 'P_13_5',   'P_14_5', 0 ],            // 0%
			[ 'P_13_6_1', null,     'zw' ],          // zwolnione
			[ 'P_13_6_2', null,     'np' ],          // nie podlega
			[ 'P_13_7',   null,     'oo' ],          // odwrotne obciążenie
		];
		$out = [];
		foreach ( $mapping as [$net_tag, $vat_tag, $rate] ) {
			$net = self::num( $xp, '//fa:Fa/fa:' . $net_tag );
			$vat = $vat_tag ? self::num( $xp, '//fa:Fa/fa:' . $vat_tag ) : 0.0;
			if ( $net > 0 || $vat > 0 ) {
				$out[] = [
					'rate'       => $rate,
					'base_net'   => round( $net, 2 ),
					'vat_amount' => round( $vat, 2 ),
				];
			}
		}
		return $out;
	}

	private static function parse_delivery_party( \DOMXPath $xp ): ?array {
		$node = $xp->query( '//fa:Podmiot3' )->item( 0 );
		if ( ! $node ) {
			return null;
		}
		$party = self::parse_party( $xp, '//fa:Podmiot3' );
		$party['role'] = self::child( $xp, $node, 'fa:RolaPodmiotu3' );
		return $party;
	}

	private static function parse_authorized_party( \DOMXPath $xp ): ?array {
		$node = $xp->query( '//fa:PodmiotUpowazniony' )->item( 0 );
		if ( ! $node ) {
			return null;
		}
		return self::parse_party( $xp, '//fa:PodmiotUpowazniony' );
	}

	private static function parse_correction( \DOMXPath $xp ): ?array {
		// FA(3) Korekta: DanePoprzednie + PrzyczynaKorekty
		$causes = $xp->query( '//fa:Fa/fa:DaneFaKorygowanej' );
		if ( ! $causes || $causes->length === 0 ) {
			return null;
		}
		$first = $causes->item( 0 );
		return [
			'reason'              => self::str( $xp, '//fa:Fa/fa:PrzyczynaKorekty' ),
			'original_number'     => self::child( $xp, $first, 'fa:NrFaKorygowanej' ),
			'original_issue_date' => self::child( $xp, $first, 'fa:DataWystFaKorygowanej' ),
			'original_ksef_no'    => self::child( $xp, $first, 'fa:NrKSeF' ),
			'p_3a'                => self::str( $xp, '//fa:Fa/fa:P_3A' ),
			'p_3b'                => self::str( $xp, '//fa:Fa/fa:P_3B' ),
			'p_3c'                => self::str( $xp, '//fa:Fa/fa:P_3C' ),
			'p_3d'                => self::str( $xp, '//fa:Fa/fa:P_3D' ),
		];
	}

	private static function parse_advance( \DOMXPath $xp ): ?array {
		// Faktura zaliczkowa: P_5A, P_5B
		$p5a = self::str( $xp, '//fa:Fa/fa:P_5A' );
		$p5b = self::str( $xp, '//fa:Fa/fa:P_5B' );
		if ( '' === $p5a && '' === $p5b ) {
			return null;
		}
		return [ 'p_5a' => $p5a, 'p_5b' => $p5b ];
	}

	private static function parse_transport_conditions( \DOMXPath $xp ): ?array {
		$node = $xp->query( '//fa:Fa/fa:WarunkiTransakcji' )->item( 0 );
		if ( ! $node ) {
			return null;
		}
		$dostawa = $xp->query( './fa:Dostawa', $node )->item( 0 );
		return [
			'transport'        => self::child( $xp, $node, 'fa:Transport' ),
			'koszt_opakowan'   => self::child( $xp, $node, 'fa:KosztOpakowan' ),
			'warunki_dostawy'  => self::child( $xp, $node, 'fa:WarunkiDostawy' ),
			'data_dostawy'     => $dostawa ? self::child( $xp, $dostawa, 'fa:DataDostawy' ) : '',
			'miejsce_dostawy'  => $dostawa ? self::child( $xp, $dostawa, 'fa:MiejsceDostawy' ) : '',
		];
	}

	private static function parse_zamowienie_details( \DOMXPath $xp ): ?array {
		$node = $xp->query( '//fa:Fa/fa:Zamowienie' )->item( 0 );
		if ( ! $node ) {
			return null;
		}
		$wiersze = [];
		foreach ( $xp->query( './fa:Zamowienie/fa:ZamowienieWiersz', $node ) ?: [] as $w ) {
			$wiersze[] = [
				'nr_wiersza' => self::child( $xp, $w, 'fa:NrWierszaZam' ),
				'opis'       => self::child( $xp, $w, 'fa:P_7Z' ),
				'jednostka'  => self::child( $xp, $w, 'fa:P_8AZ' ),
				'ilosc'      => self::child( $xp, $w, 'fa:P_8BZ' ),
				'cena_netto' => self::child( $xp, $w, 'fa:P_9AZ' ),
				'wartosc'    => self::child( $xp, $w, 'fa:P_11Z' ),
				'stawka'     => self::child( $xp, $w, 'fa:P_12Z' ),
			];
		}
		return [
			'numer'       => self::child( $xp, $node, 'fa:NrZamowienia' ),
			'data'        => self::child( $xp, $node, 'fa:DataZamowienia' ),
			'wiersze'     => $wiersze,
		];
	}

	private static function parse_advance_invoices( \DOMXPath $xp ): array {
		$out = [];
		foreach ( $xp->query( '//fa:Fa/fa:Zaliczka' ) ?: [] as $z ) {
			$out[] = [
				'kwota'    => (float) ( self::child( $xp, $z, 'fa:KwotaZaliczki' ) ?: 0 ),
				'nr_fa'   => self::child( $xp, $z, 'fa:NrFaZaliczkowej' ),
				'data_fa' => self::child( $xp, $z, 'fa:DataFaZaliczkowej' ),
			];
		}
		return $out;
	}

	private static function parse_optional_party_descriptions( \DOMXPath $xp ): array {
		// DostawcaNieobowiazkowe i NabywcaNieobowiazkowe — opcjonalne opisy dostawcy/nabywcy
		// (np. dane oddziału, departament, dział, biuro)
		$collect = function ( \DOMXPath $xp, string $base ) {
			$node = $xp->query( $base )->item( 0 );
			if ( ! $node ) return null;
			return [
				'imiona'   => self::child( $xp, $node, 'fa:Imiona' ),
				'nazwisko' => self::child( $xp, $node, 'fa:Nazwisko' ),
				'nazwa'    => self::child( $xp, $node, 'fa:Nazwa' ),
				'rola'     => self::child( $xp, $node, 'fa:RolaPodmiotu' ),
				'opis'     => self::child( $xp, $node, 'fa:Opis' ),
			];
		};
		return [
			'dostawca' => $collect( $xp, '//fa:Fa/fa:DostawcaNieobowiazkowe' ),
			'nabywca'  => $collect( $xp, '//fa:Fa/fa:NabywcaNieobowiazkowe' ),
		];
	}

	private static function parse_footer( \DOMXPath $xp ): ?array {
		$wstep    = self::str( $xp, '//fa:Stopka/fa:Wstep' );
		$zak      = self::str( $xp, '//fa:Stopka/fa:Zakonczenie' );
		$rejestr  = $xp->query( '//fa:Stopka/fa:Rejestracja' )->item( 0 );
		$reg_data = null;
		if ( $rejestr ) {
			$reg_data = [
				'kapital_zakladowy' => self::child( $xp, $rejestr, 'fa:KapitalZakladowy' ),
				'kapital_wplacony'  => self::child( $xp, $rejestr, 'fa:KapitalWplacony' ),
				'organ_rejestrowy'  => self::child( $xp, $rejestr, 'fa:OrganRejestrowy' ),
				'krs'               => self::child( $xp, $rejestr, 'fa:KRS' ),
			];
		}
		if ( '' === $wstep && '' === $zak && ! $reg_data ) {
			return null;
		}
		return [
			'intro'        => $wstep,
			'closing'      => $zak,
			'registration' => $reg_data,
		];
	}

	private static function vat_label_to_value( string $label ): float|string {
		$label = trim( $label );
		if ( in_array( strtolower( $label ), [ 'zw', 'np', 'oo' ], true ) ) {
			return strtolower( $label );
		}
		if ( preg_match( '/^(\d+)/', $label, $m ) ) {
			return (float) $m[1];
		}
		return 0.0;
	}

	private static function str( \DOMXPath $xp, string $xpath ): string {
		$node = $xp->query( $xpath )->item( 0 );
		return $node ? trim( $node->textContent ) : '';
	}

	private static function num( \DOMXPath $xp, string $xpath ): float {
		return (float) self::str( $xp, $xpath );
	}

	private static function child( \DOMXPath $xp, ?\DOMNode $context, string $xpath ): string {
		if ( ! $context ) {
			return '';
		}
		$node = $xp->query( $xpath, $context )->item( 0 );
		return $node ? trim( $node->textContent ) : '';
	}
}
