<?php
/**
 * Generator XML faktury FA(3) zgodny z oficjalnym schematem MF:
 *   schemat_FA-3_v1-0E.xsd  ·  namespace http://crd.gov.pl/wzor/2025/06/25/13775/
 *
 * Obowiązkowe od 1.02.2026 — KSeF v2 PROD nie przyjmie innych namespace/wersji.
 *
 * Input: tablica z danymi faktury zgodna z Order_Mapper output:
 *   - seller: {nip, name, address: {street, city, postal_code, country}, ...}
 *   - buyer: {nip|kod_kraju+nr_id|kod_ue+vat_ue|brak_id, name, address}
 *   - invoice_number, issue_date, sale_date, currency
 *   - items: [ {description, quantity, unit, unit_price_net, vat_rate, vat_amount, line_net, line_gross} ]
 *   - vat_summary: per stawka { rate, base_net, vat_amount }
 *   - total_net, total_vat, total_gross
 *   - payment_terms (opcjonalnie)
 *   - is_b2c: bool (true gdy klient prywatny — wymusza BrakID)
 *
 * @package KSEF\WordPress\V2
 */

declare(strict_types=1);

namespace KSEF\WordPress\V2;

class Fa3_Builder {

	private const FA_NS    = 'http://crd.gov.pl/wzor/2025/06/25/13775/';
	private const ETD_NS   = 'http://crd.gov.pl/xml/schematy/dziedzinowe/mf/2022/01/05/eD/DefinicjeTypy/';
	private const FA_KOD   = 'FA (3)';
	private const FA_WER   = '1-0E';
	private const WARIANT  = 3;

	/**
	 * @param array<string,mixed> $data
	 */
	public function build( array $data ): string {
		$dom               = new \DOMDocument( '1.0', 'UTF-8' );
		$dom->formatOutput = false;
		$dom->preserveWhiteSpace = false;

		$faktura = $dom->createElementNS( self::FA_NS, 'Faktura' );
		$faktura->setAttribute( 'xmlns:etd', self::ETD_NS );
		$dom->appendChild( $faktura );

		$this->add_naglowek( $dom, $faktura );
		$this->add_podmiot1( $dom, $faktura, $data['seller'] );
		$this->add_podmiot2( $dom, $faktura, $data['buyer'], (bool) ( $data['is_b2c'] ?? false ) );
		$this->maybe_add_podmiot3( $dom, $faktura, $data['delivery_address'] ?? null );
		$this->add_fa( $dom, $faktura, $data );

		return (string) $dom->saveXML();
	}

	/**
	 * Waliduje wygenerowany XML względem oficjalnego XSD FA(3) (bundled).
	 * Zwraca [] gdy OK, albo listę błędów (string[]).
	 *
	 * @return string[] lista błędów walidacji (pusta = XML zgodny)
	 */
	public function validate( string $xml ): array {
		$xsd = __DIR__ . '/xsd/schemat_FA-3_v1-0E.xsd';
		if ( ! is_file( $xsd ) ) {
			// Brak XSD — nie blokujemy (walidacja niedostępna), ale logujemy.
			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
				ksef_efaktury_log( '[KSEF] Brak bundled XSD — walidacja FA(3) pominięta.' );
			}
			return [];
		}
		$prev = libxml_use_internal_errors( true );
		libxml_clear_errors();
		$dom = new \DOMDocument();
		if ( ! $dom->loadXML( $xml ) ) {
			$errs = [ 'XML nie wczytał się: ' . ( libxml_get_last_error()->message ?? 'nieznany błąd' ) ];
			libxml_clear_errors();
			libxml_use_internal_errors( $prev );
			return $errs;
		}
		$ok   = $dom->schemaValidate( $xsd );
		$out  = [];
		if ( ! $ok ) {
			foreach ( libxml_get_errors() as $e ) {
				$out[] = trim( $e->message ) . ( $e->line ? " (linia {$e->line})" : '' );
			}
		}
		libxml_clear_errors();
		libxml_use_internal_errors( $prev );
		return $out;
	}

	/* ===================== Naglowek ===================== */

	private function add_naglowek( \DOMDocument $dom, \DOMElement $root ): void {
		$nag = $dom->createElementNS( self::FA_NS, 'Naglowek' );
		$root->appendChild( $nag );

		$kf = $dom->createElementNS( self::FA_NS, 'KodFormularza', 'FA' );
		$kf->setAttribute( 'kodSystemowy', self::FA_KOD );
		$kf->setAttribute( 'wersjaSchemy', self::FA_WER );
		$nag->appendChild( $kf );

		$nag->appendChild( $dom->createElementNS( self::FA_NS, 'WariantFormularza', (string) self::WARIANT ) );
		$nag->appendChild( $dom->createElementNS( self::FA_NS, 'DataWytworzeniaFa', gmdate( 'Y-m-d\TH:i:s\Z' ) ) );
		$sys_edition = ( defined( 'KSEF_WORDPRESS_PLUGIN_SLUG' ) && 'prostafaktura-ksef' === KSEF_WORDPRESS_PLUGIN_SLUG ) ? 'ProstaFaktura dla KSeF' : 'ProstaFaktura dla KSeF PRO';
		$sys_info    = $sys_edition . ' (prostafakturaksef.pl) v' . ( defined( 'KSEF_WORDPRESS_VERSION' ) ? KSEF_WORDPRESS_VERSION : 'dev' );
		$nag->appendChild( $dom->createElementNS( self::FA_NS, 'SystemInfo', $sys_info ) );
	}

	/* ===================== Podmiot1 (sprzedawca) ===================== */

	/**
	 * @param array<string,mixed> $seller
	 */
	private function add_podmiot1( \DOMDocument $dom, \DOMElement $root, array $seller ): void {
		$pm = $dom->createElementNS( self::FA_NS, 'Podmiot1' );
		$root->appendChild( $pm );

		$dane = $dom->createElementNS( self::FA_NS, 'DaneIdentyfikacyjne' );
		$pm->appendChild( $dane );
		$dane->appendChild( $dom->createElementNS( self::FA_NS, 'NIP', $this->only_digits( (string) $seller['nip'] ) ) );
		$dane->appendChild( $dom->createElementNS( self::FA_NS, 'Nazwa', $this->t( (string) ( $seller['name'] ?? '' ) ) ) );

		$this->add_adres( $dom, $pm, $seller['address'] ?? [] );

		if ( ! empty( $seller['email'] ) || ! empty( $seller['phone'] ) ) {
			$dk = $dom->createElementNS( self::FA_NS, 'DaneKontaktowe' );
			$pm->appendChild( $dk );
			if ( ! empty( $seller['email'] ) ) {
				$dk->appendChild( $dom->createElementNS( self::FA_NS, 'Email', $this->t( (string) $seller['email'] ) ) );
			}
			if ( ! empty( $seller['phone'] ) ) {
				$dk->appendChild( $dom->createElementNS( self::FA_NS, 'Telefon', $this->t( (string) $seller['phone'] ) ) );
			}
		}
	}

	/* ===================== Podmiot2 (nabywca) ===================== */

	/**
	 * @param array<string,mixed> $buyer
	 */
	private function add_podmiot2( \DOMDocument $dom, \DOMElement $root, array $buyer, bool $is_b2c ): void {
		$pm = $dom->createElementNS( self::FA_NS, 'Podmiot2' );
		$root->appendChild( $pm );

		$dane = $dom->createElementNS( self::FA_NS, 'DaneIdentyfikacyjne' );
		$pm->appendChild( $dane );

		// choice z XSD: NIP | KodUE+NrVatUE | KodKraju+NrID | BrakID
		// Priorytet: explicit B2C → BrakID; potem vat_ue → KodUE+NrVatUE; potem nr_id → KodKraju+NrID;
		// potem NIP jeśli jest. Dopiero gdy NIC z powyższych → BrakID (osoba fizyczna bez identyfikatora).
		if ( $is_b2c ) {
			$dane->appendChild( $dom->createElementNS( self::FA_NS, 'BrakID', '1' ) );
		} elseif ( ! empty( $buyer['vat_ue'] ) && ! empty( $buyer['country_ue'] ) ) {
			$kod   = strtoupper( (string) $buyer['country_ue'] );
			$vatue = strtoupper( preg_replace( '/[^A-Z0-9+*]/', '', (string) $buyer['vat_ue'] ) );
			// TNrVatUE pattern (\d|[A-Z]|\+|\*){1,12} — max 12 znaków, bez prefiksu kraju.
			if ( strpos( $vatue, $kod ) === 0 ) {
				$vatue = substr( $vatue, strlen( $kod ) );
			}
			$vatue = substr( $vatue, 0, 12 );
			$dane->appendChild( $dom->createElementNS( self::FA_NS, 'KodUE', $kod ) );
			$dane->appendChild( $dom->createElementNS( self::FA_NS, 'NrVatUE', $vatue ) );
		} elseif ( ! empty( $buyer['country_code'] ) && ! empty( $buyer['nr_id'] ) ) {
			$dane->appendChild( $dom->createElementNS( self::FA_NS, 'KodKraju', strtoupper( (string) $buyer['country_code'] ) ) );
			$dane->appendChild( $dom->createElementNS( self::FA_NS, 'NrID', $this->t( (string) $buyer['nr_id'] ) ) );
		} elseif ( ! empty( $buyer['nip'] ) ) {
			$dane->appendChild( $dom->createElementNS( self::FA_NS, 'NIP', $this->only_digits( (string) $buyer['nip'] ) ) );
		} else {
			$dane->appendChild( $dom->createElementNS( self::FA_NS, 'BrakID', '1' ) );
		}

		$dane->appendChild( $dom->createElementNS( self::FA_NS, 'Nazwa', $this->t( (string) ( $buyer['name'] ?? '' ) ) ) );

		$this->add_adres( $dom, $pm, $buyer['address'] ?? [] );

		// DaneKontaktowe — opcjonalne, ale jeśli mamy email — wstawiamy.
		$email = trim( (string) ( $buyer['email'] ?? '' ) );
		if ( $email !== '' ) {
			$dk = $dom->createElementNS( self::FA_NS, 'DaneKontaktowe' );
			$pm->appendChild( $dk );
			$dk->appendChild( $dom->createElementNS( self::FA_NS, 'Email', $this->t( $email ) ) );
		}

		// NrKlienta — dziecko Podmiot2 (po DaneKontaktowe, przed JST/GV)
		if ( ! empty( $buyer['customer_no'] ) ) {
			$pm->appendChild( $dom->createElementNS( self::FA_NS, 'NrKlienta', $this->t( (string) $buyer['customer_no'] ) ) );
		}

		// JST i GV — WYMAGANE (XSD: brak minOccurs="0").
		// Wartości: "1" = tak, "2" = nie dotyczy.
		$pm->appendChild( $dom->createElementNS( self::FA_NS, 'JST', (string) ( $buyer['jst'] ?? '2' ) ) );
		$pm->appendChild( $dom->createElementNS( self::FA_NS, 'GV',  (string) ( $buyer['gv']  ?? '2' ) ) );
	}

	/* ===================== Adres ===================== */

	/**
	 * Adres wg XSD `TAdres` — PŁASKA struktura: KodKraju, AdresL1, AdresL2?, GLN?
	 * (Nie ma żadnego AdresPol/AdresZagr — to było w starszych formularzach.)
	 *
	 * @param array<string,mixed> $addr
	 */
	private function add_adres( \DOMDocument $dom, \DOMElement $parent, array $addr ): void {
		$ad = $dom->createElementNS( self::FA_NS, 'Adres' );
		$parent->appendChild( $ad );

		$country = strtoupper( (string) ( $addr['country'] ?? 'PL' ) );
		$ad->appendChild( $dom->createElementNS( self::FA_NS, 'KodKraju', $country ) );

		// AdresL1 = ulica + numer
		$line1 = trim(
			(string) ( $addr['street'] ?? '' )
			. ( ! empty( $addr['street_number'] ) ? ' ' . (string) $addr['street_number'] : '' )
		);
		if ( $line1 === '' ) {
			$line1 = '-'; // wymagane minOccurs=1 w XSD
		}
		$ad->appendChild( $dom->createElementNS( self::FA_NS, 'AdresL1', $this->t( $line1 ) ) );

		// AdresL2 = kod pocztowy + miasto (opcjonalne)
		$line2 = trim(
			(string) ( $addr['postal_code'] ?? '' )
			. ( ! empty( $addr['city'] ) ? ' ' . (string) $addr['city'] : '' )
		);
		if ( $line2 !== '' ) {
			$ad->appendChild( $dom->createElementNS( self::FA_NS, 'AdresL2', $this->t( $line2 ) ) );
		}
	}

	/* ===================== Fa (treść faktury) ===================== */

	/**
	 * @param array<string,mixed> $data
	 */
	private function add_fa( \DOMDocument $dom, \DOMElement $root, array $data ): void {
		$fa = $dom->createElementNS( self::FA_NS, 'Fa' );
		$root->appendChild( $fa );

		// 1. KodWaluty, P_1, P_1M?, P_2
		$fa->appendChild( $dom->createElementNS( self::FA_NS, 'KodWaluty', strtoupper( (string) ( $data['currency'] ?? 'PLN' ) ) ) );
		$fa->appendChild( $dom->createElementNS( self::FA_NS, 'P_1', $this->date( (string) $data['issue_date'] ) ) );
		// P_1M (miejsce wystawienia) — z extra_data lub bezpośredniego pola
		$issue_place = trim( (string) ( $data['extra_data']['issue_place'] ?? $data['issue_place'] ?? '' ) );
		if ( $issue_place !== '' ) {
			$fa->appendChild( $dom->createElementNS( self::FA_NS, 'P_1M', $this->t( $issue_place ) ) );
		}
		$fa->appendChild( $dom->createElementNS( self::FA_NS, 'P_2', $this->t( (string) $data['invoice_number'] ) ) );

		// 1b. WZ — proste elementy z numerami magazynowymi (po P_2, przed P_6)
		$wz_no = trim( (string) ( $data['related_docs']['wz']['number'] ?? '' ) );
		if ( $wz_no !== '' ) {
			$fa->appendChild( $dom->createElementNS( self::FA_NS, 'WZ', $this->t( $wz_no ) ) );
		}

		// 2. P_6 / OkresFa — faktura za okres ma <OkresFa><P_6_Od>..</P_6_Od><P_6_Do>..</P_6_Do></OkresFa>
		$is_period = ! empty( $data['markers']['fa_za_okres'] ) && ! empty( $data['period_from'] ) && ! empty( $data['period_to'] );
		if ( $is_period ) {
			$okres = $dom->createElementNS( self::FA_NS, 'OkresFa' );
			$fa->appendChild( $okres );
			$okres->appendChild( $dom->createElementNS( self::FA_NS, 'P_6_Od', $this->date( (string) $data['period_from'] ) ) );
			$okres->appendChild( $dom->createElementNS( self::FA_NS, 'P_6_Do', $this->date( (string) $data['period_to'] ) ) );
		} elseif ( ! empty( $data['sale_date'] ) ) {
			$fa->appendChild( $dom->createElementNS( self::FA_NS, 'P_6', $this->date( (string) $data['sale_date'] ) ) );
		}

		// 3. Sumy VAT per stawka (P_13_1+P_14_1, P_13_2+P_14_2, ...) + P_13_6_1 + P_15
		// MUSI iść w SZTYWNEJ kolejności XSD. Iterowanie po vat_summary w kolejności
		// wejściowej łamie XSD gdy stawki nie rosnące → KSeF odrzuca fakturę.
		$buckets = [
			'net_1'=>0.0,'vat_1'=>0.0, 'net_2'=>0.0,'vat_2'=>0.0, 'net_3'=>0.0,'vat_3'=>0.0,
			'net_4'=>0.0,'vat_4'=>0.0, 'net_5'=>0.0, 'net_6_1'=>0.0,'net_6_2'=>0.0,'net_6_3'=>0.0,
			'net_7'=>0.0, 'net_8'=>0.0,
		];
		foreach ( $data['vat_summary'] ?? [] as $vat ) {
			$rate_raw = $vat['rate'] ?? 0;
			$net      = (float) ( $vat['base_net'] ?? 0 );
			$vamt     = (float) ( $vat['vat_amount'] ?? 0 );
			if ( is_string( $rate_raw ) && ! is_numeric( $rate_raw ) ) {
				$k = strtolower( trim( $rate_raw ) );
				if ( 'zw' === $k )     { $buckets['net_7'] += $net; }
				elseif ( 'np' === $k ) { $buckets['net_8'] += $net; }
				elseif ( 'oo' === $k ) { $buckets['net_8'] += $net; }
				else                   { $buckets['net_6_1'] += $net; }
				continue;
			}
			switch ( (int) round( (float) $rate_raw ) ) {
				case 23: case 22: $buckets['net_1'] += $net; $buckets['vat_1'] += $vamt; break;
				case 8:  case 7:  $buckets['net_2'] += $net; $buckets['vat_2'] += $vamt; break;
				case 5:           $buckets['net_3'] += $net; $buckets['vat_3'] += $vamt; break;
				case 4:           $buckets['net_4'] += $net; $buckets['vat_4'] += $vamt; break;
				case 0:           $buckets['net_6_1'] += $net; break;
				default:          $buckets['net_1'] += $net; $buckets['vat_1'] += $vamt; break;
			}
		}
		// Waluta obca: po każdym P_14_x trzeba podać P_14_xW (VAT przeliczony na PLN) + KursWalutyZ po P_15.
		$fx_cur  = strtoupper( (string) ( $data['currency'] ?? 'PLN' ) );
		$fx_rate = (float) ( $data['exchange_rate'] ?? 0 );
		$is_fx   = ( 'PLN' !== $fx_cur && $fx_rate > 0 );
		$emit = function ( string $tag, float $val ) use ( $dom, $fa ) {
			$fa->appendChild( $dom->createElementNS( self::FA_NS, $tag, $this->num( $val, 2 ) ) );
		};
		if ( $buckets['net_1'] > 0 )   { $emit( 'P_13_1', $buckets['net_1'] ); $emit( 'P_14_1', $buckets['vat_1'] ); if ( $is_fx ) { $emit( 'P_14_1W', $buckets['vat_1'] * $fx_rate ); } }
		if ( $buckets['net_2'] > 0 )   { $emit( 'P_13_2', $buckets['net_2'] ); $emit( 'P_14_2', $buckets['vat_2'] ); if ( $is_fx ) { $emit( 'P_14_2W', $buckets['vat_2'] * $fx_rate ); } }
		if ( $buckets['net_3'] > 0 )   { $emit( 'P_13_3', $buckets['net_3'] ); $emit( 'P_14_3', $buckets['vat_3'] ); if ( $is_fx ) { $emit( 'P_14_3W', $buckets['vat_3'] * $fx_rate ); } }
		if ( $buckets['net_4'] > 0 )   { $emit( 'P_13_4', $buckets['net_4'] ); $emit( 'P_14_4', $buckets['vat_4'] ); if ( $is_fx ) { $emit( 'P_14_4W', $buckets['vat_4'] * $fx_rate ); } }
		if ( $buckets['net_5'] > 0 )   { $emit( 'P_13_5', $buckets['net_5'] ); }
		if ( $buckets['net_6_1'] > 0 ) { $emit( 'P_13_6_1', $buckets['net_6_1'] ); }
		if ( $buckets['net_6_2'] > 0 ) { $emit( 'P_13_6_2', $buckets['net_6_2'] ); }
		if ( $buckets['net_6_3'] > 0 ) { $emit( 'P_13_6_3', $buckets['net_6_3'] ); }
		if ( $buckets['net_7'] > 0 )   { $emit( 'P_13_7', $buckets['net_7'] ); }
		if ( $buckets['net_8'] > 0 )   { $emit( 'P_13_8', $buckets['net_8'] ); }

		// 4. P_15 (REQUIRED — razem brutto)
		$fa->appendChild( $dom->createElementNS( self::FA_NS, 'P_15', $this->num( (float) ( $data['total_gross'] ?? 0 ), 2 ) ) );

		// 4b. KursWalutyZ — kurs przeliczenia VAT na PLN (po P_15, przed Adnotacje) dla waluty obcej.
		if ( $is_fx ) {
			$fa->appendChild( $dom->createElementNS( self::FA_NS, 'KursWalutyZ', $this->num( $fx_rate, 4 ) ) );
		}

		// 5. Adnotacje (REQUIRED — XSD bez minOccurs=0). Po P_15.
		$adn = $dom->createElementNS( self::FA_NS, 'Adnotacje' );
		$fa->appendChild( $adn );
		$adn->appendChild( $dom->createElementNS( self::FA_NS, 'P_16',  $this->wybor12( $data['adnotacje']['P_16']  ?? null ) ) );
		$adn->appendChild( $dom->createElementNS( self::FA_NS, 'P_17',  $this->wybor12( $data['adnotacje']['P_17']  ?? null ) ) );
		$adn->appendChild( $dom->createElementNS( self::FA_NS, 'P_18',  $this->wybor12( $data['adnotacje']['P_18']  ?? null ) ) );
		// P_18A (mechanizm podzielonej płatności). Auto "1" gdy kwota należności
		// ogółem przekracza 15 000 zł (art. 108a ustawy o VAT). Próg dotyczy PLN —
		// dla walut obcych respektujemy wyłącznie jawny wybór (brak kursu do równowartości).
		$currency    = strtoupper( (string) ( $data['currency'] ?? 'PLN' ) );
		$gross_total = (float) ( $data['total_gross'] ?? 0 );
		$p18a        = $this->wybor12( $data['adnotacje']['P_18A'] ?? null );
		if ( 'PLN' === $currency && $gross_total > 15000.0 ) {
			$p18a = '1';
		}
		$adn->appendChild( $dom->createElementNS( self::FA_NS, 'P_18A', $p18a ) );

		// Zwolnienie — choice w XSD: P_19=1 + podstawa prawna (P_19A/B/C) albo P_19N=1.
		$has_zw = $buckets['net_7'] > 0 || $this->has_zw_item( $data );
		$zwol = $dom->createElementNS( self::FA_NS, 'Zwolnienie' );
		$adn->appendChild( $zwol );
		if ( $has_zw ) {
			$zwol->appendChild( $dom->createElementNS( self::FA_NS, 'P_19', '1' ) );
			$basis_kind = strtoupper( (string) ( $data['adnotacje']['P_19_kind'] ?? 'A' ) );
			$basis_text = $this->t( (string) ( $data['adnotacje']['P_19_basis'] ?? $data['exemption_legal_basis'] ?? '' ) );
			if ( '' === $basis_text ) {
				$basis_text = 'art. 113 ust. 1 i 9 ustawy o VAT';
				$basis_kind = 'A';
			}
			$basis_tag = match ( $basis_kind ) {
				'B'     => 'P_19B',
				'C'     => 'P_19C',
				default => 'P_19A',
			};
			$zwol->appendChild( $dom->createElementNS( self::FA_NS, $basis_tag, $basis_text ) );
		} else {
			$zwol->appendChild( $dom->createElementNS( self::FA_NS, 'P_19N', '1' ) );
		}

		// NoweSrodkiTransportu — WDT nowych środków transportu (P_22). Jeśli formularz
		// zebrał dane pojazdu (data dopuszczenia) → emituj NowySrodekTransportu,
		// w przeciwnym razie znacznik braku (P_22N=1).
		$nst = $dom->createElementNS( self::FA_NS, 'NoweSrodkiTransportu' );
		$adn->appendChild( $nst );
		$nst_data = is_array( $data['nst'] ?? null ) ? $data['nst'] : [];
		$nst_date = trim( (string) ( $nst_data['data_dopuszczenia'] ?? '' ) );
		if ( '' !== $nst_date ) {
			$nst->appendChild( $dom->createElementNS( self::FA_NS, 'P_22', '1' ) );
			$nst->appendChild( $dom->createElementNS( self::FA_NS, 'P_42_5', '2' ) );
			$pojazd = $dom->createElementNS( self::FA_NS, 'NowySrodekTransportu' );
			$nst->appendChild( $pojazd );
			$pojazd->appendChild( $dom->createElementNS( self::FA_NS, 'P_22A', $nst_date ) );
			$pojazd->appendChild( $dom->createElementNS( self::FA_NS, 'P_NrWierszaNST', '1' ) );
			$przebieg = trim( (string) ( $nst_data['przebieg'] ?? '' ) );
			$pojazd->appendChild( $dom->createElementNS( self::FA_NS, 'P_22B', '' !== $przebieg ? $przebieg : '0' ) );
			$vin = $this->t( trim( (string) ( $nst_data['vin'] ?? '' ) ) );
			if ( '' !== $vin ) {
				$pojazd->appendChild( $dom->createElementNS( self::FA_NS, 'P_22B1', $vin ) );
			}
		} else {
			$nst->appendChild( $dom->createElementNS( self::FA_NS, 'P_22N', '1' ) );
		}

		$adn->appendChild( $dom->createElementNS( self::FA_NS, 'P_23', $this->wybor12( $data['adnotacje']['P_23'] ?? null ) ) );

		// PMarzy — procedura marży (art. 119/120). Choice: P_PMarzy=1 + rodzaj albo P_PMarzyN=1.
		$pm = $dom->createElementNS( self::FA_NS, 'PMarzy' );
		$adn->appendChild( $pm );
		$marza_kind = strtolower( trim( (string) ( $data['adnotacje']['P_PMarzy'] ?? '' ) ) );
		$marza_tag  = match ( $marza_kind ) {
			'turystyka', 'biuro', 'biuro_podrozy' => 'P_PMarzy_2',
			'uzywane', 'towary_uzywane'           => 'P_PMarzy_3_1',
			'sztuka', 'dziela_sztuki'             => 'P_PMarzy_3_2',
			'kolekcjonerskie', 'antyki'           => 'P_PMarzy_3_3',
			default                               => '',
		};
		if ( '' !== $marza_tag ) {
			$pm->appendChild( $dom->createElementNS( self::FA_NS, 'P_PMarzy', '1' ) );
			$pm->appendChild( $dom->createElementNS( self::FA_NS, $marza_tag, '1' ) );
		} else {
			$pm->appendChild( $dom->createElementNS( self::FA_NS, 'P_PMarzyN', '1' ) );
		}

		// 6. RodzajFaktury (REQUIRED): KOR dla korekty, inaczej rodzaj_faktury (domyślnie VAT).
		//    UWAGA: „FP" (faktura do paragonu) to ZNACZNIK, NIE RodzajFaktury. W FA(3) dozwolone
		//    sa tylko VAT/KOR/ZAL/ROZ/UPR/KOR_ZAL/KOR_ROZ — wpisanie 'FP' = blad walidacji XSD.
		$is_correction = ! empty( $data['correction'] ) && is_array( $data['correction'] );
		if ( $is_correction ) {
			// Podtyp korekty zależy od rodzaju faktury pierwotnej: KOR (zwykła) /
			// KOR_ZAL (korekta zaliczkowej) / KOR_ROZ (korekta rozliczeniowej).
			$rodzaj = strtoupper( (string) ( $data['rodzaj_faktury'] ?? 'KOR' ) );
			if ( ! in_array( $rodzaj, [ 'KOR', 'KOR_ZAL', 'KOR_ROZ' ], true ) ) {
				$rodzaj = 'KOR';
			}
		} else {
			$rodzaj = strtoupper( (string) ( $data['rodzaj_faktury'] ?? 'VAT' ) );
			if ( ! in_array( $rodzaj, [ 'VAT', 'ZAL', 'ROZ', 'UPR' ], true ) ) {
				$rodzaj = 'VAT';
			}
		}
		$fa->appendChild( $dom->createElementNS( self::FA_NS, 'RodzajFaktury', $rodzaj ) );

		// 6a. Blok korekty — po RodzajFaktury, przed DodatkowyOpis/FaWiersz (XSD sequence).
		if ( $is_correction ) {
			$corr = $data['correction'];
			$reason = trim( (string) ( $corr['reason'] ?? '' ) );
			if ( $reason !== '' ) {
				$fa->appendChild( $dom->createElementNS( self::FA_NS, 'PrzyczynaKorekty', $this->t( $reason ) ) );
			}
			// TypKorekty: 1 = korekta skutkująca w dacie ujęcia faktury pierwotnej (domyślnie)
			$fa->appendChild( $dom->createElementNS( self::FA_NS, 'TypKorekty', (string) ( $corr['type'] ?? '1' ) ) );

			$dfk = $dom->createElementNS( self::FA_NS, 'DaneFaKorygowanej' );
			$fa->appendChild( $dfk );
			$dfk->appendChild( $dom->createElementNS( self::FA_NS, 'DataWystFaKorygowanej', $this->date( (string) ( $corr['original_date'] ?? gmdate( 'Y-m-d' ) ) ) ) );
			$dfk->appendChild( $dom->createElementNS( self::FA_NS, 'NrFaKorygowanej', $this->t( (string) ( $corr['original_number'] ?? '' ) ) ) );
			$orig_ksef = trim( (string) ( $corr['original_ksef_number'] ?? '' ) );
			if ( $orig_ksef !== '' ) {
				$dfk->appendChild( $dom->createElementNS( self::FA_NS, 'NrKSeF', '1' ) );
				$dfk->appendChild( $dom->createElementNS( self::FA_NS, 'NrKSeFFaKorygowanej', $this->t( $orig_ksef ) ) );
			} else {
				// faktura korygowana wystawiona poza KSeF
				$dfk->appendChild( $dom->createElementNS( self::FA_NS, 'NrKSeFN', '1' ) );
			}
		}

		// 6a2. FP (faktura do paragonu, art. 109 ust. 3d) + TP (powiązania, § 10 ust. 4 pkt 3).
		//      Fa-level, kolejność XSD: ...→ (zaliczki) → FP → TP → DodatkowyOpis.
		if ( ! empty( $data['markers']['FP'] ) ) {
			$fa->appendChild( $dom->createElementNS( self::FA_NS, 'FP', '1' ) );
		}
		if ( ! empty( $data['markers']['TP'] ) ) {
			$fa->appendChild( $dom->createElementNS( self::FA_NS, 'TP', '1' ) );
		}

		// 6b. DodatkowyOpis (PRZED FaWiersz — wymóg XSD kolejności)
		$ksef_notes = $data['ksef_custom_notes'] ?? [];
		if ( is_array( $ksef_notes ) ) {
			foreach ( $ksef_notes as $kv ) {
				$k = trim( (string) ( $kv['key'] ?? '' ) );
				$v = trim( (string) ( $kv['value'] ?? '' ) );
				if ( $k === '' || $v === '' ) {
					continue;
				}
				$ad_opis = $dom->createElementNS( self::FA_NS, 'DodatkowyOpis' );
				$fa->appendChild( $ad_opis );
				$ad_opis->appendChild( $dom->createElementNS( self::FA_NS, 'Klucz', $this->t( $k ) ) );
				$ad_opis->appendChild( $dom->createElementNS( self::FA_NS, 'Wartosc', $this->t( $v ) ) );
			}
		}

		// 7. FaWiersz* (po RodzajFaktury / DodatkowyOpis)
		foreach ( $data['items'] ?? [] as $i => $item ) {
			$wiersz = $dom->createElementNS( self::FA_NS, 'FaWiersz' );
			$fa->appendChild( $wiersz );
			$wiersz->appendChild( $dom->createElementNS( self::FA_NS, 'NrWierszaFa', (string) ( $i + 1 ) ) );
			// P_7 (nazwa). PKWiU/CN to OSOBNE elementy w XSD — nie P_7A.
			$wiersz->appendChild( $dom->createElementNS( self::FA_NS, 'P_7', $this->t( (string) ( $item['description'] ?? '' ) ) ) );
			if ( ! empty( $item['pkwiu'] ) ) {
				// Heurystyka: PKWiU jest 7-cyfrowy z kropkami (np. 62.01.11.0), CN jest 8-cyfrowy bez kropek (np. 84713000).
				$code = trim( (string) $item['pkwiu'] );
				$tag  = ( strpos( $code, '.' ) !== false ) ? 'PKWiU' : ( preg_match( '/^\d{6,10}$/', $code ) ? 'CN' : 'PKWiU' );
				$wiersz->appendChild( $dom->createElementNS( self::FA_NS, $tag, $this->t( $code ) ) );
			}
			$wiersz->appendChild( $dom->createElementNS( self::FA_NS, 'P_8A', $this->t( (string) ( $item['unit'] ?? 'szt.' ) ) ) );
			$wiersz->appendChild( $dom->createElementNS( self::FA_NS, 'P_8B', $this->num( (float) ( $item['quantity'] ?? 1 ), 4 ) ) );
			$wiersz->appendChild( $dom->createElementNS( self::FA_NS, 'P_9A', $this->num( (float) ( $item['unit_price_net'] ?? 0 ), 2 ) ) );
			// P_10 = wartość rabatu (jeśli zastosowany)
			$discount_pct = (float) ( $item['discount_pct'] ?? 0 );
			if ( $discount_pct > 0 ) {
				$gross_pre_disc = (float) ( $item['quantity'] ?? 0 ) * (float) ( $item['unit_price_net'] ?? 0 );
				$discount_amt   = round( $gross_pre_disc * $discount_pct / 100, 2 );
				$wiersz->appendChild( $dom->createElementNS( self::FA_NS, 'P_10', $this->num( $discount_amt, 2 ) ) );
			}
			$wiersz->appendChild( $dom->createElementNS( self::FA_NS, 'P_11', $this->num( (float) ( $item['line_net'] ?? 0 ), 2 ) ) );
			$rate_raw = $item['vat_rate'] ?? 23;
			$kind     = is_string( $rate_raw ) && in_array( strtolower( (string) $rate_raw ), [ 'zw','np','oo' ], true ) ? strtolower( (string) $rate_raw ) : null;
			$wiersz->appendChild( $dom->createElementNS( self::FA_NS, 'P_12', $this->vat_rate_label( is_numeric( $rate_raw ) ? (float) $rate_raw : 0, $kind ) ) );

			// GTU per linia
			if ( ! empty( $item['gtu'] ) ) {
				$wiersz->appendChild( $dom->createElementNS( self::FA_NS, 'GTU', (string) $item['gtu'] ) );
			}

			// Procedura (XSD: po GTU). Per-pozycja $item['procedura'] albo znacznik OSS → WSTO_EE
			// (wewnątrzwspólnotowa sprzedaż towarów na odległość — procedura unijna OSS).
			$proc = ! empty( $item['procedura'] )
				? strtoupper( trim( (string) $item['procedura'] ) )
				: ( ! empty( $data['markers']['OSS'] ) ? 'WSTO_EE' : '' );
			if ( in_array( $proc, [ 'WSTO_EE', 'IED', 'TT_D', 'I_42', 'I_63', 'B_SPV', 'B_SPV_DOSTAWA', 'B_MPV_PROWIZJA' ], true ) ) {
				$wiersz->appendChild( $dom->createElementNS( self::FA_NS, 'Procedura', $proc ) );
			}
		}

		// 7b. Rozliczenie — kaucja / opakowania zwrotne jako Obciazenie (XSD: po FaWiersz, przed Platnosc).
		$deposit_amt = round( (float) ( $data['deposit_amount'] ?? 0 ), 2 );
		if ( $deposit_amt > 0 ) {
			$gross_amt = round( (float) ( $data['total_gross'] ?? 0 ), 2 );
			$roz = $dom->createElementNS( self::FA_NS, 'Rozliczenie' );
			$fa->appendChild( $roz );
			$obc = $dom->createElementNS( self::FA_NS, 'Obciazenia' );
			$roz->appendChild( $obc );
			$obc->appendChild( $dom->createElementNS( self::FA_NS, 'Kwota', $this->num( $deposit_amt, 2 ) ) );
			$obc->appendChild( $dom->createElementNS( self::FA_NS, 'Powod', $this->t( (string) ( $data['deposit_label'] ?? '' ) !== '' ? (string) $data['deposit_label'] : 'Kaucja / opakowania zwrotne' ) ) );
			$roz->appendChild( $dom->createElementNS( self::FA_NS, 'SumaObciazen', $this->num( $deposit_amt, 2 ) ) );
			$roz->appendChild( $dom->createElementNS( self::FA_NS, 'DoZaplaty', $this->num( $gross_amt + $deposit_amt, 2 ) ) );
		}

		// 8. Platnosc (opcjonalnie) — XSD: choice(Zaplacono+DataZaplaty | partial), TerminPlatnosci{Termin,TerminOpis?}*, choice(FormaPlatnosci|...)
		if ( ! empty( $data['payment'] ) ) {
			$plat = $dom->createElementNS( self::FA_NS, 'Platnosc' );
			$fa->appendChild( $plat );

			// Choice XSD: Zaplacono+DataZaplaty (całość) ALBO ZnacznikZaplatyCzesciowej+ZaplataCzesciowa* (częściowo).
			$paid = (float) ( $data['payment']['paid'] ?? 0 );
			$partials = is_array( $data['payment']['partial'] ?? null ) ? $data['payment']['partial'] : [];
			$partials = array_values( array_filter( array_map( static function ( $p ) {
				$a = round( (float) ( $p['amount'] ?? 0 ), 2 );
				return $a > 0 ? [ 'amount' => $a, 'date' => (string) ( $p['date'] ?? '' ) ] : null;
			}, is_array( $partials ) ? $partials : [] ) ) );
			if ( $paid > 0 ) {
				$plat->appendChild( $dom->createElementNS( self::FA_NS, 'Zaplacono', '1' ) );
				$pay_date = ! empty( $data['payment']['paid_date'] )
					? $this->date( (string) $data['payment']['paid_date'] )
					: $this->date( (string) ( $data['issue_date'] ?? gmdate( 'Y-m-d' ) ) );
				$plat->appendChild( $dom->createElementNS( self::FA_NS, 'DataZaplaty', $pay_date ) );
			} elseif ( ! empty( $partials ) ) {
				// ZnacznikZaplatyCzesciowej: 1 = zapłacono w części; 2 = całość zapłacona w częściach.
				$sum_partial = array_sum( array_column( $partials, 'amount' ) );
				$gross_total = (float) ( $data['total_gross'] ?? $data['total_payable'] ?? 0 );
				$znacznik    = ( $gross_total > 0 && $sum_partial + 0.01 >= $gross_total ) ? '2' : '1';
				$plat->appendChild( $dom->createElementNS( self::FA_NS, 'ZnacznikZaplatyCzesciowej', $znacznik ) );
				$default_date = $this->date( (string) ( $data['issue_date'] ?? gmdate( 'Y-m-d' ) ) );
				foreach ( $partials as $pp ) {
					$zc = $dom->createElementNS( self::FA_NS, 'ZaplataCzesciowa' );
					$plat->appendChild( $zc );
					$zc->appendChild( $dom->createElementNS( self::FA_NS, 'KwotaZaplatyCzesciowej', $this->num( (float) $pp['amount'], 2 ) ) );
					$zc->appendChild( $dom->createElementNS( self::FA_NS, 'DataZaplatyCzesciowej', '' !== $pp['date'] ? $this->date( (string) $pp['date'] ) : $default_date ) );
				}
			}

			// TerminPlatnosci{ Termin, TerminOpis? }
			if ( ! empty( $data['payment']['due_date'] ) ) {
				$term = $dom->createElementNS( self::FA_NS, 'TerminPlatnosci' );
				$plat->appendChild( $term );
				$term->appendChild( $dom->createElementNS( self::FA_NS, 'Termin', $this->date( (string) $data['payment']['due_date'] ) ) );
			}

			// FormaPlatnosci (choice — uproszczone do standardowej formy)
			$plat->appendChild( $dom->createElementNS( self::FA_NS, 'FormaPlatnosci', (string) ( $data['payment']['form'] ?? '6' ) ) );

			// RachunekBankowy — gdy wybrano rachunek dla faktury
			$bank = $data['bank_account'] ?? null;
			if ( is_array( $bank ) && ! empty( $bank['account_number'] ) ) {
				$rach = $dom->createElementNS( self::FA_NS, 'RachunekBankowy' );
				$plat->appendChild( $rach );
				$rach->appendChild( $dom->createElementNS( self::FA_NS, 'NrRB', preg_replace( '/\s+/', '', (string) $bank['account_number'] ) ) );
				if ( ! empty( $bank['swift'] ) ) {
					$rach->appendChild( $dom->createElementNS( self::FA_NS, 'SWIFT', $this->t( (string) $bank['swift'] ) ) );
				}
				if ( ! empty( $bank['bank_name'] ) ) {
					$rach->appendChild( $dom->createElementNS( self::FA_NS, 'NazwaBanku', $this->t( (string) $bank['bank_name'] ) ) );
				}
			}
		}

		// 9. WarunkiTransakcji → Zamowienia (Nr zamówienia + Data zamówienia)
		// Uwaga: WZ trafia jako element bezpośrednio dziecko Fa (już emitowane wcześniej).
		// NumerKlienta NIE jest dzieckiem Fa — to NrKlienta w Podmiot2 (emitowane w add_podmiot2 gdy buyer ma customer_no).
		$related = $data['related_docs'] ?? [];
		if ( ! empty( $related['order']['number'] ) ) {
			$wt = $dom->createElementNS( self::FA_NS, 'WarunkiTransakcji' );
			$fa->appendChild( $wt );
			$zam = $dom->createElementNS( self::FA_NS, 'Zamowienia' );
			$wt->appendChild( $zam );
			if ( ! empty( $related['order']['date'] ) ) {
				$zam->appendChild( $dom->createElementNS( self::FA_NS, 'DataZamowienia', $this->date( (string) $related['order']['date'] ) ) );
			}
			$zam->appendChild( $dom->createElementNS( self::FA_NS, 'NrZamowienia', $this->t( (string) $related['order']['number'] ) ) );
		}
	}

	/**
	 * Podmiot3 — adres dostawy (rola: Odbiorca). Wstawiany przed Fa.
	 *
	 * @param array<string,mixed>|null $delivery
	 */
	private function maybe_add_podmiot3( \DOMDocument $dom, \DOMElement $root, ?array $delivery ): void {
		if ( ! is_array( $delivery ) ) {
			return;
		}
		$has = trim( (string) ( $delivery['name'] ?? '' ) . (string) ( $delivery['street'] ?? '' ) . (string) ( $delivery['city'] ?? '' ) ) !== '';
		if ( ! $has ) {
			return;
		}
		$pm3 = $dom->createElementNS( self::FA_NS, 'Podmiot3' );
		$root->appendChild( $pm3 );
		$dane3 = $dom->createElementNS( self::FA_NS, 'DaneIdentyfikacyjne' );
		$pm3->appendChild( $dane3 );
		$dane3->appendChild( $dom->createElementNS( self::FA_NS, 'BrakID', '1' ) );
		if ( ! empty( $delivery['name'] ) ) {
			$dane3->appendChild( $dom->createElementNS( self::FA_NS, 'Nazwa', $this->t( (string) $delivery['name'] ) ) );
		}
		$this->add_adres( $dom, $pm3, [
			'country'     => $delivery['country'] ?? 'PL',
			'street'      => $delivery['street'] ?? '',
			'city'        => $delivery['city'] ?? '',
			'postal_code' => $delivery['postal_code'] ?? '',
		] );
		// TRolaPodmiotu3 (integer): 1=Faktor, 2=Odbiorca, 3=Podmiot pierwotny, 4=Procedura uproszczona
		$pm3->appendChild( $dom->createElementNS( self::FA_NS, 'Rola', '2' ) );
	}

	/* ===================== Helpers ===================== */

	private function only_digits( string $s ): string {
		return (string) preg_replace( '/\D+/', '', $s );
	}

	private function t( string $s ): string {
		// Sanityzacja tekstu do XML
		$s = trim( $s );
		// usuń kontrolne (poza tab/newline)
		$s = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/u', '', $s ) ?? '';
		return $s;
	}

	private function date( string $s ): string {
		$ts = strtotime( $s );
		return $ts ? gmdate( 'Y-m-d', $ts ) : $s;
	}

	private function num( float $v, int $decimals ): string {
		return number_format( $v, $decimals, '.', '' );
	}

	/**
	 * Etykieta stawki VAT zgodna z XSD TStawkaPodatku:
	 * '23' | '22' | '8' | '7' | '5' | '4' | '3' | '0 KR' | '0 WDT' | '0 EX' | 'zw' | 'oo' | 'np'
	 *
	 * @param float        $rate     liczba 0..23 lub <0 dla 'zw'
	 * @param string|null  $kind     '0 KR'|'0 WDT'|'0 EX' (gdy rate=0) lub 'zw'/'np'/'oo'/null
	 */
	private function vat_rate_label( float $rate, ?string $kind = null ): string {
		// XSD TStawkaPodatku enum: 23, 22, 8, 7, 5, 4, 3, "0 KR", "0 WDT", "0 EX", "zw", "oo", "np I", "np II"
		if ( $kind !== null && $kind !== '' ) {
			$kind = strtolower( $kind );
			if ( 'np' === $kind ) {
				return 'np I'; // "nie podlega" — domyślnie I (poza terytorium kraju)
			}
			if ( 'zw' === $kind || 'oo' === $kind ) {
				return $kind;
			}
		}
		if ( $rate < 0 ) {
			return 'zw';
		}
		$n = (int) round( $rate );
		if ( $n === 0 ) {
			return '0 KR'; // domyślnie krajowa stawka 0%
		}
		return (string) $n;
	}

	private function vat_sum_net_tag( float $rate ): ?string {
		return match ( (int) round( $rate ) ) {
			23 => 'P_13_1',  // suma netto 23%
			8  => 'P_13_2',  // suma netto 8%
			5  => 'P_13_3',  // suma netto 5%
			0  => 'P_13_6_1',  // suma netto 0% krajowa (P_13_6_1=KR, _6_2=WDT, _6_3=EX)
			default => null,
		};
	}

	private function vat_sum_vat_tag( float $rate ): ?string {
		return match ( (int) round( $rate ) ) {
			23 => 'P_14_1',
			8  => 'P_14_2',
			5  => 'P_14_3',
			0  => null, // 0% nie ma kwoty VAT
			default => null,
		};
	}

	/**
	 * TWybor1_2 — XSD wymaga wartości "1" (TAK) lub "2" (NIE dotyczy).
	 * Default: "2" gdy brak danych.
	 */
	private function wybor12( mixed $v ): string {
		$s = trim( (string) $v );
		// Akceptujemy też legacy "0" / "1"  i mapujemy: "1"→"1", reszta→"2"
		return $s === '1' ? '1' : '2';
	}

	/**
	 * Czy faktura zawiera pozycję ze stawką zwolnioną (zw)? Sprawdza zarówno
	 * jawne pole stawki w pozycjach, jak i etykietę 'zw' w vat_summary.
	 *
	 * @param array<string,mixed> $data
	 */
	private function has_zw_item( array $data ): bool {
		foreach ( (array) ( $data['items'] ?? [] ) as $item ) {
			$rate = $item['vat_rate'] ?? ( $item['rate'] ?? null );
			if ( is_string( $rate ) && 'zw' === strtolower( trim( $rate ) ) ) {
				return true;
			}
		}
		foreach ( (array) ( $data['vat_summary'] ?? [] ) as $vat ) {
			$rate = $vat['rate'] ?? null;
			if ( is_string( $rate ) && 'zw' === strtolower( trim( $rate ) ) ) {
				return true;
			}
		}
		return false;
	}
}
