<?php
/**
 * Główna klasa wtyczki KSEF - WordPress
 *
 * @package KSEF\WordPress
 */

namespace KSEF\WordPress;

// phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders, WordPress.DB.SlowDBQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Wlasne tabele gksef_*: wartosci przez $wpdb->prepare()/esc_like(), nazwy tabel z $wpdb->prefix, paginacja rzutowana (int); nonce weryfikowany w handlerze (check_ajax_referer/check_admin_referer).

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Plugin class
 */
class Plugin {

	/**
	 * Singleton instance
	 *
	 * @var self
	 */
	private static $instance = null;

	/**
	 * Get singleton instance
	 *
	 * @return self
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Hook aktywacji
	 *
	 * @return void
	 */
	public static function activate() {
		// Zaladuj Installer
		require_once KSEF_WORDPRESS_PATH . 'includes/class-installer.php';
		Installer::install();

		// Zaplanuj cron dla KSeF
		if ( ! wp_next_scheduled( 'ksef_poll_status_cron' ) ) {
			wp_schedule_event( time(), 'twicedaily', 'ksef_poll_status_cron' );
		}

		flush_rewrite_rules();
	}

	/**
	 * Hook deaktywacji
	 *
	 * @return void
	 */
	public static function deactivate() {
		wp_clear_scheduled_hook( 'ksef_poll_status_cron' );
		flush_rewrite_rules();
	}

	/**
	 * Inicjalizacja wtyczki
	 *
	 * @return void
	 */
	public static function init() {
		$plugin = self::instance();

		// Sprawdź czy WooCommerce jest aktywny
		if ( ! class_exists( 'WooCommerce' ) ) {
			add_action( 'admin_notices', [ $plugin, 'woocommerce_required_notice' ] );
			return;
		}

		// Auto-migracja schema DB przy update wersji wtyczki (dbDelta jest idempotentne,
		// dodaje tylko brakujące kolumny/indeksy).
		$installed_version = get_option( 'ksef_db_version', '0' );
		if ( version_compare( (string) $installed_version, KSEF_WORDPRESS_VERSION, '<' ) ) {
			require_once KSEF_WORDPRESS_PATH . 'includes/class-installer.php';
			Installer::install();
			update_option( 'ksef_db_version', KSEF_WORDPRESS_VERSION, false );
		}

		// Zaladuj klasy
		$plugin->load_dependencies();

		// Hook dla administracji
		if ( is_admin() ) {
			add_action( 'admin_menu', [ $plugin, 'register_admin_menu' ] );
			add_action( 'admin_notices', [ $plugin, 'support_notice' ] );
			add_action( 'admin_enqueue_scripts', [ $plugin, 'enqueue_admin_assets' ] );
			// Link „Ustawienia" jako pierwszy w wierszu wtyczki na liście wtyczek.
			if ( defined( 'KSEF_WORDPRESS_FILE' ) ) {
				add_filter( 'plugin_action_links_' . plugin_basename( KSEF_WORDPRESS_FILE ), [ $plugin, 'add_settings_action_link' ] );
			}
			// Strony admina wtyczki NIE mogą być cache'owane (lista po delete to stały URL ?msg=deleted).
			add_action( 'admin_init', [ $plugin, 'send_admin_nocache' ] );
		}

		// KSeF cron
		add_action( 'ksef_poll_status_cron', [ $plugin, 'poll_ksef_status_cron' ] );

		// Powiadomienie o braku potwierdzenia KSeF po 4h (funkcja Pro) — cron godzinowy.
		add_action( 'ksef_unconfirmed_check', [ $plugin, 'check_unconfirmed_invoices' ] );
		if ( ! wp_next_scheduled( 'ksef_unconfirmed_check' ) ) {
			wp_schedule_event( time() + 300, 'hourly', 'ksef_unconfirmed_check' );
		}

		// Tlumaczenia laduje WordPress automatycznie (text domain = slug wtyczki, od WP 4.6).
	}

	/**
	 * Zaladuj zależności
	 *
	 * @return void
	 */
	private function load_dependencies() {
		// Szyfrowanie
		require_once KSEF_WORDPRESS_PATH . 'includes/class-crypto.php';

		// Order_Mapper — mapowanie WC_Order → struktura FA(3). Używany przez V2\Invoice_Sender.
		// (Pozostałe stare klasy api/invoices/purchase usunięte w v1.6.8 — zastąpione przez includes/v2/.)
		require_once KSEF_WORDPRESS_PATH . 'includes/invoices/class-order-mapper.php';

		// Admin (Etap 6)
		require_once KSEF_WORDPRESS_PATH . 'includes/admin/class-modern-shell.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/admin/class-growth-notices.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/admin/class-outgoing-invoices-list.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/admin/class-contractor-mappings-page.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/admin/class-contractors-page.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/admin/class-monitoring-dashboard.php';

		// Email, Export, Auto-Link (Etap 7)
		require_once KSEF_WORDPRESS_PATH . 'includes/class-email-notifier.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/class-invoice-export.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/class-invoice-auto-linker.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/admin/class-email-settings.php';

		// Konfiguracja KSeF + Ustawienia ogólne (Etap 8 — uzupełnienie v1.0)
		require_once KSEF_WORDPRESS_PATH . 'includes/admin/class-credentials-page.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/admin/class-settings-page.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/admin/class-manual-invoice-page.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/admin/class-onboarding-page.php';

		// WC Integration (Etap 8 — auto-trigger + NIP w checkout + metabox)
		require_once KSEF_WORDPRESS_PATH . 'includes/wc/class-checkout-field.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/wc/class-billing-address-mode.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/wc/class-order-trigger.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/wc/class-order-metabox.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/wc/class-wc-bootstrap.php';

		// KSeF API v2 (Etap 9 — przepisanie zgodne ze spec MF)
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-ksef-client.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-public-keys.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-xades-signer.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-invoice-numbering.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/class-payments-repository.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/class-expiry-monitor.php';
		Expiry_Monitor::register();
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-auth.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-fa3-builder.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-invoice-sender.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-upo-parser.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-status-poller.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-fa3-parser.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-contractors-sync.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-bootstrap.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/class-secure-storage.php';
		require_once KSEF_WORDPRESS_PATH . 'includes/class-capabilities.php';

		// Bootstrap modułów
		\KSEF\WordPress\Admin\Settings_Page::register();
		\KSEF\WordPress\Admin\Credentials_Page::register_ajax();
		\KSEF\WordPress\Admin\Manual_Invoice_Page::register_ajax();
		\KSEF\WordPress\Admin\Onboarding_Page::register_ajax();
		\KSEF\WordPress\WC\WC_Bootstrap::register();
		\KSEF\WordPress\V2\Bootstrap::register();
		\KSEF\WordPress\Capabilities::register();
		\KSEF\WordPress\Admin\Growth_Notices::register();
		// Proaktywnie utwórz katalog ksef-private/ z .htaccess deny + index.php blank.
		\KSEF\WordPress\Secure_Storage::bootstrap_base();

		// DB schema migration: dorzuca brakujące kolumny w gksef_outgoing_invoices.
		// Wersjonujemy w wp_options — pojedyncze wywołanie ALTER per release.
		$schema_v = (int) get_option( 'ksef_outgoing_schema_v', 0 );
		if ( $schema_v < 8 ) {
			require_once KSEF_WORDPRESS_PATH . 'includes/class-installer.php';
			Installer::ensure_outgoing_columns();
			update_option( 'ksef_outgoing_schema_v', 8 );
		}
		// v9 — zabezpiecz katalog cache PDF (.htaccess Deny from all) dla starych instalacji.
		if ( (int) get_option( 'ksef_pdf_cache_locked_v', 0 ) < 1 ) {
			require_once KSEF_WORDPRESS_PATH . 'includes/class-invoice-pdf.php';
			\KSEF\WordPress\Invoice_PDF::lock_existing_cache();
			update_option( 'ksef_pdf_cache_locked_v', 1, false );
		}
		// v10 — zaszyfruj istniejące plaintext payload_json / generated_xml / raw_xml.
		if ( (int) get_option( 'ksef_sensitive_encrypted_v', 0 ) < 1 ) {
			self::encrypt_existing_sensitive_rows();
			update_option( 'ksef_sensitive_encrypted_v', 1, false );
		}
		// v11 — wyzeruj zapisany emergency-disable z poprzednich wersji
		// (od 1.2.0 plugin nie blokuje UI, tylko gating'uje Pro features).
		if ( (int) get_option( 'ksef_free_tier_unlocked_v', 0 ) < 1 ) {
			delete_option( 'ksef_emergency_disabled' );
			delete_transient( 'ksef_license_last_check' );
			update_option( 'ksef_free_tier_unlocked_v', 1, false );
		}
		// Cron: codzienne czyszczenie cache PDF starszego niż 30 dni.
		if ( ! wp_next_scheduled( 'ksef_cleanup_pdf_cache' ) ) {
			wp_schedule_event( time() + 600, 'daily', 'ksef_cleanup_pdf_cache' );
		}
		add_action( 'ksef_cleanup_pdf_cache', [ self::class, 'cron_cleanup_pdf_cache' ] );
	}

	/**
	 * Migracja: zaszyfrowanie istniejących plaintext rekordów w tabelach
	 * gksef_outgoing_invoices (payload_json, generated_xml) i
	 * gksef_incoming_invoices (raw_xml). Bezpieczne wywoływać wielokrotnie —
	 * pomija rekordy już z sentinelem KSEF_ENC1:.
	 */
	private static function encrypt_existing_sensitive_rows(): void {
		global $wpdb;
		$sentinel = Crypto::STORAGE_SENTINEL;
		$prefix   = $wpdb->esc_like( $sentinel ) . '%';

		// Outgoing — payload_json + generated_xml
		$out = $wpdb->prefix . 'gksef_outgoing_invoices';
		if ( $wpdb->get_var( "SHOW TABLES LIKE '{$out}'" ) === $out ) {
			$rows = $wpdb->get_results( $wpdb->prepare(
				"SELECT id, payload_json, generated_xml FROM {$out}
				 WHERE ( payload_json IS NOT NULL AND payload_json <> '' AND payload_json NOT LIKE %s )
				    OR ( generated_xml IS NOT NULL AND generated_xml <> '' AND generated_xml NOT LIKE %s )",
				$prefix, $prefix
			), ARRAY_A );
			foreach ( (array) $rows as $r ) {
				$update = [];
				if ( ! empty( $r['payload_json'] ) && 0 !== strpos( (string) $r['payload_json'], $sentinel ) ) {
					$update['payload_json'] = Crypto::encrypt_for_storage( $r['payload_json'] );
				}
				if ( ! empty( $r['generated_xml'] ) && 0 !== strpos( (string) $r['generated_xml'], $sentinel ) ) {
					$update['generated_xml'] = Crypto::encrypt_for_storage( $r['generated_xml'] );
				}
				if ( $update ) {
					$wpdb->update( $out, $update, [ 'id' => (int) $r['id'] ] );
				}
			}
		}

		// Incoming — raw_xml
		$in = $wpdb->prefix . 'gksef_incoming_invoices';
		if ( $wpdb->get_var( "SHOW TABLES LIKE '{$in}'" ) === $in ) {
			$rows = $wpdb->get_results( $wpdb->prepare(
				"SELECT id, raw_xml FROM {$in}
				 WHERE raw_xml IS NOT NULL AND raw_xml <> '' AND raw_xml NOT LIKE %s",
				$prefix
			), ARRAY_A );
			foreach ( (array) $rows as $r ) {
				if ( ! empty( $r['raw_xml'] ) && 0 !== strpos( (string) $r['raw_xml'], $sentinel ) ) {
					$wpdb->update( $in, [ 'raw_xml' => Crypto::encrypt_for_storage( $r['raw_xml'] ) ], [ 'id' => (int) $r['id'] ] );
				}
			}
		}
	}

	/**
	 * Cron: czyść cache PDF starszy niż 30 dni z wp-content/uploads/ksef-pdf/.
	 * PDF jest tylko cache — `Invoice_PDF::stream()` regeneruje on-the-fly z DB.
	 */
	public static function cron_cleanup_pdf_cache(): void {
		$uploads = wp_upload_dir();
		if ( empty( $uploads['basedir'] ) ) {
			return;
		}
		$root = trailingslashit( $uploads['basedir'] ) . 'ksef-pdf';
		if ( ! is_dir( $root ) ) {
			return;
		}
		$cutoff = time() - 30 * DAY_IN_SECONDS;
		$iter = new \RecursiveIteratorIterator(
			new \RecursiveDirectoryIterator( $root, \FilesystemIterator::SKIP_DOTS ),
			\RecursiveIteratorIterator::CHILD_FIRST
		);
		foreach ( $iter as $entry ) {
			if ( $entry->isFile() && '.pdf' === strtolower( substr( (string) $entry->getFilename(), -4 ) ) ) {
				if ( $entry->getMTime() < $cutoff ) {
					@unlink( $entry->getPathname() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- lokalne operacje na wlasnych plikach wtyczki (cache/upload).
				}
			}
		}
	}

	/**
	 * Wyślij nagłówki „no-cache" dla stron admina wtyczki (page=ksef-*), zanim WordPress
	 * wyśle nagłówek HTML. Bez tego przeglądarka potrafi pokazać zcache'owaną listę po
	 * usunięciu (redirect zawsze leci na ten sam URL ?page=ksef-contractors&msg=deleted).
	 *
	 * @return void
	 */
	public function send_admin_nocache() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- tylko routing nagłówka cache, brak operacji na danych formularza.
		$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
		if ( 0 === strpos( $page, 'ksef-' ) && ! headers_sent() ) {
			nocache_headers();
		}
	}

	/**
	 * Baner wsparcia u góry każdej strony modułu KSeF (admin).
	 */
	public function support_notice(): void {
		$page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- tylko odczyt parametru strony do warunku wyswietlenia.
		if ( 0 !== strpos( $page, 'ksef' ) ) {
			return;
		}
		echo '<div style="margin:14px 20px 4px 2px;padding:12px 16px;background:linear-gradient(135deg,#eef2ff,#f5f3ff);border:1px solid #c7d2fe;border-left:4px solid #5b5bd6;border-radius:10px;font-size:13.5px;line-height:1.5;color:#1e293b;display:flex;align-items:center;gap:11px;flex-wrap:wrap;">'
			. '<span style="font-size:20px;line-height:1;">💬</span>'
			. '<span>' . esc_html__( 'Masz pytania albo coś nie działa? Chcesz, żebyśmy poprawili ten moduł albo zbudowali dla Ciebie inny — napisz, chętnie pomożemy:', 'prostafaktura-ksef' )
			. ' <a href="mailto:hello@prostafakturaksef.pl" style="font-weight:600;color:#4338ca;text-decoration:none;">hello@prostafakturaksef.pl</a></span>'
			. '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- statyczny HTML; treść przez esc_html__, adres e-mail stały.
	}

	/**
	 * „Ustawienia" jako pierwszy link akcji w wierszu wtyczki (lista wtyczek) → ekran ustawień.
	 *
	 * @param array<int,string> $links
	 * @return array<int,string>
	 */
	public function add_settings_action_link( $links ) {
		if ( ! is_array( $links ) ) {
			return $links;
		}
		$settings = '<a href="' . esc_url( admin_url( 'admin.php?page=ksef-settings' ) ) . '">' . esc_html__( 'Ustawienia', 'prostafaktura-ksef' ) . '</a>';
		array_unshift( $links, $settings );
		return $links;
	}

	/**
	 * Rejestruj menu administracyjne
	 *
	 * @return void
	 */
	public function register_admin_menu() {
		// Wersja darmowa (.org): wystawianie faktur sprzedaży bez limitów; brak
		// funkcji Pro (zakup/sync/raporty/auto-WooCommerce) — te są w wersji Pro.
		// Capability dla głównego entry-pointa (Monitoring) — to najmniej wrażliwy
		// widok, więc używamy 'ksef_view_monitoring'. Jeśli user nie ma ŻADNEGO
		// cap KSeF, menu i tak go nie zobaczy.
		// Ikona menu: faktura (dokument z pozycjami) jako inline SVG — zawsze
		// widoczna niezależnie od wersji Dashicons, w kolorze menu admina (#a7aaad).
		$menu_icon = 'data:image/svg+xml;base64,' . base64_encode(
			'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><g fill="none" stroke="#a7aaad" stroke-width="1.3" stroke-linejoin="round" stroke-linecap="round"><path d="M5 2.4h6L14.6 6v11a.8.8 0 0 1-.8.8H5a.8.8 0 0 1-.8-.8V3.2A.8.8 0 0 1 5 2.4Z"/><path d="M10.8 2.6V6.2h3.6"/><path d="M6.7 9.6h6M6.7 12h6M6.7 14.4h3.4"/></g></svg>'
		);
		add_menu_page(
			__( 'e-Faktura KSeF', 'prostafaktura-ksef' ),
			__( 'e-Faktura KSeF', 'prostafaktura-ksef' ),
			'ksef_view_monitoring',
			'ksef-dashboard',
			[ new Admin\Monitoring_Dashboard(), 'render_page' ],
			$menu_icon,
			57
		);

		// 1) Wystaw fakturę
		add_submenu_page(
			'ksef-dashboard',
			__( 'Wystaw fakturę', 'prostafaktura-ksef' ),
			__( '+ Wystaw fakturę', 'prostafaktura-ksef' ),
			'ksef_create_invoice',
			'ksef-manual-invoice',
			[ new Admin\Manual_Invoice_Page(), 'render_page' ]
		);

		// 2) Faktury sprzedaży
		add_submenu_page(
			'ksef-dashboard',
			__( 'Faktury sprzedaży', 'prostafaktura-ksef' ),
			__( 'Faktury sprzedaży', 'prostafaktura-ksef' ),
			'ksef_view_outgoing',
			'ksef-outgoing',
			[ new Admin\Outgoing_Invoices_List(), 'render_page' ]
		);

		// (Faktury zakupu — funkcja wersji Pro; w wersji darmowej brak tej pozycji menu.)

		// 4) Kontrahenci
		add_submenu_page(
			'ksef-dashboard',
			__( 'Kontrahenci', 'prostafaktura-ksef' ),
			__( 'Kontrahenci', 'prostafaktura-ksef' ),
			'ksef_view_contractors',
			'ksef-contractors',
			[ new Admin\Contractors_Page(), 'render_page' ]
		);

		// (Raporty — funkcja wersji Pro; w wersji darmowej brak tej pozycji menu.)

		// 6) Ustawienia
		add_submenu_page(
			'ksef-dashboard',
			__( 'Ustawienia', 'prostafaktura-ksef' ),
			__( 'Ustawienia', 'prostafaktura-ksef' ),
			'ksef_view_settings',
			'ksef-settings',
			[ new Admin\Settings_Page(), 'render_page' ]
		);

		// 7) Wersja Pro — STRONA-REKLAMA (zgodna z wytycznymi WordPress.org): opisuje
		// płatne funkcje dostępne w OSOBNEJ wtyczce Pro. Nie zawiera działającego kodu
		// tych funkcji ani bramki licencyjnej — to czysty upsell, a nie „zablokowana
		// funkcja" (Wytyczna 5). WP.org pozwala wskazać funkcje z osobnej wtyczki.
		add_submenu_page(
			'ksef-dashboard',
			__( 'Wersja Pro', 'prostafaktura-ksef' ),
			'✨ ' . __( 'Wersja Pro', 'prostafaktura-ksef' ),
			'ksef_view_settings',
			'ksef-pro',
			[ $this, 'render_pro_showcase' ]
		);

		// Przemianuj auto-generowane przez add_menu_page submenu (slug == parent,
		// label = "KSEF") na "Monitoring" i przesuń przed Ustawienia. Bez tego
		// menu pokazywało dwie pozycje: KSEF (auto) + Monitoring (osobny submenu).
		global $submenu;
		if ( isset( $submenu['ksef-dashboard'] ) ) {
			foreach ( $submenu['ksef-dashboard'] as $key => $item ) {
				if ( isset( $item[2] ) && 'ksef-dashboard' === $item[2] ) {
					$submenu['ksef-dashboard'][ $key ][0] = __( 'Monitoring', 'prostafaktura-ksef' );
					if ( isset( $submenu['ksef-dashboard'][ $key ][3] ) ) {
						$submenu['ksef-dashboard'][ $key ][3] = __( 'Monitoring', 'prostafaktura-ksef' );
					}
				}
			}
			$order = [
				'ksef-manual-invoice', 'ksef-outgoing', 'ksef-incoming',
				'ksef-contractors', 'ksef-reports', 'ksef-dashboard', 'ksef-settings', 'ksef-pro',
			];
			$reordered = [];
			$i = 0;
			foreach ( $order as $slug ) {
				foreach ( $submenu['ksef-dashboard'] as $item ) {
					if ( isset( $item[2] ) && $slug === $item[2] ) {
						$reordered[ $i++ ] = $item;
						break;
					}
				}
			}
			foreach ( $submenu['ksef-dashboard'] as $item ) {
				if ( isset( $item[2] ) && ! in_array( $item[2], $order, true ) ) {
					$reordered[ $i++ ] = $item;
				}
			}
			$submenu['ksef-dashboard'] = $reordered;
		}

		// Konfiguracja KSeF (lokalna pod-strona Ustawień, ukryta w bocznym pasku — link w tabach)
		add_submenu_page(
			null,
			__( 'Kredencjały KSeF', 'prostafaktura-ksef' ),
			__( 'Kredencjały KSeF', 'prostafaktura-ksef' ),
			'ksef_view_settings',
			'ksef-credentials',
			[ new Admin\Credentials_Page(), 'render_page' ]
		);

		// Kreator onboardingu (ukryty w pasku — wchodzi się przez banner „Otwórz kreator”).
		add_submenu_page(
			null,
			__( 'Kreator KSeF', 'prostafaktura-ksef' ),
			__( 'Kreator KSeF', 'prostafaktura-ksef' ),
			'ksef_view_settings',
			'ksef-onboarding',
			[ new Admin\Onboarding_Page(), 'render_page' ]
		);
	}

	/**
	 * Strona „Wersja Pro" — wyłącznie reklama (zgodna z WordPress.org).
	 *
	 * Opisuje płatne funkcje dostępne w OSOBNEJ wtyczce Pro i linkuje do zakupu.
	 * NIE zawiera kodu tych funkcji ani żadnej bramki licencyjnej — nic nie jest tu
	 * „zablokowane", bo tych funkcji w wersji darmowej po prostu nie ma. Style są
	 * wyłącznie w atrybutach `style=""` elementów (brak znaczników <style>/<script>).
	 *
	 * @return void
	 */
	public function render_pro_showcase(): void {
		if ( ! current_user_can( 'ksef_view_settings' ) ) {
			wp_die( esc_html__( 'Brak uprawnień.', 'prostafaktura-ksef' ) );
		}
		$cennik   = 'https://prostafakturaksef.pl/cennik';
		$checkout = 'https://prostafakturaksef.pl/checkout';
		$features = [
			[ '📥', __( 'Faktury zakupu (kosztowe)', 'prostafaktura-ksef' ), __( 'Pobieraj faktury wystawione na Twój NIP prosto z KSeF — dostawca, kwota, VAT i termin płatności w jednym miejscu. Oznaczaj płatności i kategorie wydatków.', 'prostafaktura-ksef' ) ],
			[ '✏️', __( 'Korekty', 'prostafaktura-ksef' ), __( 'Wystawiaj faktury korygujące (KOR, KOR_ZAL, KOR_ROZ) zgodnie z FA(3), z automatycznym powiązaniem z fakturą pierwotną.', 'prostafaktura-ksef' ) ],
			[ '📊', __( 'Raporty i eksport', 'prostafaktura-ksef' ), __( 'Raporty sprzedaży, kosztów i zysku za dowolny okres oraz eksport zestawień do CSV i PDF dla księgowej.', 'prostafaktura-ksef' ) ],
			[ '⚡', __( 'Automatyzacja WooCommerce', 'prostafaktura-ksef' ), __( 'Automatyczne wystawianie i wysyłanie faktur do KSeF z zamówień WooCommerce — według statusu, dla B2B i B2C.', 'prostafaktura-ksef' ) ],
			[ '🏢', __( 'Dane z GUS', 'prostafaktura-ksef' ), __( 'Auto-uzupełnianie danych kontrahenta po numerze NIP z rejestru GUS/REGON, także w koszyku.', 'prostafaktura-ksef' ) ],
			[ '🔑', __( 'Wiele kont KSeF', 'prostafaktura-ksef' ), __( 'Obsługa wielu firm / NIP-ów z jednego panelu, z osobnymi tokenami i danymi sprzedawcy.', 'prostafaktura-ksef' ) ],
		];
		?>
		<div class="wrap" style="max-width:980px;">
			<div style="background:linear-gradient(135deg,#d9531e 0%,#b8451a 100%);color:#fff;border-radius:16px;padding:30px 34px;margin:18px 0 24px;">
				<div style="font-size:13px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.85;">✨ <?php esc_html_e( 'ProstaFaktura dla KSeF — wersja Pro', 'prostafaktura-ksef' ); ?></div>
				<h1 style="color:#fff;font-size:30px;margin:8px 0 6px;"><?php esc_html_e( 'Odblokuj pełną automatyzację KSeF', 'prostafaktura-ksef' ); ?></h1>
				<p style="font-size:16px;opacity:.95;margin:0 0 18px;max-width:660px;"><?php esc_html_e( 'Wersja darmowa wystawia i wysyła faktury sprzedaży do KSeF bez limitu. Wersja Pro (osobna wtyczka) dokłada faktury kosztowe, korekty, raporty i automatyzację WooCommerce.', 'prostafaktura-ksef' ); ?></p>
				<a href="<?php echo esc_url( $checkout ); ?>" target="_blank" rel="noopener" style="display:inline-block;background:#fff;color:#b8451a;font-weight:700;padding:12px 26px;border-radius:10px;text-decoration:none;"><?php esc_html_e( 'Kup Pro — 120 zł / rok', 'prostafaktura-ksef' ); ?></a>
				<a href="<?php echo esc_url( $cennik ); ?>" target="_blank" rel="noopener" style="display:inline-block;margin-left:12px;color:#fff;font-weight:600;padding:12px 8px;text-decoration:underline;"><?php esc_html_e( 'Zobacz cennik i plany →', 'prostafaktura-ksef' ); ?></a>
				<div style="margin-top:12px;font-size:13px;opacity:.85;"><?php esc_html_e( '10 zł / mies · 120 zł / rok · 30 dni gwarancji zwrotu · faktura VAT', 'prostafaktura-ksef' ); ?></div>
			</div>
			<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;">
				<?php foreach ( $features as $f ) : ?>
					<div style="background:#fff;border:1px solid #e2e4e7;border-radius:12px;padding:18px 20px;">
						<div style="font-size:26px;line-height:1;"><?php echo esc_html( $f[0] ); ?></div>
						<div style="font-size:16px;font-weight:700;margin:8px 0 4px;color:#1d2327;"><?php echo esc_html( $f[1] ); ?></div>
						<div style="font-size:13px;color:#50575e;line-height:1.6;"><?php echo esc_html( $f[2] ); ?></div>
					</div>
				<?php endforeach; ?>
			</div>
			<div style="text-align:center;margin:28px 0 10px;">
				<a href="<?php echo esc_url( $checkout ); ?>" target="_blank" rel="noopener" style="display:inline-block;background:#d9531e;color:#fff;font-weight:700;padding:13px 32px;border-radius:10px;text-decoration:none;font-size:15px;"><?php esc_html_e( 'Przejdź na Pro', 'prostafaktura-ksef' ); ?></a>
				<p style="font-size:12px;color:#787c82;margin-top:10px;max-width:620px;margin-left:auto;margin-right:auto;"><?php esc_html_e( 'Pro to osobna wtyczka. Po jej instalacji Twoje dane (faktury, kontrahenci) pozostają — wersja darmowa i Pro współdzielą tę samą bazę.', 'prostafaktura-ksef' ); ?></p>
			</div>
		</div>
		<?php
	}

	/**
	 * Renderuj stronę ustawień
	 *
	 * @return void
	 */
	public function render_settings_page() {
		echo '<div class="wrap"><h1>' . esc_html__( 'KSEF - Ustawienia', 'prostafaktura-ksef' ) . '</h1>';
		echo '<p>' . esc_html__( 'Sekcja ustawień (wkrótce)', 'prostafaktura-ksef' ) . '</p>';
		echo '</div>';
	}

	/**
	 * Zaladuj zasoby administracyjne
	 *
	 * @return void
	 */
	public function enqueue_admin_assets() {
		wp_enqueue_style(
			'ksef-admin-css',
			KSEF_WORDPRESS_URL . 'assets/css/admin.css',
			[],
			KSEF_WORDPRESS_VERSION
		);

		// LEGACY ksef-admin.css (orange brand, body[class*="page_ksef-"] selectors)
		// został wyłączony — wszystkie strony KSeF są teraz w Modern_Shell i
		// dostają pełen zestaw stylów z assets/admin-modern.css. Ładowanie tego
		// pliku tylko konflikowało (orange override indigo brand).

		wp_enqueue_script(
			'ksef-admin-js',
			KSEF_WORDPRESS_URL . 'assets/js/admin.js',
			[ 'jquery' ],
			KSEF_WORDPRESS_VERSION,
			true
		);

		wp_enqueue_script(
			'ksef-admin-modals',
			KSEF_WORDPRESS_URL . 'assets/js/admin-modals.js',
			[ 'jquery' ],
			KSEF_WORDPRESS_VERSION,
			true
		);

		wp_localize_script(
			'ksef-admin-js',
			'KsefWpData',
			[
				'ajaxUrl' => admin_url( 'admin-ajax.php' ),
				'nonce'  => wp_create_nonce( 'ksef-nonce' ),
			]
		);
	}

	/**
	 * Cron: Polling statusu KSeF
	 *
	 * Sprawdza status faktur wysłanych (status=sent) co 2 minuty
	 *
	 * @return void
	 */
	public function poll_ksef_status_cron() {
		// Deleguj do działającego pollera v2 (klasa Invoices\Status_Poller nie istnieje —
		// to był martwy v1; v2 robi confirmed/rejected/UPO i jest bezpieczny).
		if ( class_exists( '\\KSEF\\WordPress\\V2\\Bootstrap' ) ) {
			\KSEF\WordPress\V2\Bootstrap::run_poll_cron();
		}
	}

	/**
	 * Funkcja Pro: jeśli po 4h od wysyłki faktura nie ma potwierdzenia przyjęcia przez KSeF
	 * (brak numeru KSeF / status nie „confirmed"), powiadom e-mailem admina i autora faktury,
	 * by sprawdził problem. Każda faktura powiadamiana raz (kolumna unconfirmed_notified).
	 */
	public function check_unconfirmed_invoices() {
		global $wpdb;
		$table = $wpdb->prefix . 'gksef_outgoing_invoices';
		$cols  = $wpdb->get_col( "SHOW COLUMNS FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery -- nazwa z $wpdb->prefix.
		if ( ! in_array( 'unconfirmed_notified', (array) $cols, true ) ) {
			return; // migracja kolumny jeszcze nie wykonana
		}
		$cutoff = gmdate( 'Y-m-d H:i:s', time() - 4 * HOUR_IN_SECONDS );
		$rows   = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			"SELECT id, invoice_number, buyer_name, buyer_nip, created_by, submission_timestamp, status
			 FROM {$table}
			 WHERE status IN ('sent','pending','sent_from_draft')
			   AND ( ksef_reference_number IS NULL OR ksef_reference_number = '' )
			   AND unconfirmed_notified = 0
			   AND submission_timestamp IS NOT NULL
			   AND submission_timestamp < %s
			 ORDER BY id ASC LIMIT 50",
			$cutoff
		), ARRAY_A );
		if ( empty( $rows ) ) {
			return;
		}
		$admin_email = (string) get_option( 'admin_email' );
		$panel_url   = admin_url( 'admin.php?page=ksef-outgoing' );
		foreach ( (array) $rows as $r ) {
			$recipients = [];
			if ( '' !== $admin_email ) {
				$recipients[] = $admin_email;
			}
			$uid = (int) ( $r['created_by'] ?? 0 );
			if ( $uid > 0 ) {
				$u = get_userdata( $uid );
				if ( $u && ! empty( $u->user_email ) ) {
					$recipients[] = (string) $u->user_email;
				}
			}
			$recipients = array_values( array_unique( array_filter( $recipients ) ) );
			if ( empty( $recipients ) ) {
				// brak adresatów — i tak oznacz, by nie powtarzać sprawdzania w nieskończoność
				$wpdb->update( $table, [ 'unconfirmed_notified' => 1 ], [ 'id' => (int) $r['id'] ], [ '%d' ], [ '%d' ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
				continue;
			}
			$num     = (string) ( $r['invoice_number'] ?: ( '#' . $r['id'] ) );
			$subject = sprintf( 'KSeF: brak potwierdzenia faktury %s po 4 godzinach', $num );
			$body    = "Faktura nr {$num} została wysłana do KSeF " . (string) $r['submission_timestamp'] . " (UTC),\n"
				. "ale po ponad 4 godzinach system nie otrzymał potwierdzenia jej przyjęcia (numer KSeF / UPO).\n\n"
				. 'Nabywca: ' . (string) ( $r['buyer_name'] ?? '' ) . ( $r['buyer_nip'] ? ' (NIP ' . (string) $r['buyer_nip'] . ')' : '' ) . "\n"
				. 'Status w systemie: ' . (string) ( $r['status'] ?? '' ) . "\n\n"
				. "Sprawdź status faktury w panelu wtyczki oraz w KSeF i ewentualny problem (odrzucenie, awaria MF, token).\n"
				. $panel_url . "\n";
			wp_mail( $recipients, $subject, $body );
			$wpdb->update( $table, [ 'unconfirmed_notified' => 1 ], [ 'id' => (int) $r['id'] ], [ '%d' ], [ '%d' ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
		}
	}

	/**
	 * Notice o wymaganym WooCommerce
	 *
	 * @return void
	 */
	public function woocommerce_required_notice() {
		?>
		<div class="notice notice-error is-dismissible">
			<p>
				<strong><?php echo esc_html__( 'KSEF - WordPress', 'prostafaktura-ksef' ); ?>:</strong>
				<?php echo esc_html__( 'Ta wtyczka wymaga aktywnego WooCommerce.', 'prostafaktura-ksef' ); ?>
			</p>
		</div>
		<?php
	}


	/**
	 * AJAX: Pobierz szczegóły faktury
	 *
	 * @return void
	 */
	public function ajax_get_invoice_detail() {
		global $wpdb;

		check_ajax_referer( 'ksef-nonce' );

		$invoice_id = intval( $_POST['invoice_id'] ?? 0 );
		$type       = sanitize_text_field( wp_unslash( $_POST['type'] ?? '' ) );

		// Per-typ cap: pracownik z dostępem tylko do incoming nie zobaczy outgoing i odwrotnie.
		$cap = ( 'incoming' === $type ) ? 'ksef_view_incoming' : 'ksef_view_outgoing';
		if ( ! current_user_can( $cap ) ) {
			wp_send_json_error( __( 'Brak uprawnień.', 'prostafaktura-ksef' ) );
		}

		if ( ! $invoice_id ) {
			wp_send_json_error( __( 'Invalid invoice ID.', 'prostafaktura-ksef' ) );
		}

		if ( 'outgoing' === $type ) {
			$invoice = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT * FROM {$wpdb->prefix}gksef_outgoing_invoices WHERE id = %d",
					$invoice_id
				),
				ARRAY_A
			);

			if ( ! $invoice ) {
				wp_send_json_error( __( 'Invoice not found.', 'prostafaktura-ksef' ) );
			}

			$html = $this->render_outgoing_invoice_detail( $invoice );
		} elseif ( 'incoming' === $type ) {
			$invoice = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT * FROM {$wpdb->prefix}gksef_incoming_invoices WHERE id = %d",
					$invoice_id
				),
				ARRAY_A
			);

			if ( ! $invoice ) {
				wp_send_json_error( __( 'Invoice not found.', 'prostafaktura-ksef' ) );
			}

			$html = $this->render_incoming_invoice_detail( $invoice );
		} else {
			wp_send_json_error( __( 'Invalid invoice type.', 'prostafaktura-ksef' ) );
		}

		wp_send_json_success( [ 'html' => $html ] );
	}

	/**
	 * Renderuj szczegóły faktury wysyłanej
	 *
	 * @param array $invoice Dane faktury.
	 * @return string HTML
	 */
	private function render_outgoing_invoice_detail( $invoice ) {
		// Płynne łączenie payloadu (faktury wystawione przez wtyczkę) z parsed XML
		// (faktury pulled z KSeF, source='ksef_pulled' albo gdy mamy generated_xml).
		$payload = ! empty( $invoice['payload_json'] ) ? json_decode( (string) Crypto::decrypt_from_storage( $invoice['payload_json'] ), true ) : null;
		$payload = is_array( $payload ) ? $payload : [];

		// Spróbuj parsować pełen XML jeśli jest — dla faktur pulled-z-KSeF mamy
		// generated_xml lub raw_xml, którego payload_json może nie zawierać.
		$xml_for_parse = (string) Crypto::decrypt_from_storage( $invoice['generated_xml'] ?? ( $invoice['raw_xml'] ?? '' ) );
		if ( '' !== $xml_for_parse && strpos( ltrim( $xml_for_parse ), '<' ) === 0 ) {
			require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-fa3-parser.php';
			$parsed_xml = \KSEF\WordPress\V2\Fa3_Parser::parse( $xml_for_parse );
			if ( is_array( $parsed_xml ) ) {
				// Parsed XML ma priorytet (pełne dane), payload uzupełnia brakujące pola.
				$payload = array_merge( $payload, $parsed_xml );
			}
		}

		$html  = '<div class="ksef-detail">';
		$html .= self::detail_styles();
		$html .= '<h3 style="font-size:20px;margin:0 0 4px;">' . esc_html( $invoice['invoice_number'] ?: ( '#' . $invoice['id'] ) ) . '</h3>';
		$html .= '<p style="color:#888;margin:0 0 16px;font-size:12px;">' . esc_html__( 'Pełne dane faktury — wszystkie pola', 'prostafaktura-ksef' ) . '</p>';

		// Pełny widok faktury z payloadu
		$html .= self::render_full_invoice_view( $payload, $invoice );

		// Sekcja: Płatność i status
		$summary  = Payments_Repository::summary( (int) $invoice['id'] );
		$payments = Payments_Repository::list_for_invoice( (int) $invoice['id'] );
		$currency = $summary['currency'] ?? ( $invoice['currency'] ?? 'PLN' );
		$status_labels = [
			'unpaid'  => __( 'Nieopłacona', 'prostafaktura-ksef' ),
			'partial' => __( 'Częściowo opłacona', 'prostafaktura-ksef' ),
			'paid'    => __( 'Opłacona', 'prostafaktura-ksef' ),
		];
		$status_colors = [
			'unpaid'  => '#990000;background:#ffcccc',
			'partial' => '#996600;background:#fff3cc',
			'paid'    => '#006400;background:#ccf0dd',
		];
		$pstat = $summary['payment_status'] ?? 'unpaid';

		$html .= '<div class="detail-section">';
		$html .= '<h4>' . esc_html__( 'Płatność', 'prostafaktura-ksef' ) . '</h4>';
		$html .= '<table>';
		$html .= '<tr><td><strong>' . esc_html__( 'Status', 'prostafaktura-ksef' ) . ':</strong></td><td><span style="padding:3px 8px;border-radius:3px;font-weight:bold;color:' . $status_colors[ $pstat ] . ';">' . esc_html( $status_labels[ $pstat ] ?? $pstat ) . '</span></td></tr>';
		if ( ! empty( $invoice['due_date'] ) && '0000-00-00' !== $invoice['due_date'] ) {
			$overdue = strtotime( $invoice['due_date'] ) < strtotime( 'today' ) && $pstat !== 'paid';
			$html   .= '<tr><td><strong>' . esc_html__( 'Termin płatności', 'prostafaktura-ksef' ) . ':</strong></td><td>' . esc_html( $invoice['due_date'] ) . ( $overdue ? ' <span style="color:#990000;font-weight:bold;">— PRZETERMINOWANE</span>' : '' ) . '</td></tr>';
		}
		$html .= '<tr><td><strong>' . esc_html__( 'Zapłacono', 'prostafaktura-ksef' ) . ':</strong></td><td>' . esc_html( number_format_i18n( (float) ( $summary['paid_amount'] ?? 0 ), 2 ) ) . ' ' . esc_html( $currency ) . ' z ' . esc_html( number_format_i18n( (float) ( $summary['gross_amount'] ?? 0 ), 2 ) ) . ' ' . esc_html( $currency ) . '</td></tr>';
		$html .= '<tr><td><strong>' . esc_html__( 'Do zapłaty', 'prostafaktura-ksef' ) . ':</strong></td><td><strong>' . esc_html( number_format_i18n( (float) ( $summary['outstanding'] ?? 0 ), 2 ) ) . ' ' . esc_html( $currency ) . '</strong></td></tr>';
		$html .= '</table>';

		// Lista płatności
		$html .= '<h5 style="margin-top:14px;">' . esc_html__( 'Historia płatności', 'prostafaktura-ksef' ) . '</h5>';
		if ( empty( $payments ) ) {
			$html .= '<p style="color:#888;">' . esc_html__( 'Brak zarejestrowanych płatności.', 'prostafaktura-ksef' ) . '</p>';
		} else {
			$html .= '<table style="width:100%;border-collapse:collapse;" class="ksef-payments-table">';
			$html .= '<thead><tr style="background:#f5f5f5;"><th style="padding:6px;text-align:left;">Data</th><th style="padding:6px;text-align:right;">Kwota</th><th style="padding:6px;text-align:left;">Metoda</th><th style="padding:6px;text-align:left;">Ref.</th><th style="padding:6px;"></th></tr></thead><tbody>';
			foreach ( $payments as $p ) {
				$html .= '<tr><td style="padding:6px;border-top:1px solid #eee;">' . esc_html( $p['payment_date'] ) . '</td>';
				$html .= '<td style="padding:6px;border-top:1px solid #eee;text-align:right;">' . esc_html( number_format_i18n( (float) $p['amount'], 2 ) ) . ' ' . esc_html( $p['currency'] ?: 'PLN' ) . '</td>';
				$html .= '<td style="padding:6px;border-top:1px solid #eee;">' . esc_html( $p['payment_method'] ?: '—' ) . '</td>';
				$html .= '<td style="padding:6px;border-top:1px solid #eee;">' . esc_html( $p['reference'] ?: '—' ) . '</td>';
				$html .= '<td style="padding:6px;border-top:1px solid #eee;text-align:right;"><button class="button button-link-delete ksef-del-payment" data-payment-id="' . (int) $p['id'] . '" data-invoice-id="' . (int) $invoice['id'] . '">×</button></td></tr>';
			}
			$html .= '</tbody></table>';
		}

		// Formularz dodawania płatności (gdy outstanding > 0)
		if ( ( $summary['outstanding'] ?? 0 ) > 0 ) {
			$html .= '<div style="margin-top:14px;padding:10px;background:#f9f9f9;border:1px solid #ddd;border-radius:4px;">';
			$html .= '<strong>' . esc_html__( 'Dodaj płatność', 'prostafaktura-ksef' ) . '</strong><br>';
			$html .= '<form class="ksef-add-payment-form" data-invoice-id="' . (int) $invoice['id'] . '" style="margin-top:8px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;align-items:end;">';
			$html .= '<label>Kwota<br><input type="number" step="0.01" min="0.01" name="amount" required value="' . esc_attr( (string) round( (float) ( $summary['outstanding'] ?? 0 ), 2 ) ) . '" style="width:100%;"></label>';
			$html .= '<label>Data<br><input type="date" name="payment_date" required value="' . esc_attr( gmdate( 'Y-m-d' ) ) . '" style="width:100%;"></label>';
			$html .= '<label>Metoda<br><select name="payment_method" style="width:100%;"><option>Przelew</option><option>Gotówka</option><option>Karta</option><option>BLIK</option><option>Inna</option></select></label>';
			$html .= '<label style="grid-column:span 2;">Numer referencyjny<br><input type="text" name="reference" placeholder="np. nr przelewu" style="width:100%;"></label>';
			$html .= '<button type="submit" class="button button-primary">+ Dodaj</button>';
			$html .= '</form></div>';
		}
		$html .= '</div>';

		// Sekcja: Akcje (PDF + email)
		$html .= '<div class="detail-section" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">';
		$pdf_url = add_query_arg( [
			'action'     => 'ksef_invoice_pdf',
			'invoice_id' => (int) $invoice['id'],
			'_ajax_nonce' => wp_create_nonce( 'ksef-nonce' ),
		], admin_url( 'admin-ajax.php' ) );
		$html .= '<a class="button button-primary" href="' . esc_url( $pdf_url ) . '" target="_blank">📄 Pobierz PDF</a>';
		$html .= '<form class="ksef-send-email-form" data-invoice-id="' . (int) $invoice['id'] . '" style="display:flex;gap:6px;align-items:center;">';
		$html .= '<input type="email" name="to" placeholder="email@klient.pl" style="min-width:240px;" required>';
		$html .= '<button type="submit" class="button">📧 Wyślij PDF mailem</button>';
		$html .= '</form>';
		$html .= '</div>';

		$xml = $xml_for_parse;
		if ( '' !== $xml ) {
			$html .= '<div class="ksdt-section"><div class="ksdt-h">Wszystkie pola źródłowe XML (FA(3))</div>';
			$html .= '<details open><summary style="cursor:pointer;color:#5B5BD6;font-weight:600;">Rozwiń pełen widok XML</summary>';
			$html .= '<div class="ksdt-xml-tree">' . self::xml_to_tree( $xml ) . '</div></details>';
			$html .= '<details><summary style="cursor:pointer;color:#888;font-size:11px;margin-top:10px;">Surowy XML (dla księgowej / audytu)</summary>';
			$html .= '<pre style="background:#f5f6f9;padding:12px;overflow-x:auto;max-height:400px;font-size:11px;border-radius:6px;">' . esc_html( substr( $xml, 0, 30000 ) ) . ( strlen( $xml ) > 30000 ? "\n…" : '' ) . '</pre></details>';
			$html .= '</div>';
		}

		$html .= '</div>';

		return $html;
	}

	/**
	 * Renderuj szczegóły faktury zakupowej
	 *
	 * @param array $invoice Dane faktury.
	 * @return string HTML
	 */
	private function render_incoming_invoice_detail( $invoice ) {
		// Parsuj raw_xml Fa3_Parserem żeby pokazać pełne dane (pozycje, strony, markery, ...)
		$payload = [];
		$raw_xml = (string) Crypto::decrypt_from_storage( $invoice['raw_xml'] ?? '' );
		if ( '' !== $raw_xml && strpos( ltrim( $raw_xml ), '<' ) === 0 ) {
			require_once KSEF_WORDPRESS_PATH . 'includes/v2/class-fa3-parser.php';
			$parsed = \KSEF\WordPress\V2\Fa3_Parser::parse( $raw_xml );
			if ( is_array( $parsed ) ) {
				$payload = $parsed;
			}
		}

		$html  = '<div class="ksef-detail">';
		$html .= self::detail_styles();
		$html .= '<h3 style="font-size:20px;margin:0 0 4px;">' . esc_html( $invoice['invoice_number'] ?: ( '#' . $invoice['id'] ) ) . '</h3>';
		$html .= '<p style="color:#888;margin:0 0 16px;font-size:12px;">' . esc_html__( 'Faktura zakupowa — pełne dane', 'prostafaktura-ksef' ) . '</p>';

		// Meta: status przetwarzania
		$html .= '<div class="ksdt-section"><div class="ksdt-h">' . esc_html__( 'Status w systemie', 'prostafaktura-ksef' ) . '</div><div class="ksdt-kv">';
		$html .= '<span>Status przetwarzania</span><span>' . esc_html( (string) ( $invoice['processing_status'] ?? '' ) ) . '</span>';
		$html .= '<span>Numer KSeF</span><span><code>' . esc_html( (string) ( $invoice['ksef_reference_number'] ?? '' ) ) . '</code></span>';
		if ( ! empty( $invoice['synced_at'] ) ) {
			$html .= '<span>Zsynchronizowano</span><span>' . esc_html( (string) $invoice['synced_at'] ) . '</span>';
		}
		$html .= '</div></div>';

		if ( ! empty( $payload ) ) {
			$html .= self::render_full_invoice_view( $payload, $invoice );
		} else {
			// Fallback gdy nie ma jeszcze pełnego XML (tylko metadane)
			$html .= '<div class="ksdt-section"><div class="ksdt-h">' . esc_html__( 'Dane podstawowe (metadane)', 'prostafaktura-ksef' ) . '</div><div class="ksdt-kv">';
			$html .= '<span>Dostawca</span><span>' . esc_html( (string) ( $invoice['seller_name'] ?? '' ) ) . '</span>';
			$html .= '<span>NIP dostawcy</span><span>' . esc_html( (string) ( $invoice['seller_nip'] ?? '' ) ) . '</span>';
			$html .= '<span>Data wystawienia</span><span>' . esc_html( (string) ( $invoice['issue_date'] ?? '' ) ) . '</span>';
			$html .= '<span>Netto</span><span>' . esc_html( number_format_i18n( (float) ( $invoice['net_amount'] ?? 0 ), 2 ) ) . ' PLN</span>';
			$html .= '<span>VAT</span><span>' . esc_html( number_format_i18n( (float) ( $invoice['vat_amount'] ?? 0 ), 2 ) ) . ' PLN</span>';
			$html .= '<span>Brutto</span><span><strong>' . esc_html( number_format_i18n( (float) ( $invoice['gross_amount'] ?? 0 ), 2 ) ) . ' PLN</strong></span>';
			$html .= '</div>';
			$html .= '<p style="color:#999;font-size:12px;margin-top:10px;">' . esc_html__( 'Pełna treść faktury (pozycje, oznaczenia) zostanie pobrana z KSeF przy następnej synchronizacji.', 'prostafaktura-ksef' ) . '</p>';
			$html .= '</div>';
		}

		// ── Płatności (incoming-only) ──
		$html .= self::render_incoming_payments_section( (int) $invoice['id'], (float) ( $invoice['gross_amount'] ?? 0 ), (string) ( $invoice['currency'] ?? 'PLN' ) );

		// ── Załączniki użytkownika ──
		$html .= self::render_attachments_section( 'incoming', (int) $invoice['id'] );

		if ( '' !== $raw_xml ) {
			// Pretty tree XML — gwarantuje 100% pokrycia pól. Każdy element FA(3)
			// pokazany w czytelnym drzewie, nawet jeśli nie ma dedykowanej sekcji.
			$html .= '<div class="ksdt-section"><div class="ksdt-h">Wszystkie pola źródłowe XML (FA(3))</div>';
			$html .= '<details open><summary style="cursor:pointer;color:#5B5BD6;font-weight:600;">Rozwiń pełen widok XML</summary>';
			$html .= '<div class="ksdt-xml-tree">' . self::xml_to_tree( $raw_xml ) . '</div>';
			$html .= '</details>';
			$html .= '<details><summary style="cursor:pointer;color:#888;font-size:11px;margin-top:10px;">Surowy XML (dla księgowej / audytu)</summary>';
			$html .= '<pre style="background:#f5f6f9;padding:12px;overflow-x:auto;max-height:400px;font-size:11px;border-radius:6px;">' . esc_html( substr( $raw_xml, 0, 30000 ) ) . ( strlen( $raw_xml ) > 30000 ? "\n…" : '' ) . '</pre></details>';
			$html .= '</div>';
		}

		$html .= '</div>';
		return $html;
	}

	/**
	 * Sekcja "Płatności" dla faktury zakupowej — lista + formularz dodawania.
	 */
	private static function render_incoming_payments_section( int $invoice_id, float $gross, string $currency ): string {
		global $wpdb;
		$payments = $wpdb->get_results(
			$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}gksef_incoming_payments WHERE incoming_invoice_id = %d ORDER BY payment_date DESC, id DESC", $invoice_id ),
			ARRAY_A
		);
		$paid      = 0.0;
		foreach ( (array) $payments as $p ) { $paid += (float) $p['amount']; }
		$remaining = max( 0, $gross - $paid );

		$h = static fn ( $v ) => esc_html( (string) $v );
		$f = static fn ( $v ) => number_format( (float) $v, 2, ',', ' ' );

		$html  = '<div class="ksdt-section"><div class="ksdt-h">' . esc_html__( 'Płatności', 'prostafaktura-ksef' ) . '</div>';
		$html .= '<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:14px;">';
		$html .= '<div><div style="font-size:11px;color:#888;text-transform:uppercase;">Do zapłaty</div><div style="font-size:18px;font-weight:600;">' . $f( $gross ) . ' ' . $h( $currency ) . '</div></div>';
		$html .= '<div><div style="font-size:11px;color:#888;text-transform:uppercase;">Zapłacono</div><div style="font-size:18px;font-weight:600;color:#15803D;">' . $f( $paid ) . ' ' . $h( $currency ) . '</div></div>';
		$html .= '<div><div style="font-size:11px;color:#888;text-transform:uppercase;">Pozostało</div><div style="font-size:18px;font-weight:600;color:' . ( $remaining > 0 ? '#B45309' : '#15803D' ) . ';">' . $f( $remaining ) . ' ' . $h( $currency ) . '</div></div>';
		$html .= '</div>';

		if ( ! empty( $payments ) ) {
			$html .= '<table class="ksdt-items" style="margin-bottom:12px;"><thead><tr>'
				. '<th>Data</th><th>Kwota</th><th>Metoda</th><th>Referencja</th><th>Notatka</th><th></th>'
				. '</tr></thead><tbody>';
			foreach ( $payments as $p ) {
				$html .= '<tr>'
					. '<td>' . $h( $p['payment_date'] ) . '</td>'
					. '<td class="r">' . $f( (float) $p['amount'] ) . ' ' . $h( $p['currency'] ) . '</td>'
					. '<td>' . $h( $p['payment_method'] ) . '</td>'
					. '<td>' . $h( $p['reference'] ) . '</td>'
					. '<td>' . $h( $p['note'] ) . '</td>'
					. '<td><button type="button" class="ksef-incoming-del-payment" data-payment-id="' . (int) $p['id'] . '" data-invoice-id="' . (int) $invoice_id . '" style="background:none;border:none;color:#DC2626;cursor:pointer;">🗑</button></td>'
					. '</tr>';
			}
			$html .= '</tbody></table>';
		}

		$today = gmdate( 'Y-m-d' );
		$html .= '<form class="ksef-incoming-add-payment-form" data-invoice-id="' . (int) $invoice_id . '" style="display:grid;grid-template-columns:130px 130px 1fr 1fr 110px;gap:8px;">'
			. '<input type="date" name="payment_date" value="' . $h( $today ) . '" required>'
			. '<input type="number" step="0.01" min="0" name="amount" placeholder="Kwota" value="' . $h( $remaining > 0 ? $remaining : '' ) . '" required>'
			. '<input type="text" name="payment_method" placeholder="Metoda (np. Przelew)">'
			. '<input type="text" name="reference" placeholder="Numer referencji / nr przelewu">'
			. '<button type="submit" class="button button-primary">+ Dodaj płatność</button>'
			. '</form>';
		$html .= '</div>';
		return $html;
	}

	/**
	 * Sekcja "Załączniki" — pliki użytkownika (PDF, skany, korespondencja) per faktura.
	 */
	private static function render_attachments_section( string $type, int $invoice_id ): string {
		global $wpdb;
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}gksef_invoice_attachments WHERE invoice_type = %s AND invoice_id = %d ORDER BY created_at DESC",
				$type, $invoice_id
			),
			ARRAY_A
		);

		$html  = '<div class="ksdt-section"><div class="ksdt-h">' . esc_html__( 'Załączniki', 'prostafaktura-ksef' ) . '</div>';
		$html .= '<p style="font-size:12px;color:#6b7280;margin:0 0 10px;">Twoje pliki (skany, korespondencja, potwierdzenia). Niezależne od XML z KSeF.</p>';

		if ( ! empty( $rows ) ) {
			$html .= '<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:10px;">';
			foreach ( $rows as $r ) {
				$url  = self::attachment_download_url( (int) $r['id'] );
				$name = (string) ( $r['original_name'] ?? 'załącznik' );
				$size = (int) ( $r['size_bytes'] ?? 0 );
				$size_label = $size > 0 ? ' <small style="color:#888;">(' . esc_html( size_format( $size ) ) . ')</small>' : '';
				$html .= '<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;background:#f9fafb;border-radius:6px;">'
					. '<div><a href="' . esc_url( (string) $url ) . '" target="_blank">📎 ' . esc_html( (string) $name ) . '</a>' . $size_label
					. ( ! empty( $r['label'] ) ? ' <span style="color:#6b7280;font-size:11.5px;">— ' . esc_html( (string) $r['label'] ) . '</span>' : '' ) . '</div>'
					. '<button type="button" class="ksef-attach-del" data-id="' . (int) $r['id'] . '" data-invoice-id="' . (int) $invoice_id . '" data-invoice-type="' . esc_attr( $type ) . '" style="background:none;border:none;color:#DC2626;cursor:pointer;">🗑</button>'
					. '</div>';
			}
			$html .= '</div>';
		} else {
			$html .= '<p style="font-size:12px;color:#9ca3af;margin:0 0 10px;">Brak załączników.</p>';
		}

		$html .= '<form class="ksef-attach-upload-form" enctype="multipart/form-data" data-invoice-id="' . (int) $invoice_id . '" data-invoice-type="' . esc_attr( $type ) . '" style="display:grid;grid-template-columns:1fr 1fr auto;gap:8px;">'
			. '<input type="file" name="file" required>'
			. '<input type="text" name="label" placeholder="Opis (opcj.)">'
			. '<button type="submit" class="button button-primary">+ Wgraj</button>'
			. '</form>';
		$html .= '</div>';
		return $html;
	}

	/**
	 * Wspólne style dla widoku szczegółów faktury.
	 */
	/**
	 * Konwertuje XML faktury FA(3) na czytelne drzewo HTML.
	 * Pokazuje każdy element + zawartość. Gwarantuje że ŻADNE pole z XML nie zostanie pominięte.
	 */
	private static function xml_to_tree( string $xml ): string {
		$xml = trim( $xml );
		if ( '' === $xml ) {
			return '<em>brak XML</em>';
		}
		libxml_use_internal_errors( true );
		$dom = new \DOMDocument();
		if ( ! $dom->loadXML( $xml ) ) {
			return '<em>XML nie został wczytany</em>';
		}
		return '<ul class="ksdt-xml-ul">' . self::xml_node_to_html( $dom->documentElement ) . '</ul>';
	}

	private static function xml_node_to_html( \DOMNode $node ): string {
		if ( $node->nodeType !== XML_ELEMENT_NODE ) {
			return '';
		}
		$name  = $node->localName ?? $node->nodeName;
		$human = self::humanize_field_name( $name );

		// Element bez dzieci elementów — pokazujemy wartość inline
		$has_child_el = false;
		foreach ( $node->childNodes as $c ) {
			if ( $c->nodeType === XML_ELEMENT_NODE ) { $has_child_el = true; break; }
		}
		if ( ! $has_child_el ) {
			$val = trim( $node->textContent );
			if ( '' === $val ) {
				return ''; // pomijamy puste leafs
			}
			return sprintf(
				'<li><span class="ksdt-xml-k">%s</span><span class="ksdt-xml-v">%s</span><small class="ksdt-xml-tag">&lt;%s&gt;</small></li>',
				esc_html( $human ),
				esc_html( $val ),
				esc_html( $name )
			);
		}
		// Element z dziećmi — rekurencja
		$children_html = '';
		foreach ( $node->childNodes as $c ) {
			$children_html .= self::xml_node_to_html( $c );
		}
		if ( '' === $children_html ) {
			return '';
		}
		return sprintf(
			'<li class="ksdt-xml-sec"><span class="ksdt-xml-k ksdt-xml-section">%s</span><small class="ksdt-xml-tag">&lt;%s&gt;</small><ul class="ksdt-xml-ul">%s</ul></li>',
			esc_html( $human ),
			esc_html( $name ),
			$children_html
		);
	}

	/**
	 * Mapowanie nazw FA(3) na czytelne polskie etykiety.
	 */
	private static function humanize_field_name( string $name ): string {
		static $map = [
			'Faktura' => 'Faktura', 'Naglowek' => 'Nagłówek',
			'KodFormularza' => 'Kod formularza', 'WariantFormularza' => 'Wariant',
			'DataWytworzeniaFa' => 'Data wytworzenia XML', 'SystemInfo' => 'System wystawcy',
			'KodUrzedu' => 'Kod urzędu',
			'Podmiot1' => 'Sprzedawca', 'Podmiot1k' => 'Sprzedawca dodatkowy',
			'Podmiot2' => 'Nabywca', 'Podmiot2k' => 'Nabywca dodatkowy',
			'Podmiot3' => 'Odbiorca', 'PodmiotUpowazniony' => 'Podmiot upoważniony',
			'DaneIdentyfikacyjne' => 'Dane identyfikacyjne', 'DaneKontaktowe' => 'Dane kontaktowe',
			'PelnaNazwa' => 'Pełna nazwa', 'Nazwa' => 'Nazwa',
			'OsobaFizyczna1' => 'Osoba fizyczna', 'ImiePierwsze' => 'Imię', 'Nazwisko' => 'Nazwisko', 'IDNS' => 'ID NS',
			'NIP' => 'NIP', 'KodKraju' => 'Kod kraju', 'NrVatUE' => 'VAT-UE', 'KodUE' => 'Kod UE',
			'NrID' => 'Nr identyfikatora', 'BrakID' => 'Brak identyfikatora',
			'REGON' => 'REGON', 'KRS' => 'KRS', 'PESEL' => 'PESEL', 'NrEORI' => 'EORI',
			'Email' => 'Email', 'Telefon' => 'Telefon',
			'Adres' => 'Adres', 'AdresKoresp' => 'Adres korespondencyjny',
			'KodPocztowy' => 'Kod pocztowy', 'Miejscowosc' => 'Miejscowość',
			'Ulica' => 'Ulica', 'NrDomu' => 'Nr domu', 'NrLokalu' => 'Nr lokalu',
			'Wojewodztwo' => 'Województwo', 'Powiat' => 'Powiat', 'Gmina' => 'Gmina', 'Dzielnica' => 'Dzielnica',
			'AdresL1' => 'Adres (linia 1)', 'AdresL2' => 'Adres (linia 2)',
			'Fa' => 'Faktura — szczegóły', 'KodWaluty' => 'Waluta', 'KursWaluty' => 'Kurs waluty',
			'P_1' => 'Data wystawienia', 'P_1M' => 'Miejsce wystawienia', 'P_2' => 'Numer faktury',
			'P_3A' => 'Nr faktury korygowanej', 'P_3B' => 'Data faktury korygowanej',
			'P_3C' => 'Data sprzedaży na korygowanej', 'P_3D' => 'Suma korygowanych pozycji',
			'P_4A' => 'Kod waluty dla sprzedaży', 'P_4B' => 'Data sprzedaży',
			'P_5A' => 'Suma zaliczek', 'P_5B' => 'Data otrzymania zaliczki',
			'P_6' => 'Data sprzedaży', 'OkresFa' => 'Okres faktury', 'P_6_Od' => 'Od', 'P_6_Do' => 'Do',
			'DataWplywuFaktury' => 'Data wpływu faktury',
			'RodzajFaktury' => 'Rodzaj faktury', 'PrzyczynaKorekty' => 'Przyczyna korekty',
			'DaneFaKorygowanej' => 'Dane faktury korygowanej', 'NrFaKorygowanej' => 'Nr faktury korygowanej',
			'DataWystFaKorygowanej' => 'Data wystawienia korygowanej', 'NrKSeF' => 'Numer KSeF',
			'FaWiersz' => 'Pozycja faktury', 'NrWierszaFa' => 'Lp.',
			'P_7' => 'Nazwa towaru/usługi', 'P_7A' => 'PKWiU', 'P_7B' => 'CN', 'P_7C' => 'PKOB',
			'P_7D' => 'Wydobycie surowca',
			'P_8A' => 'Jednostka miary', 'P_8B' => 'Ilość',
			'P_9A' => 'Cena jednostkowa netto', 'P_9B' => 'Cena jednostkowa brutto',
			'P_10' => 'Kwota opustu/obniżki',
			'P_11' => 'Wartość sprzedaży netto', 'P_11A' => 'Wartość sprzedaży brutto',
			'P_12' => 'Stawka VAT', 'P_12_XII' => 'Rodzaj stawki', 'P_12_Procedura' => 'Procedura VAT',
			'GTU' => 'Kod GTU',
			'P_13_1' => 'Suma netto 23%', 'P_14_1' => 'Suma VAT 23%',
			'P_13_2' => 'Suma netto 8%',  'P_14_2' => 'Suma VAT 8%',
			'P_13_3' => 'Suma netto 5%',  'P_14_3' => 'Suma VAT 5%',
			'P_13_4' => 'Suma netto inne', 'P_14_4' => 'Suma VAT inne',
			'P_13_5' => 'Suma netto 0%',  'P_14_5' => 'Suma VAT 0%',
			'P_13_6_1' => 'Suma zwolnione', 'P_13_6_2' => 'Suma nie podlega',
			'P_13_7' => 'Suma odwrotne obciążenie',
			'P_15' => 'Razem brutto', 'Platnosc' => 'Płatność',
			'Zaplacono' => 'Zapłacono', 'ZaplaconoData' => 'Data zapłaty',
			'KwotaZaplacona' => 'Kwota zapłacona', 'NaleznoscPozostala' => 'Pozostało do zapłaty',
			'FormaPlatnosci' => 'Forma płatności', 'TerminPlatnosci' => 'Termin płatności',
			'Termin' => 'Termin', 'TerminOpis' => 'Opis terminu',
			'RachunekBankowy' => 'Rachunek bankowy', 'NrRB' => 'Numer rachunku',
			'NazwaBanku' => 'Nazwa banku', 'SWIFT' => 'SWIFT/BIC', 'OpisRachunku' => 'Opis rachunku',
			'OdRachunkuZagranicznego' => 'Konto zagraniczne',
			'SkontoUmowne' => 'Skonto', 'SkontoUmowneOpis' => 'Skonto — opis',
			'PrzeterminePlatnosci' => 'Odsetki za zwłokę',
			'Adnotacje' => 'Adnotacje', 'P_16' => 'Metoda kasowa', 'P_17' => 'Samofakturowanie',
			'P_18' => 'Odwrotne obciążenie', 'P_18A' => 'Mechanizm podzielonej płatności',
			'P_19' => 'Zwolnienie z VAT', 'P_19N' => 'Zwolnienie — nie dotyczy',
			'P_19A' => 'Artykuł zwolnienia', 'P_19B' => 'Ustawa', 'P_19C' => 'Dyrektywa UE',
			'Zwolnienie' => 'Zwolnienie z VAT (szczegół)',
			'P_21' => 'Eksport', 'P_22' => 'Nowe środki transportu',
			'P_22A' => 'NST — data oddania', 'P_22B' => 'NST — przebieg/godziny',
			'NoweSrodkiTransportu' => 'Nowe środki transportu (NST)',
			'P_23' => 'WTT uproszczona',
			'PMarzy' => 'Procedura marży', 'P_PMarzyN' => 'Marża — nie dotyczy',
			'P_PMarzy_2' => 'Marża wariant 2', 'P_PMarzy_3' => 'Marża wariant 3',
			'FP' => 'Faktura do paragonu', 'TP' => 'Powiązania między stronami',
			'WarunkiTransakcji' => 'Warunki transakcji', 'Transport' => 'Transport',
			'KosztOpakowan' => 'Koszt opakowań', 'WarunkiDostawy' => 'Warunki dostawy',
			'Dostawa' => 'Dostawa', 'DataDostawy' => 'Data dostawy', 'MiejsceDostawy' => 'Miejsce dostawy',
			'DokumentyPowiazane' => 'Dokumenty powiązane',
			'ZamowienieZakupu' => 'Zamówienie zakupu', 'NrZamowieniaZakupu' => 'Nr zamówienia',
			'DataZamowieniaZakupu' => 'Data zamówienia',
			'Zamowienie' => 'Zamówienie', 'NrZamowienia' => 'Nr zamówienia',
			'DataZamowienia' => 'Data zamówienia',
			'ZamowienieWiersz' => 'Wiersz zamówienia',
			'NrWierszaZam' => 'Lp.', 'P_7Z' => 'Nazwa', 'P_8AZ' => 'Jednostka',
			'P_8BZ' => 'Ilość', 'P_9AZ' => 'Cena netto', 'P_11Z' => 'Wartość', 'P_12Z' => 'Stawka VAT',
			'DokumentMagazynowy' => 'Dokument magazynowy', 'NumerDokumentu' => 'Numer dokumentu',
			'NumerKlienta' => 'Numer klienta', 'NrKlienta' => 'Nr klienta', 'IDNabywcy' => 'ID nabywcy',
			'Zaliczka' => 'Zaliczka', 'KwotaZaliczki' => 'Kwota zaliczki',
			'NrFaZaliczkowej' => 'Nr faktury zaliczkowej', 'DataFaZaliczkowej' => 'Data faktury zaliczkowej',
			'DostawcaNieobowiazkowe' => 'Dostawca — dane opcjonalne',
			'NabywcaNieobowiazkowe' => 'Nabywca — dane opcjonalne',
			'RolaPodmiotu3' => 'Rola odbioru',
			'DodatkowyOpis' => 'Dodatkowy opis', 'Klucz' => 'Klucz', 'Wartosc' => 'Wartość',
			'Stopka' => 'Stopka faktury', 'Wstep' => 'Wstęp', 'Zakonczenie' => 'Zakończenie',
			'Rejestracja' => 'Rejestracja', 'KapitalZakladowy' => 'Kapitał zakładowy',
			'KapitalWplacony' => 'Kapitał wpłacony', 'OrganRejestrowy' => 'Organ rejestrowy',
			'AdnotacjaPodlegaCit' => 'Podlega CIT',
			'UU_ID' => 'UUID wiersza', 'IDZewnetrzny' => 'ID zewnętrzny', 'P_11Nr' => 'Nr poz. korygowanej',
			'P_6A' => 'Data dostawy (wiersz)',
		];
		return $map[ $name ] ?? $name;
	}

	private static function detail_styles(): string {
		return '<style>
		.ksef-detail { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; color: #1a1d21; }
		.ksdt-section { border: 1px solid #e7eaee; border-radius: 8px; padding: 14px 16px; margin-bottom: 12px; background: #fff; }
		.ksdt-h { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: #d9531e; margin-bottom: 10px; border-bottom: 1px solid #f0f1f3; padding-bottom: 6px; }
		.ksdt-kv { display: grid; grid-template-columns: 200px 1fr; gap: 5px 14px; font-size: 13px; }
		.ksdt-kv > span:nth-child(odd) { color: #6b7280; }
		.ksdt-kv code { background: #f3f4f6; padding: 1px 6px; border-radius: 3px; font-size: 11.5px; }
		.ksdt-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
		.ksdt-items { width: 100%; border-collapse: collapse; font-size: 12px; }
		.ksdt-items th, .ksdt-items td { border: 1px solid #e7eaee; padding: 5px 8px; text-align: left; }
		.ksdt-items th { background: #fafbfc; color: #6b7280; font-size: 10.5px; text-transform: uppercase; }
		.ksdt-items td.r, .ksdt-items th.r { text-align: right; }
		.ksdt-chip { display: inline-block; background: #fff2eb; color: #d9531e; border: 1px solid #ffd7be; padding: 2px 9px; border-radius: 12px; font-size: 11.5px; margin: 2px 3px 2px 0; }
		.ksdt-totals { margin-left: auto; width: 480px; font-size: 12.5px; border-collapse: collapse; }
		.ksdt-totals thead th { padding: 6px 10px; background: #fafbfc; color: #6b7280; font-size: 11px; text-transform: uppercase; border-bottom: 1px solid #e7eaee; }
		.ksdt-totals td { padding: 5px 10px; border-bottom: 1px solid #f0f1f3; }
		.ksdt-totals tr.tot td { font-weight: 700; border-top: 1px solid #d3d7dd; font-size: 14px; background: #fafbfc; }

		.ksdt-party { display: flex; flex-direction: column; gap: 6px; }
		.ksdt-party-name { font-size: 15px; font-weight: 700; color: #1a1d21; margin: 4px 0; }
		.ksdt-party-addr { font-size: 13px; color: #1a1d21; line-height: 1.45; }
		.ksdt-party-addr div { line-height: 1.4; }
		.ksdt-party-meta { display: grid; grid-template-columns: 110px 1fr; gap: 3px 12px; margin: 8px 0 0; font-size: 12.5px; }
		.ksdt-party-meta dt { color: #6b7280; margin: 0; }
		.ksdt-party-meta dd { margin: 0; color: #1a1d21; font-weight: 500; }

		.ksdt-correction { background: #fff5ed; border-color: #ffcaa1; }
		.ksdt-correction .ksdt-h { color: #b8451a; border-bottom-color: #ffd7be; }

		/* Pretty XML tree — gwarantuje 100% pokrycia pól */
		.ksdt-xml-tree { font-size: 12.5px; margin-top: 10px; max-height: 600px; overflow-y: auto; padding: 12px; background: #fafbfc; border: 1px solid #e7eaee; border-radius: 8px; }
		.ksdt-xml-tree ul.ksdt-xml-ul { list-style: none; padding-left: 18px; margin: 0; border-left: 1px dashed #d3d7dd; }
		.ksdt-xml-tree > ul.ksdt-xml-ul { padding-left: 0; border-left: 0; }
		.ksdt-xml-tree li { padding: 3px 0; line-height: 1.4; }
		.ksdt-xml-tree li.ksdt-xml-sec { padding-top: 6px; }
		.ksdt-xml-k { color: #6b7280; min-width: 200px; display: inline-block; }
		.ksdt-xml-section { color: #1a1d21; font-weight: 600; font-size: 13px; }
		.ksdt-xml-v { color: #1a1d21; font-weight: 500; margin-right: 8px; }
		.ksdt-xml-tag { color: #aab; font-family: monospace; font-size: 10.5px; opacity: .75; }
		</style>';
	}

	/**
	 * Publiczny wrapper widoku tylko-do-odczytu — używany przez pełnostronicowy
	 * podgląd faktury (class-manual-invoice-page, action=view). Zwraca style +
	 * kompletny widok (tylko pola obecne na fakturze) + opcjonalne drzewo XML.
	 *
	 * @param array  $payload  payload z Fa3_Parser/payload_json
	 * @param array  $invoice  wiersz z DB
	 * @param string $raw_xml  surowy XML FA(3) (opcjonalnie — dla drzewa pól źródłowych)
	 */
	public static function render_invoice_full_readonly( array $payload, array $invoice, string $raw_xml = '' ): string {
		$html  = self::detail_styles();
		$html .= self::render_full_invoice_view( $payload, $invoice );
		if ( '' !== $raw_xml && strpos( ltrim( $raw_xml ), '<' ) === 0 ) {
			$html .= '<div class="ksdt-section"><div class="ksdt-h">Wszystkie pola źródłowe XML (FA(3))</div>';
			$html .= '<details><summary style="cursor:pointer;color:#5B5BD6;font-weight:600;">Pokaż pełny widok XML (audyt)</summary>';
			$html .= '<div class="ksdt-xml-tree">' . self::xml_to_tree( $raw_xml ) . '</div></details></div>';
		}
		return $html;
	}

	/**
	 * Renderuje KOMPLETNY widok faktury z payloadu (wszystkie pola).
	 *
	 * @param array $p   payload (z payload_json lub Fa3_Parser)
	 * @param array $inv wiersz z DB (dla pól nieobecnych w payloadzie)
	 */
	private static function render_full_invoice_view( array $p, array $inv ): string {
		$cur = (string) ( $p['currency'] ?? $inv['currency'] ?? 'PLN' );
		$fmt = static fn ( $v ) => number_format( (float) $v, 2, ',', ' ' );
		$h   = static fn ( $v ) => esc_html( (string) $v );
		$row = '';

		// Format pełnego adresu z parsed party (FA(3) szczegóły lub fallback do line1/line2)
		$addr_lines = static function ( array $addr ) use ( $h ) {
			$lines = [];
			$street = trim( (string) ( $addr['street'] ?? '' ) );
			if ( '' !== $street ) {
				$lines[] = $h( $street );
			} elseif ( ! empty( $addr['line1'] ) ) {
				$lines[] = $h( $addr['line1'] );
				if ( ! empty( $addr['line2'] ) ) {
					$lines[] = $h( $addr['line2'] );
				}
			}
			$kp_city = trim( ( (string) ( $addr['postal_code'] ?? '' ) ) . ' ' . ( (string) ( $addr['city'] ?? '' ) ) );
			if ( '' !== $kp_city ) {
				$lines[] = $h( $kp_city );
			}
			if ( ! empty( $addr['country'] ) && 'PL' !== $addr['country'] ) {
				$lines[] = $h( $addr['country'] );
			}
			return $lines ?: [ '—' ];
		};

		// ── Nagłówek ──
		$row .= '<div class="ksdt-section"><div class="ksdt-h">Nagłówek faktury</div><div class="ksdt-kv">';
		$row .= '<span>Numer faktury</span><span><strong>' . $h( $p['invoice_number'] ?? $inv['invoice_number'] ?? '' ) . '</strong></span>';
		$row .= '<span>Data wystawienia</span><span>' . $h( $p['issue_date'] ?? '' ) . '</span>';
		if ( ! empty( $p['period_from'] ) || ! empty( $p['period_to'] ) ) {
			$row .= '<span>Okres faktury</span><span>' . $h( $p['period_from'] ?? '' ) . ' — ' . $h( $p['period_to'] ?? '' ) . '</span>';
		} else {
			$row .= '<span>Data sprzedaży</span><span>' . $h( $p['sale_date'] ?? '' ) . '</span>';
		}
		$row .= '<span>Waluta</span><span>' . $h( $cur ) . '</span>';
		$row .= '<span>Rodzaj faktury</span><span>' . $h( $p['rodzaj_faktury'] ?? 'VAT' ) . '</span>';
		if ( ! empty( $p['extra_data']['issue_place'] ) ) {
			$row .= '<span>Miejsce wystawienia</span><span>' . $h( $p['extra_data']['issue_place'] ) . '</span>';
		}
		if ( ! empty( $inv['ksef_reference_number'] ) ) {
			$row .= '<span>Numer KSeF</span><span><code>' . $h( $inv['ksef_reference_number'] ) . '</code></span>';
		}
		if ( ! empty( $inv['ksef_session_id'] ) ) {
			$row .= '<span>Sesja KSeF</span><span><code>' . $h( $inv['ksef_session_id'] ) . '</code></span>';
		}
		$row .= '</div></div>';

		// ── Sprzedawca + Nabywca w klasycznym układzie 2 kolumny ──
		$seller = $p['seller'] ?? [];
		$buyer  = $p['buyer'] ?? [];

		$render_party = static function ( string $title, array $party, array $accent_color ) use ( $h, $addr_lines ) {
			$out  = '<div class="ksdt-section ksdt-party"><div class="ksdt-h">' . $h( $title ) . '</div>';
			$out .= '<div class="ksdt-party-name">' . $h( $party['name'] ?? '—' ) . '</div>';
			$out .= '<div class="ksdt-party-addr">';
			foreach ( $addr_lines( (array) ( $party['address'] ?? [] ) ) as $line ) {
				$out .= '<div>' . $line . '</div>';
			}
			$out .= '</div>';
			$out .= '<dl class="ksdt-party-meta">';
			if ( ! empty( $party['nip'] ) )      $out .= '<dt>NIP</dt><dd>' . $h( $party['nip'] ) . '</dd>';
			if ( ! empty( $party['vat_ue'] ) )   $out .= '<dt>VAT-UE</dt><dd>' . $h( ( $party['country_ue'] ?? '' ) . ' ' . $party['vat_ue'] ) . '</dd>';
			if ( ! empty( $party['regon'] ) )    $out .= '<dt>REGON</dt><dd>' . $h( $party['regon'] ) . '</dd>';
			if ( ! empty( $party['krs'] ) )      $out .= '<dt>KRS</dt><dd>' . $h( $party['krs'] ) . '</dd>';
			if ( ! empty( $party['pesel'] ) )    $out .= '<dt>PESEL</dt><dd>' . $h( $party['pesel'] ) . '</dd>';
			if ( ! empty( $party['nr_id'] ) )    $out .= '<dt>Identyfikator</dt><dd>' . $h( ( $party['country_code'] ?? '' ) . ' ' . $party['nr_id'] ) . '</dd>';
			if ( ! empty( $party['eori'] ) )     $out .= '<dt>EORI</dt><dd>' . $h( $party['eori'] ) . '</dd>';
			if ( ! empty( $party['brak_id'] ) )  $out .= '<dt>Typ</dt><dd>Osoba prywatna (BrakID)</dd>';
			if ( ! empty( $party['email'] ) )    $out .= '<dt>Email</dt><dd>' . $h( $party['email'] ) . '</dd>';
			if ( ! empty( $party['phone'] ) )    $out .= '<dt>Telefon</dt><dd>' . $h( $party['phone'] ) . '</dd>';
			$out .= '</dl></div>';
			return $out;
		};

		$row .= '<div class="ksdt-2col">';
		$row .= $render_party( 'Sprzedawca', $seller, [] );
		$row .= $render_party( 'Nabywca', $buyer, [] );
		$row .= '</div>';

		// ── Korekta (gdy faktura jest korygująca) ──
		$correction = $p['correction'] ?? null;
		if ( is_array( $correction ) ) {
			$row .= '<div class="ksdt-section ksdt-correction"><div class="ksdt-h">⚠ Faktura korygująca</div><div class="ksdt-kv">';
			if ( ! empty( $correction['original_number'] ) ) {
				$row .= '<span>Korekta faktury</span><span><strong>' . $h( $correction['original_number'] ) . '</strong></span>';
			}
			if ( ! empty( $correction['original_issue_date'] ) ) {
				$row .= '<span>z dnia</span><span>' . $h( $correction['original_issue_date'] ) . '</span>';
			}
			if ( ! empty( $correction['original_ksef_no'] ) ) {
				$row .= '<span>Nr KSeF oryginału</span><span><code>' . $h( $correction['original_ksef_no'] ) . '</code></span>';
			}
			if ( ! empty( $correction['reason'] ) ) {
				$row .= '<span>Przyczyna korekty</span><span>' . $h( $correction['reason'] ) . '</span>';
			}
			$row .= '</div></div>';
		}

		// ── Faktura zaliczkowa P_5A/P_5B ──
		$advance = $p['advance'] ?? null;
		if ( is_array( $advance ) ) {
			$row .= '<div class="ksdt-section"><div class="ksdt-h">Faktura zaliczkowa</div><div class="ksdt-kv">';
			if ( ! empty( $advance['p_5a'] ) ) $row .= '<span>Otrzymane zaliczki</span><span>' . $h( $advance['p_5a'] ) . '</span>';
			if ( ! empty( $advance['p_5b'] ) ) $row .= '<span>Data otrzymania zaliczki</span><span>' . $h( $advance['p_5b'] ) . '</span>';
			$row .= '</div></div>';
		}

		// ── Odbiorca/adres dostawy (Podmiot3) ──
		$del = $p['delivery'] ?? null;
		if ( is_array( $del ) ) {
			$row .= '<div class="ksdt-section ksdt-party"><div class="ksdt-h">Odbiorca / adres dostawy</div>';
			if ( ! empty( $del['role'] ) ) {
				$row .= '<div style="font-size:11px;color:#888;">Rola: ' . $h( $del['role'] ) . '</div>';
			}
			$row .= '<div class="ksdt-party-name">' . $h( $del['name'] ?? '—' ) . '</div>';
			$row .= '<div class="ksdt-party-addr">';
			foreach ( $addr_lines( (array) ( $del['address'] ?? [] ) ) as $line ) {
				$row .= '<div>' . $line . '</div>';
			}
			$row .= '</div>';
			if ( ! empty( $del['nip'] ) ) {
				$row .= '<dl class="ksdt-party-meta"><dt>NIP</dt><dd>' . $h( $del['nip'] ) . '</dd></dl>';
			}
			$row .= '</div>';
		}

		// ── Podmiot upoważniony (biuro rachunkowe) ──
		$auth_p = $p['authorized'] ?? null;
		if ( is_array( $auth_p ) ) {
			$row .= '<div class="ksdt-section ksdt-party"><div class="ksdt-h">Podmiot upoważniony (np. biuro rachunkowe)</div>';
			$row .= '<div class="ksdt-party-name">' . $h( $auth_p['name'] ?? '—' ) . '</div>';
			$row .= '<div class="ksdt-party-addr">';
			foreach ( $addr_lines( (array) ( $auth_p['address'] ?? [] ) ) as $line ) {
				$row .= '<div>' . $line . '</div>';
			}
			$row .= '</div>';
			if ( ! empty( $auth_p['nip'] ) ) {
				$row .= '<dl class="ksdt-party-meta"><dt>NIP</dt><dd>' . $h( $auth_p['nip'] ) . '</dd></dl>';
			}
			$row .= '</div>';
		}

		// Nr klienta z extra_data
		if ( ! empty( $p['extra_data']['customer_no'] ) ) {
			$row .= '<div class="ksdt-section"><div class="ksdt-kv"><span>Nr klienta</span><span>' . $h( $p['extra_data']['customer_no'] ) . '</span></div></div>';
		}

		// ── Pozycje faktury (pełen layout FA(3)) ──
		$items = $p['items'] ?? [];
		if ( ! empty( $items ) ) {
			$row .= '<div class="ksdt-section"><div class="ksdt-h">Pozycje faktury (' . count( $items ) . ')</div>';
			$row .= '<table class="ksdt-items"><thead><tr>';
			$row .= '<th>Lp.</th><th>Produkt / opis</th><th>PKWiU<br>CN/PKOB</th><th>Jedn.</th><th class="r">Ilość</th>'
				. '<th class="r">Cena netto</th><th class="r">Cena brutto</th><th class="r">Rabat</th>'
				. '<th>Stawka</th><th class="r">Wart. netto</th><th class="r">Wart. brutto</th><th>GTU</th>';
			$row .= '</tr></thead><tbody>';
			foreach ( $items as $i => $it ) {
				$rate = $it['vat_rate'] ?? 23;
				$rate_str = is_numeric( $rate ) ? ( (int) round( (float) $rate ) ) . '%' : strtoupper( (string) $rate );
				$vat_kind = (string) ( $it['vat_kind'] ?? '' );
				if ( '' !== $vat_kind ) {
					$rate_str .= ' <small style="color:#888">(' . $h( $vat_kind ) . ')</small>';
				}
				$code = trim( (string) ( $it['pkwiu'] ?? '' ) );
				if ( ! empty( $it['cn'] ) )   $code .= ( '' !== $code ? '<br>CN ' : 'CN ' ) . $h( (string) $it['cn'] );
				if ( ! empty( $it['pkob'] ) ) $code .= ( '' !== $code ? '<br>PKOB ' : 'PKOB ' ) . $h( (string) $it['pkob'] );
				$row .= '<tr>';
				$row .= '<td>' . $h( $it['lp'] ?? ( $i + 1 ) ) . '</td>';
				$row .= '<td>' . $h( $it['description'] ?? '' );
				if ( ! empty( $it['delivery_date'] ) ) {
					$row .= '<br><small style="color:#888">data dostawy: ' . $h( (string) $it['delivery_date'] ) . '</small>';
				}
				$row .= '</td>';
				$row .= '<td>' . $code . '</td>';
				$row .= '<td>' . $h( $it['unit'] ?? '' ) . '</td>';
				$row .= '<td class="r">' . $h( number_format( (float) ( $it['quantity'] ?? 0 ), 3, ',', ' ' ) ) . '</td>';
				$row .= '<td class="r">' . $h( $fmt( $it['unit_price_net'] ?? 0 ) ) . '</td>';
				$row .= '<td class="r">' . ( ! empty( $it['unit_price_gross'] ) ? $h( $fmt( $it['unit_price_gross'] ) ) : '—' ) . '</td>';
				$row .= '<td class="r">' . ( ! empty( $it['discount'] ) && (float) $it['discount'] > 0 ? $h( $fmt( $it['discount'] ) ) : '—' ) . '</td>';
				$row .= '<td>' . $rate_str . '</td>';
				$row .= '<td class="r">' . $h( $fmt( $it['line_net'] ?? 0 ) ) . '</td>';
				$row .= '<td class="r">' . $h( $fmt( $it['line_gross'] ?? 0 ) ) . '</td>';
				$row .= '<td>' . $h( $it['gtu'] ?? '' ) . '</td>';
				$row .= '</tr>';
			}
			$row .= '</tbody></table>';

			// Podsumowanie VAT — wszystkie stawki z FA(3)
			$vs = $p['vat_summary'] ?? [];
			if ( ! empty( $vs ) ) {
				$row .= '<table class="ksdt-totals" style="margin-top:14px;"><thead><tr><th style="text-align:left">Stawka</th><th class="r">Netto</th><th class="r">VAT</th><th class="r">Brutto</th></tr></thead><tbody>';
				foreach ( $vs as $v ) {
					$rl = is_numeric( $v['rate'] ?? '' ) ? ( (int) round( (float) $v['rate'] ) ) . '%' : strtoupper( (string) ( $v['rate'] ?? '' ) );
					$brutto = (float) ( $v['base_net'] ?? 0 ) + (float) ( $v['vat_amount'] ?? 0 );
					$row .= '<tr><td>' . $h( $rl ) . '</td><td class="r">' . $h( $fmt( $v['base_net'] ?? 0 ) ) . '</td><td class="r">' . $h( $fmt( $v['vat_amount'] ?? 0 ) ) . '</td><td class="r">' . $h( $fmt( $brutto ) ) . '</td></tr>';
				}
				$total_net = (float) ( $p['total_net'] ?? $inv['net_amount'] ?? 0 );
				$total_vat = (float) ( $p['total_vat'] ?? $inv['vat_amount'] ?? 0 );
				$total_gr  = (float) ( $p['total_gross'] ?? $inv['gross_amount'] ?? 0 );
				$row .= '<tr class="tot"><td>RAZEM</td><td class="r">' . $h( $fmt( $total_net ) ) . '</td><td class="r">' . $h( $fmt( $total_vat ) ) . '</td><td class="r">' . $h( $fmt( $total_gr ) ) . ' ' . $h( $cur ) . '</td></tr>';
				$row .= '</tbody></table>';
			}
			$row .= '</div>';
		}

		// ── Oznaczenia / markery ──
		$markers = $p['markers'] ?? [];
		if ( ! empty( $markers ) ) {
			$labels = [
				'FP' => 'FP — Faktura do paragonu', 'P_18A' => 'P_18A — Podzielona płatność',
				'TP' => 'TP — Powiązania między stronami', 'fa_za_okres' => 'Faktura za okres',
				'P_19' => 'P_19 — Zwolnienie z VAT', 'P_16' => 'P_16 — Metoda kasowa',
				'P_17' => 'P_17 — Samofakturowanie', 'P_18' => 'P_18 — Odwrotne obciążenie',
				'P_23' => 'P_23 — WTT uproszczona', 'PMarzy' => 'Procedura marży',
				'P_22' => 'P_22 — WDT nowych środków transportu', 'OSS' => 'Procedura OSS VAT',
			];
			$row .= '<div class="ksdt-section"><div class="ksdt-h">Oznaczenia faktury</div>';
			foreach ( $markers as $code => $_v ) {
				$row .= '<span class="ksdt-chip">' . $h( $labels[ $code ] ?? $code ) . '</span>';
			}
			$row .= '</div>';
		}

		// ── Dokumenty powiązane ──
		$rel = $p['related_docs'] ?? [];
		if ( ! empty( $rel['order']['number'] ) || ! empty( $rel['wz']['number'] ) ) {
			$row .= '<div class="ksdt-section"><div class="ksdt-h">Dokumenty powiązane</div><div class="ksdt-kv">';
			if ( ! empty( $rel['order']['number'] ) ) {
				$row .= '<span>Nr zamówienia</span><span>' . $h( $rel['order']['number'] ) . '</span>';
				if ( ! empty( $rel['order']['date'] ) ) {
					$row .= '<span>Data zamówienia</span><span>' . $h( $rel['order']['date'] ) . '</span>';
				}
			}
			if ( ! empty( $rel['wz']['number'] ) ) {
				$row .= '<span>Nr WZ</span><span>' . $h( $rel['wz']['number'] ) . '</span>';
			}
			$row .= '</div></div>';
		}

		// ── Kaucja ──
		if ( ! empty( $p['deposit_amount'] ) && (float) $p['deposit_amount'] > 0 ) {
			$row .= '<div class="ksdt-section"><div class="ksdt-h">Kaucja / opakowania zwrotne</div><div class="ksdt-kv">';
			$row .= '<span>Opis</span><span>' . $h( $p['deposit_label'] ?? '' ) . '</span>';
			$row .= '<span>Kwota</span><span><strong>' . $h( $fmt( $p['deposit_amount'] ) ) . ' ' . $h( $cur ) . '</strong></span>';
			$row .= '</div></div>';
		}

		// ── Rachunki bankowe (FA(3) może mieć >1) ──
		$banks = $p['bank_accounts'] ?? ( ! empty( $p['bank_account'] ) ? [ $p['bank_account'] ] : [] );
		if ( ! empty( $banks ) ) {
			$row .= '<div class="ksdt-section"><div class="ksdt-h">Rachunki bankowe (' . count( $banks ) . ')</div>';
			foreach ( $banks as $bnk ) {
				$row .= '<div class="ksdt-kv" style="border-bottom:1px solid #f0f1f3;padding-bottom:6px;margin-bottom:6px;">';
				if ( ! empty( $bnk['description'] ) ) {
					$row .= '<span>Opis</span><span>' . $h( $bnk['description'] ) . '</span>';
				}
				$row .= '<span>Numer konta</span><span><code>' . $h( $bnk['account_number'] ?? '' ) . '</code></span>';
				if ( ! empty( $bnk['bank_name'] ) ) $row .= '<span>Nazwa banku</span><span>' . $h( $bnk['bank_name'] ) . '</span>';
				if ( ! empty( $bnk['swift'] ) )     $row .= '<span>SWIFT/BIC</span><span>' . $h( $bnk['swift'] ) . '</span>';
				if ( ! empty( $bnk['foreign'] ) && '1' === (string) $bnk['foreign'] ) {
					$row .= '<span>Konto zagraniczne</span><span>Tak</span>';
				}
				$row .= '</div>';
			}
			$row .= '</div>';
		}

		// ── Płatność z payloadu (FA(3) Platnosc) ──
		if ( ! empty( $p['payment'] ) ) {
			$pay = $p['payment'];
			// FA(3) TFormaPlatnosci: 1=Gotówka,2=Karta,3=Bon,4=Czek,5=Kredyt,6=Przelew,7=Płatność mobilna (+legacy 9/10/99).
			$pf = [ '1' => 'Gotówka', '2' => 'Karta', '3' => 'Bon', '4' => 'Czek', '5' => 'Kredyt', '6' => 'Przelew', '7' => 'Płatność mobilna (BLIK)', '9' => 'Bon', '10' => 'Czek', '99' => 'Inna' ];
			$status = (string) ( $pay['payment_status'] ?? 'unpaid' );
			$status_label = [
				'paid'    => '<span style="color:#15803D;font-weight:600;">✓ Opłacona</span>',
				'partial' => '<span style="color:#B45309;font-weight:600;">⚠ Częściowo</span>',
				'unpaid'  => '<span style="color:#991B1B;font-weight:600;">✗ Nieopłacona</span>',
			][ $status ] ?? esc_html( $status );

			$row .= '<div class="ksdt-section"><div class="ksdt-h">Warunki płatności</div><div class="ksdt-kv">';
			$row .= '<span>Status (z faktury KSeF)</span><span>' . $status_label . '</span>';
			$row .= '<span>Forma płatności</span><span>' . $h( $pf[ (string) ( $pay['form'] ?? '6' ) ] ?? ( 'Kod ' . ( $pay['form'] ?? '' ) ) ) . '</span>';
			if ( ! empty( $pay['due_date'] ) ) {
				$overdue = strtotime( (string) $pay['due_date'] ) < strtotime( 'today' ) && 'paid' !== $status;
				$row .= '<span>Termin płatności</span><span>' . $h( $pay['due_date'] ) . ( $overdue ? ' <span style="color:#DC2626;font-weight:600;">— po terminie</span>' : '' ) . '</span>';
			}
			if ( ! empty( $pay['zaplacono_data'] ) ) {
				$row .= '<span>Data zapłaty</span><span>' . $h( $pay['zaplacono_data'] ) . '</span>';
			}
			if ( ! empty( $pay['kwota_zaplacona'] ) && (float) $pay['kwota_zaplacona'] > 0 ) {
				$row .= '<span>Kwota zapłacona</span><span>' . $h( $fmt( $pay['kwota_zaplacona'] ) ) . ' ' . $h( $cur ) . '</span>';
			}
			if ( ! empty( $pay['naleznosc_pozostala'] ) && (float) $pay['naleznosc_pozostala'] > 0 ) {
				$row .= '<span>Pozostało do zapłaty</span><span><strong>' . $h( $fmt( $pay['naleznosc_pozostala'] ) ) . ' ' . $h( $cur ) . '</strong></span>';
			}
			if ( ! empty( $pay['due_date_desc'] ) ) {
				$row .= '<span>Opis terminu</span><span>' . $h( $pay['due_date_desc'] ) . '</span>';
			}
			if ( ! empty( $pay['skonto'] ) ) {
				$row .= '<span>Skonto</span><span>' . $h( $pay['skonto'] ) . '</span>';
			}
			if ( ! empty( $pay['skonto_opis'] ) ) {
				$row .= '<span>Skonto — opis</span><span>' . $h( $pay['skonto_opis'] ) . '</span>';
			}
			$row .= '</div></div>';
		}

		// ── Stopka faktury (Wstęp / Zakończenie / Rejestracja) ──
		$footer = $p['footer'] ?? null;
		if ( is_array( $footer ) ) {
			$row .= '<div class="ksdt-section"><div class="ksdt-h">Stopka faktury</div>';
			if ( ! empty( $footer['intro'] ) ) {
				$row .= '<p><strong>Wstęp:</strong><br>' . nl2br( $h( $footer['intro'] ) ) . '</p>';
			}
			if ( ! empty( $footer['closing'] ) ) {
				$row .= '<p><strong>Zakończenie:</strong><br>' . nl2br( $h( $footer['closing'] ) ) . '</p>';
			}
			$reg = $footer['registration'] ?? null;
			if ( is_array( $reg ) ) {
				$row .= '<div class="ksdt-kv" style="margin-top:8px;">';
				if ( ! empty( $reg['organ_rejestrowy'] ) ) $row .= '<span>Organ rejestrowy</span><span>' . $h( $reg['organ_rejestrowy'] ) . '</span>';
				if ( ! empty( $reg['krs'] ) )              $row .= '<span>KRS</span><span>' . $h( $reg['krs'] ) . '</span>';
				if ( ! empty( $reg['kapital_zakladowy'] ) ) $row .= '<span>Kapitał zakładowy</span><span>' . $h( $fmt( $reg['kapital_zakladowy'] ) ) . '</span>';
				if ( ! empty( $reg['kapital_wplacony'] ) )  $row .= '<span>Kapitał wpłacony</span><span>' . $h( $fmt( $reg['kapital_wplacony'] ) ) . '</span>';
				$row .= '</div>';
			}
			$row .= '</div>';
		}

		// ── Info systemowe KSeF (nagłówek XML) ──
		$sys = $p['system_info'] ?? null;
		if ( is_array( $sys ) && ! empty( $sys['kod_formularza'] ) ) {
			$row .= '<div class="ksdt-section"><div class="ksdt-h">Metadane systemowe</div><div class="ksdt-kv">';
			$row .= '<span>Kod formularza</span><span><code>' . $h( $sys['kod_formularza'] ) . '</code></span>';
			if ( ! empty( $sys['wariant'] ) )          $row .= '<span>Wariant</span><span>' . $h( $sys['wariant'] ) . '</span>';
			if ( ! empty( $sys['data_wytworzenia'] ) ) $row .= '<span>Data wytworzenia XML</span><span>' . $h( $sys['data_wytworzenia'] ) . '</span>';
			if ( ! empty( $sys['system_info'] ) )      $row .= '<span>System wystawcy</span><span>' . $h( $sys['system_info'] ) . '</span>';
			if ( ! empty( $sys['kod_urzedu'] ) )       $row .= '<span>Kod urzędu</span><span>' . $h( $sys['kod_urzedu'] ) . '</span>';
			$row .= '</div></div>';
		}

		// ── Notatki KSeF (key/value) ──
		$kn = $p['ksef_custom_notes'] ?? [];
		if ( ! empty( $kn ) && is_array( $kn ) ) {
			$row .= '<div class="ksdt-section"><div class="ksdt-h">Notatki KSeF (DodatkowyOpis)</div><div class="ksdt-kv">';
			foreach ( $kn as $kv ) {
				if ( empty( $kv['key'] ) ) continue;
				$row .= '<span>' . $h( $kv['key'] ) . '</span><span>' . $h( $kv['value'] ?? '' ) . '</span>';
			}
			$row .= '</div></div>';
		}

		// ── Uwagi wewnętrzne ──
		$notes = trim( (string) ( $p['internal_notes'] ?? $p['notes'] ?? '' ) );
		if ( '' !== $notes ) {
			$row .= '<div class="ksdt-section"><div class="ksdt-h">Uwagi do faktury (wewnętrzne)</div>';
			$row .= '<div style="white-space:pre-wrap;font-size:13px;">' . $h( $notes ) . '</div></div>';
		}

		return $row;
	}

	/**
	 * AJAX: Wyślij ponownie offline fakturę
	 *
	 * @return void
	 */
	public function ajax_retry_offline_invoice() {
		global $wpdb;

		check_ajax_referer( 'ksef-nonce' );

		if ( ! current_user_can( 'ksef_view_outgoing' ) ) {
			wp_send_json_error( __( 'Brak uprawnień.', 'prostafaktura-ksef' ) );
		}

		$invoice_id = intval( $_POST['invoice_id'] ?? 0 );

		if ( ! $invoice_id ) {
			wp_send_json_error( __( 'Invalid invoice ID.', 'prostafaktura-ksef' ) );
		}

		// Pobierz fakturę
		$invoice = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}gksef_outgoing_invoices WHERE id = %d AND status = 'offline'",
				$invoice_id
			),
			ARRAY_A
		);

		if ( ! $invoice ) {
			wp_send_json_error( __( 'Invoice not found or is not offline.', 'prostafaktura-ksef' ) );
		}

		// Ustaw status na pending dla retry
		$wpdb->update(
			$wpdb->prefix . 'gksef_outgoing_invoices',
			[ 'status' => 'pending' ],
			[ 'id' => $invoice_id ]
		);

		wp_send_json_success( [ 'message' => __( 'Invoice marked for retry.', 'prostafaktura-ksef' ) ] );
	}

	/**
	 * AJAX: Wyślij testowy email
	 *
	 * @return void
	 */
	public function ajax_send_test_email() {
		check_ajax_referer( 'ksef-nonce', 'nonce' );

		if ( ! current_user_can( 'ksef_view_settings' ) ) {
			wp_send_json_error( __( 'Brak uprawnień.', 'prostafaktura-ksef' ) );
		}

		$recipient = get_option( 'gksef_email_recipient' );
		if ( ! $recipient ) {
			$recipient = get_option( 'admin_email' );
		}

		$subject = __( '[KSEF] Test powiadomienia', 'prostafaktura-ksef' );
		$body    = '<h2>' . esc_html__( 'Testowy email KSeF', 'prostafaktura-ksef' ) . '</h2>';
		$body    .= '<p>' . esc_html__( 'Jeśli czytasz tę wiadomość, powiadomienia email są skonfigurowane prawidłowo.', 'prostafaktura-ksef' ) . '</p>';

		$headers = [ 'Content-Type: text/html; charset=UTF-8' ];

		$result = wp_mail( $recipient, $subject, $body, $headers );

		if ( $result ) {
			wp_send_json_success( [ 'message' => __( 'Test email sent successfully.', 'prostafaktura-ksef' ) ] );
		} else {
			wp_send_json_error( __( 'Failed to send test email.', 'prostafaktura-ksef' ) );
		}
	}

	/**
	 * AJAX: Export invoices
	 *
	 * @return void
	 */
	public function ajax_export_invoices() {
		check_ajax_referer( 'ksef-nonce' );

		// Export wymaga dostępu do dowolnego typu faktur (typ wybierany w params)
		if ( ! current_user_can( 'ksef_view_outgoing' ) && ! current_user_can( 'ksef_view_incoming' ) ) {
			wp_send_json_error( __( 'Brak uprawnień.', 'prostafaktura-ksef' ) );
		}

		$type   = sanitize_text_field( wp_unslash( $_POST['type'] ?? '' ) );
		$format = sanitize_text_field( wp_unslash( $_POST['format'] ?? 'csv' ) );

		// Filtry
		$filters = [];
		if ( ! empty( $_POST['status'] ) ) {
			$filters['status'] = sanitize_text_field( wp_unslash( $_POST['status'] ) );
		}
		if ( ! empty( $_POST['nip'] ) ) {
			$filters['nip'] = sanitize_text_field( wp_unslash( $_POST['nip'] ) );
		}
		if ( ! empty( $_POST['from_date'] ) ) {
			$filters['from_date'] = sanitize_text_field( wp_unslash( $_POST['from_date'] ) );
		}
		if ( ! empty( $_POST['to_date'] ) ) {
			$filters['to_date'] = sanitize_text_field( wp_unslash( $_POST['to_date'] ) );
		}

		// Export
		if ( 'csv' === $format ) {
			Invoice_Export::export_csv( $type, $filters );
		} elseif ( 'pdf' === $format ) {
			Invoice_Export::export_pdf( $type, $filters );
		} else {
			wp_send_json_error( __( 'Invalid format.', 'prostafaktura-ksef' ) );
		}
	}

	/**
	 * AJAX: dodaj płatność do faktury (multi-rata).
	 */
	public function ajax_add_payment() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_outgoing' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$invoice_id = intval( wp_unslash( $_POST['invoice_id'] ?? 0 ) );
		if ( $invoice_id <= 0 ) {
			wp_send_json_error( [ 'message' => 'Brak invoice_id.' ], 400 );
		}
		$id = Payments_Repository::add( $invoice_id, [
			'amount'         => floatval( wp_unslash( $_POST['amount'] ?? 0 ) ),
			'payment_date'   => sanitize_text_field( wp_unslash( $_POST['payment_date'] ?? '' ) ),
			'payment_method' => sanitize_text_field( wp_unslash( $_POST['payment_method'] ?? '' ) ),
			'reference'      => sanitize_text_field( wp_unslash( $_POST['reference'] ?? '' ) ),
			'note'           => sanitize_textarea_field( wp_unslash( $_POST['note'] ?? '' ) ),
			'currency'       => sanitize_text_field( wp_unslash( $_POST['currency'] ?? 'PLN' ) ),
		] );
		if ( is_wp_error( $id ) ) {
			wp_send_json_error( [ 'message' => $id->get_error_message() ], 400 );
		}
		wp_send_json_success( [
			'payment_id' => $id,
			'summary'    => Payments_Repository::summary( $invoice_id ),
			'payments'   => Payments_Repository::list_for_invoice( $invoice_id ),
		] );
	}

	public function ajax_delete_payment() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_outgoing' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$payment_id = intval( wp_unslash( $_POST['payment_id'] ?? 0 ) );
		$invoice_id = intval( wp_unslash( $_POST['invoice_id'] ?? 0 ) );
		if ( ! Payments_Repository::delete( $payment_id ) ) {
			wp_send_json_error( [ 'message' => 'Nie znaleziono płatności.' ], 404 );
		}
		wp_send_json_success( [
			'summary'  => Payments_Repository::summary( $invoice_id ),
			'payments' => Payments_Repository::list_for_invoice( $invoice_id ),
		] );
	}

	/**
	 * AJAX: szybka zmiana statusu płatności prosto z listy (outgoing/incoming).
	 * POST: invoice_id, type ('outgoing'|'incoming'), status ('paid'|'partial'|'unpaid').
	 * Nie tworzy rekordu w gksef_payments — to tylko ręczna nadpisana flaga
	 * dla faktur importowanych z KSeF lub starszych (gdzie nie prowadzimy szczegółowych płatności).
	 */
	public function ajax_set_payment_status() {
		check_ajax_referer( 'ksef-nonce' );
		$type = sanitize_text_field( wp_unslash( $_POST['type'] ?? '' ) );
		$cap  = ( 'incoming' === $type ) ? 'ksef_view_incoming' : 'ksef_view_outgoing';
		if ( ! current_user_can( $cap ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		global $wpdb;
		$invoice_id = intval( wp_unslash( $_POST['invoice_id'] ?? 0 ) );
		$status     = sanitize_text_field( wp_unslash( $_POST['status'] ?? '' ) );
		if ( $invoice_id <= 0 ) {
			wp_send_json_error( [ 'message' => 'Brak invoice_id.' ], 400 );
		}
		if ( ! in_array( $status, [ 'paid', 'partial', 'unpaid' ], true ) ) {
			wp_send_json_error( [ 'message' => 'Nieprawidłowy status.' ], 400 );
		}
		$table = ( 'incoming' === $type )
			? $wpdb->prefix . 'gksef_incoming_invoices'
			: $wpdb->prefix . 'gksef_outgoing_invoices';
		$data = [ 'payment_status' => $status ];
		if ( 'paid' === $status ) {
			$data['paid_date'] = gmdate( 'Y-m-d' );
		}
		$ok = $wpdb->update( $table, $data, [ 'id' => $invoice_id ] );
		if ( false === $ok ) {
			wp_send_json_error( [ 'message' => 'Błąd bazy: ' . $wpdb->last_error ], 500 );
		}
		wp_send_json_success( [ 'invoice_id' => $invoice_id, 'status' => $status ] );
	}

	/**
	 * AJAX: ustaw kategorię wydatku dla faktury zakupu (kolumna category).
	 */
	public function ajax_set_incoming_category() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_incoming' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		global $wpdb;
		$invoice_id = intval( wp_unslash( $_POST['invoice_id'] ?? 0 ) );
		$category   = sanitize_text_field( wp_unslash( $_POST['category'] ?? '' ) );
		if ( $invoice_id <= 0 ) {
			wp_send_json_error( [ 'message' => 'Brak invoice_id.' ], 400 );
		}
		$ok = $wpdb->update(
			$wpdb->prefix . 'gksef_incoming_invoices',
			[ 'category' => mb_substr( $category, 0, 64 ) ],
			[ 'id' => $invoice_id ],
			[ '%s' ],
			[ '%d' ]
		);
		if ( false === $ok ) {
			wp_send_json_error( [ 'message' => 'Błąd bazy: ' . $wpdb->last_error ], 500 );
		}
		wp_send_json_success( [ 'invoice_id' => $invoice_id, 'category' => $category ] );
	}

	/**
	 * AJAX: pobierz fakturę jako PDF (strumieniowo).
	 */
	public function ajax_invoice_pdf() {
		check_ajax_referer( 'ksef-nonce' );
		$type = ( ( sanitize_text_field( wp_unslash( $_REQUEST['type'] ?? '' ) ) ) === 'incoming' ) ? 'incoming' : 'outgoing';
		$cap  = ( 'incoming' === $type ) ? 'ksef_view_incoming' : 'ksef_view_outgoing';
		if ( ! current_user_can( $cap ) ) {
			wp_die( 'Brak uprawnień.', '', [ 'response' => 403 ] );
		}
		$invoice_id = intval( wp_unslash( $_REQUEST['invoice_id'] ?? 0 ) );
		if ( $invoice_id <= 0 ) {
			wp_die( 'Brak invoice_id.', '', [ 'response' => 400 ] );
		}
		require_once KSEF_WORDPRESS_PATH . 'includes/class-invoice-pdf.php';
		Invoice_PDF::stream( $invoice_id, $type, ! empty( $_REQUEST['preview'] ) );
	}

	/**
	 * AJAX: wyślij fakturę PDF na podany e-mail.
	 */
	public function ajax_invoice_email() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_outgoing' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$invoice_id = intval( wp_unslash( $_POST['invoice_id'] ?? 0 ) );
		$to         = sanitize_email( wp_unslash( $_POST['to'] ?? '' ) );
		if ( $invoice_id <= 0 || ! is_email( $to ) ) {
			wp_send_json_error( [ 'message' => 'Brak invoice_id lub nieprawidłowy email.' ], 400 );
		}
		require_once KSEF_WORDPRESS_PATH . 'includes/class-invoice-pdf.php';
		$subject = isset( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : null;
		$body    = isset( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : null;
		$result  = Invoice_PDF::send_email( $invoice_id, $to, $subject, $body );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
		}
		wp_send_json_success( $result );
	}

	/**
	 * AJAX: podglad tresci maila (temat + tresc wyrenderowane + sugerowany email) do modala wysylki.
	 */
	public function ajax_invoice_email_preview() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_outgoing' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnien.' ], 403 );
		}
		$invoice_id = intval( wp_unslash( $_POST['invoice_id'] ?? 0 ) );
		if ( $invoice_id <= 0 ) {
			wp_send_json_error( [ 'message' => 'Brak invoice_id.' ], 400 );
		}
		require_once KSEF_WORDPRESS_PATH . 'includes/class-invoice-pdf.php';
		wp_send_json_success( Invoice_PDF::email_preview( $invoice_id ) );
	}
	/**
	 * AJAX: szukaj kontrahentów (po nazwie / NIP). Zwraca tablicę dla autocomplete.
	 */
	public function ajax_search_contractors() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_contractors' ) && ! current_user_can( 'ksef_create_invoice' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		global $wpdb;
		$q = trim( (string) sanitize_text_field( wp_unslash( $_GET['q'] ?? $_POST['q'] ?? '' ) ) );
		$like = '%' . $wpdb->esc_like( $q ) . '%';
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}gksef_contractor_mappings
				WHERE contractor_name LIKE %s OR contractor_nip LIKE %s
				ORDER BY contractor_name ASC LIMIT 50",
				$like, $like
			),
			ARRAY_A
		);
		wp_send_json_success( [ 'items' => $rows ?: [] ] );
	}

	// GUS (auto-uzupełnianie danych firmy z rejestru GUS po NIP) to funkcja wersji Pro —
	// wymaga zewnętrznej usługi (serwer/proxy GUS) i nie jest częścią wersji darmowej.

	public function ajax_save_contractor() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_contractors' ) && ! current_user_can( 'ksef_create_invoice' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		global $wpdb;
		$id  = intval( wp_unslash( $_POST['id'] ?? 0 ) );
		$name = trim( (string) sanitize_text_field( wp_unslash( $_POST['contractor_name'] ?? '' ) ) );
		if ( '' === $name ) {
			wp_send_json_error( [ 'message' => 'Brak nazwy.' ], 400 );
		}
		$data = [
			'contractor_name' => $name,
			'contractor_nip'  => preg_replace( '/\D+/', '', (string) sanitize_text_field( wp_unslash( $_POST['contractor_nip'] ?? '' ) ) ),
			'buyer_type'      => (string) sanitize_text_field( wp_unslash( $_POST['buyer_type'] ?? 'pl_firm' ) ),
			'country_code'    => strtoupper( (string) sanitize_text_field( wp_unslash( $_POST['country_code'] ?? 'PL' ) ) ),
			'vat_ue'          => sanitize_text_field( wp_unslash( $_POST['vat_ue'] ?? '' ) ),
			'nr_id'           => sanitize_text_field( wp_unslash( $_POST['nr_id'] ?? '' ) ),
			'street'          => sanitize_text_field( wp_unslash( $_POST['street'] ?? '' ) ),
			'street_number'   => sanitize_text_field( wp_unslash( $_POST['street_number'] ?? '' ) ),
			'postal_code'     => sanitize_text_field( wp_unslash( $_POST['postal_code'] ?? '' ) ),
			'city'            => sanitize_text_field( wp_unslash( $_POST['city'] ?? '' ) ),
			'email'           => sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ),
			'phone'           => sanitize_text_field( wp_unslash( $_POST['phone'] ?? '' ) ),
			'customer_no'     => sanitize_text_field( wp_unslash( $_POST['customer_no'] ?? '' ) ),
		];
		if ( $id > 0 ) {
			$wpdb->update( $wpdb->prefix . 'gksef_contractor_mappings', $data, [ 'id' => $id ] );
		} else {
			$wpdb->insert( $wpdb->prefix . 'gksef_contractor_mappings', $data );
			$id = (int) $wpdb->insert_id;
		}
		// Świadomy zapis kontrahenta zdejmuje nagrobek (sync znów może go uzupełniać).
		\KSEF\WordPress\V2\Contractors_Sync::remove_tombstone( (string) $data['contractor_nip'], (string) $data['contractor_name'] );
		$row = $wpdb->get_row(
			$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}gksef_contractor_mappings WHERE id = %d", $id ),
			ARRAY_A
		);
		wp_send_json_success( $row );
	}

	public function ajax_search_wc_products() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_create_invoice' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		if ( ! function_exists( 'wc_get_products' ) ) {
			wp_send_json_error( [ 'message' => 'WooCommerce nieaktywne.' ], 400 );
		}
		$q = trim( (string) sanitize_text_field( wp_unslash( $_GET['q'] ?? $_POST['q'] ?? '' ) ) );
		$products = wc_get_products( [
			'limit'  => 30,
			's'      => $q,
			'status' => 'publish',
		] );
		$out = [];
		foreach ( $products as $p ) {
			$tax_rate = 23;
			$tax_class = $p->get_tax_class();
			if ( wc_tax_enabled() ) {
				$rates = \WC_Tax::get_rates( $tax_class );
				$rate  = is_array( $rates ) ? reset( $rates ) : null;
				if ( $rate ) {
					$tax_rate = (float) $rate['rate'];
				}
			}
			$price_net = wc_get_price_excluding_tax( $p ) ?: $p->get_price();
			$out[] = [
				'id'            => $p->get_id(),
				'name'          => $p->get_name(),
				'sku'           => $p->get_sku(),
				'unit_price'    => (float) $price_net,
				'vat_rate'      => (float) $tax_rate,
				'unit'          => 'szt.',
				'pkwiu'         => $p->get_meta( '_pkwiu' ) ?: '',
				'gtu'           => $p->get_meta( '_ksef_gtu' ) ?: '',
			];
		}
		wp_send_json_success( [ 'items' => $out ] );
	}

	/**
	 * AJAX: kurs średni NBP (tab. A) dla waluty obcej — z ostatniego dnia roboczego
	 * przed podaną datą (data powstania obowiązku podatkowego). Używany do P_14_xW + KursWalutyZ.
	 */
	public function ajax_fetch_nbp_rate() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_create_invoice' ) && ! current_user_can( 'ksef_view_settings' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$currency = strtoupper( (string) preg_replace( '/[^A-Za-z]/', '', sanitize_text_field( (string) wp_unslash( $_POST['currency'] ?? '' ) ) ) );
		if ( '' === $currency || 'PLN' === $currency ) {
			wp_send_json_error( [ 'message' => 'Waluta PLN nie wymaga kursu.' ] );
		}
		$date = (string) sanitize_text_field( wp_unslash( $_POST['date'] ?? '' ) );
		$ts   = ( $date && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) ? strtotime( $date ) : time();
		if ( ! $ts ) { $ts = time(); }
		for ( $i = 1; $i <= 10; $i++ ) {
			$d    = gmdate( 'Y-m-d', $ts - $i * DAY_IN_SECONDS );
			$url  = 'https://api.nbp.pl/api/exchangerates/rates/a/' . strtolower( $currency ) . '/' . $d . '/?format=json';
			$resp = wp_remote_get( $url, [ 'timeout' => 8, 'headers' => [ 'Accept' => 'application/json' ] ] );
			if ( is_wp_error( $resp ) ) { continue; }
			if ( 200 !== (int) wp_remote_retrieve_response_code( $resp ) ) { continue; } // 404 = brak notowania (weekend/święto)
			$body = json_decode( (string) wp_remote_retrieve_body( $resp ), true );
			$rate = $body['rates'][0]['mid'] ?? null;
			if ( $rate ) {
				wp_send_json_success( [
					'rate'     => (float) $rate,
					'date'     => (string) ( $body['rates'][0]['effectiveDate'] ?? $d ),
					'table'    => 'A',
					'currency' => $currency,
				] );
			}
		}
		wp_send_json_error( [ 'message' => 'Nie znaleziono kursu NBP dla ' . $currency . ' — wpisz ręcznie.' ] );
	}

	public function ajax_list_bank_accounts() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_create_invoice' ) && ! current_user_can( 'ksef_view_settings' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		global $wpdb;
		$cred_id = intval( wp_unslash( $_GET['credential_id'] ?? $_POST['credential_id'] ?? 0 ) );
		if ( $cred_id <= 0 ) {
			wp_send_json_success( [ 'items' => [] ] );
		}
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}gksef_bank_accounts WHERE credential_id = %d ORDER BY is_default DESC, display_order ASC, id ASC",
				$cred_id
			),
			ARRAY_A
		);
		wp_send_json_success( [ 'items' => $rows ?: [] ] );
	}

	public function ajax_save_bank_account() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_settings' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		global $wpdb;
		$cred_id = intval( wp_unslash( $_POST['credential_id'] ?? 0 ) );
		$bank_id = intval( wp_unslash( $_POST['id'] ?? 0 ) );
		$acc     = preg_replace( '/\s+/', '', (string) sanitize_text_field( wp_unslash( $_POST['account_number'] ?? '' ) ) );
		if ( $cred_id <= 0 || $acc === '' ) {
			wp_send_json_error( [ 'message' => 'Wybierz konto KSeF i podaj numer rachunku.' ], 400 );
		}
		$fields = [
			'credential_id'  => $cred_id,
			'account_number' => $acc,
			'bank_name'      => sanitize_text_field( wp_unslash( $_POST['bank_name'] ?? '' ) ),
			'swift'          => sanitize_text_field( wp_unslash( $_POST['swift'] ?? '' ) ),
			'currency'       => strtoupper( substr( (string) sanitize_text_field( wp_unslash( $_POST['currency'] ?? 'PLN' ) ), 0, 3 ) ),
			'is_default'     => empty( $_POST['is_default'] ) ? 0 : 1,
		];
		if ( $bank_id > 0 ) {
			$wpdb->update( $wpdb->prefix . 'gksef_bank_accounts', $fields, [ 'id' => $bank_id ], null, [ '%d' ] );
			$id = $bank_id;
		} else {
			$wpdb->insert( $wpdb->prefix . 'gksef_bank_accounts', $fields );
			$id = (int) $wpdb->insert_id;
		}
		if ( ! empty( $_POST['is_default'] ) ) {
			$wpdb->query( $wpdb->prepare(
				"UPDATE {$wpdb->prefix}gksef_bank_accounts SET is_default = 0 WHERE credential_id = %d AND id <> %d",
				$cred_id, $id
			) );
		}
		wp_send_json_success( [ 'id' => $id ] );
	}

	public function ajax_delete_bank_account() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_settings' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		global $wpdb;
		$id = intval( wp_unslash( $_POST['id'] ?? 0 ) );
		if ( $id <= 0 ) {
			wp_send_json_error( [ 'message' => 'Brak id.' ], 400 );
		}
		$wpdb->delete( $wpdb->prefix . 'gksef_bank_accounts', [ 'id' => $id ], [ '%d' ] );
		wp_send_json_success( [ 'deleted' => $id ] );
	}

	// === Presety „Dane dodatkowe" (zestawy danych sprzedawcy) — przechowywane w opcji ===
	const OPT_EXTRA_PRESETS = 'gksef_extra_data_presets';

	public function ajax_list_extra_presets() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_create_invoice' ) && ! current_user_can( 'ksef_view_settings' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$items = get_option( self::OPT_EXTRA_PRESETS, [] );
		$items = is_array( $items ) ? array_values( $items ) : [];
		// Filtr per konto KSeF: gdy podano credential_id, zwróć presety przypisane do tego konta
		// LUB globalne (credential_id == 0 = „wszystkie konta"). Bez parametru = wszystkie (Ustawienia).
		$cred_id = isset( $_GET['credential_id'] ) || isset( $_POST['credential_id'] )
			? intval( wp_unslash( $_GET['credential_id'] ?? $_POST['credential_id'] ?? 0 ) ) : null;
		if ( null !== $cred_id && $cred_id > 0 ) {
			$items = array_values( array_filter(
				$items,
				static fn( $it ) => 0 === (int) ( $it['credential_id'] ?? 0 ) || (int) ( $it['credential_id'] ?? 0 ) === $cred_id
			) );
		}
		wp_send_json_success( [ 'items' => $items ] );
	}

	public function ajax_save_extra_preset() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_create_invoice' ) && ! current_user_can( 'ksef_view_settings' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) );
		if ( '' === trim( $name ) ) {
			wp_send_json_error( [ 'message' => 'Podaj nazwę zestawu.' ], 400 );
		}
		$items = get_option( self::OPT_EXTRA_PRESETS, [] );
		$items = is_array( $items ) ? array_values( $items ) : [];
		$id    = intval( wp_unslash( $_POST['id'] ?? 0 ) );
		$is_def = ! empty( $_POST['is_default'] );
		$cred_id = intval( wp_unslash( $_POST['credential_id'] ?? 0 ) ); // 0 = wszystkie konta KSeF
		$row = [
			'id'            => $id > 0 ? $id : ( time() * 1000 + wp_rand( 0, 999 ) ),
			'name'          => $name,
			'credential_id' => $cred_id,
			'seller_email'  => sanitize_text_field( wp_unslash( $_POST['seller_email'] ?? '' ) ),
			'seller_phone'  => sanitize_text_field( wp_unslash( $_POST['seller_phone'] ?? '' ) ),
			'issue_place'   => sanitize_text_field( wp_unslash( $_POST['issue_place'] ?? '' ) ),
			'is_default'    => $is_def ? 1 : 0,
		];
		$found = false;
		foreach ( $items as $k => $it ) {
			if ( (int) ( $it['id'] ?? 0 ) === (int) $row['id'] ) { $items[ $k ] = $row; $found = true; }
			// „Domyślny" jest per konto KSeF — zdejmij flagę tylko z presetów tego samego przypisania.
			elseif ( $is_def && (int) ( $it['credential_id'] ?? 0 ) === $cred_id ) { $items[ $k ]['is_default'] = 0; }
		}
		if ( ! $found ) { $items[] = $row; }
		update_option( self::OPT_EXTRA_PRESETS, $items, false );
		wp_send_json_success( [ 'id' => $row['id'], 'items' => array_values( $items ) ] );
	}

	public function ajax_delete_extra_preset() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_create_invoice' ) && ! current_user_can( 'ksef_view_settings' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$id    = intval( wp_unslash( $_POST['id'] ?? 0 ) );
		$items = get_option( self::OPT_EXTRA_PRESETS, [] );
		$items = is_array( $items ) ? array_values( $items ) : [];
		$items = array_values( array_filter( $items, static fn( $it ) => (int) ( $it['id'] ?? 0 ) !== $id ) );
		update_option( self::OPT_EXTRA_PRESETS, $items, false );
		wp_send_json_success( [ 'deleted' => $id, 'items' => $items ] );
	}

	/**
	 * AJAX: pobierz z KSeF faktury sprzedażowe (wystawione z innych źródeł — księgowa, inne narzędzia).
	 * Filtr: Subject1 (wystawca) = NIP wybranego kredencjału.
	 */
	/**
	 * AJAX: usuń fakturę — dopuszczalne tylko dla statusu 'draft' (gdy nie wysłana do KSeF).
	 */
	public function ajax_delete_invoice() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_outgoing' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$id = intval( wp_unslash( $_POST['invoice_id'] ?? 0 ) );
		if ( $id <= 0 ) {
			wp_send_json_error( [ 'message' => 'Brak invoice_id.' ], 400 );
		}
		global $wpdb;
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT id, status, invoice_number, ksef_reference_number FROM {$wpdb->prefix}gksef_outgoing_invoices WHERE id = %d", $id ), ARRAY_A );
		if ( ! $row ) {
			wp_send_json_error( [ 'message' => 'Faktura nie istnieje.' ], 404 );
		}
		$was_in_ksef = ! empty( $row['ksef_reference_number'] );
		// Usuwamy z bazy lokalnej. Faktura w KSeF (urzędowy rejestr) zostaje —
		// nie ma fizycznej możliwości usunięcia jej z KSeF, wyłącznie anulacja/korekta.
		// JS przed wywołaniem tego endpointu pokazuje stosowne ostrzeżenie.
		self::delete_invoice_attachments( 'outgoing', $id );
		$wpdb->delete( $wpdb->prefix . 'gksef_invoice_payments', [ 'invoice_id' => $id ], [ '%d' ] );
		$wpdb->delete( $wpdb->prefix . 'gksef_outgoing_invoices', [ 'id' => $id ], [ '%d' ] );
		wp_send_json_success( [
			'deleted'       => $id,
			'was_in_ksef'   => $was_in_ksef,
			'ksef_ref'      => $row['ksef_reference_number'],
			'invoice_number'=> $row['invoice_number'],
		] );
	}

	/**
	 * AJAX: usuń fakturę zakupową z bazy wewnętrznej. Faktura w KSeF (urzędowy
	 * rejestr) zostaje — usuwamy tylko nasz rekord.
	 */
	public function ajax_delete_incoming_invoice() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_incoming' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$id = intval( wp_unslash( $_POST['invoice_id'] ?? 0 ) );
		if ( $id <= 0 ) {
			wp_send_json_error( [ 'message' => 'Brak invoice_id.' ], 400 );
		}
		global $wpdb;
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT id, invoice_number, ksef_reference_number FROM {$wpdb->prefix}gksef_incoming_invoices WHERE id = %d", $id ), ARRAY_A );
		if ( ! $row ) {
			wp_send_json_error( [ 'message' => 'Faktura nie istnieje.' ], 404 );
		}
		$was_in_ksef = ! empty( $row['ksef_reference_number'] );
		self::delete_invoice_attachments( 'incoming', $id );
		$wpdb->delete( $wpdb->prefix . 'gksef_incoming_payments', [ 'incoming_invoice_id' => $id ], [ '%d' ] );
		$wpdb->delete( $wpdb->prefix . 'gksef_incoming_invoices', [ 'id' => $id ], [ '%d' ] );
		wp_send_json_success( [
			'deleted'       => $id,
			'was_in_ksef'   => $was_in_ksef,
			'ksef_ref'      => $row['ksef_reference_number'],
			'invoice_number'=> $row['invoice_number'],
		] );
	}

	/**
	 * Usuwa wszystkie załączniki danej faktury (rekordy w gksef_invoice_attachments
	 * + fizyczne pliki w katalogu prywatnym + sam katalog faktury). Bezpieczne do
	 * wywołania nawet gdy faktura nie ma żadnych załączników.
	 *
	 * @param string $type 'outgoing' | 'incoming'
	 * @param int    $id   ID faktury.
	 * @return int Liczba usuniętych plików.
	 */
	private static function delete_invoice_attachments( string $type, int $id ): int {
		global $wpdb;
		$type = in_array( $type, [ 'outgoing', 'incoming' ], true ) ? $type : 'incoming';
		if ( $id <= 0 ) {
			return 0;
		}
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, file_path FROM {$wpdb->prefix}gksef_invoice_attachments WHERE invoice_type = %s AND invoice_id = %d",
				$type,
				$id
			),
			ARRAY_A
		);
		$deleted_files = 0;
		if ( $rows ) {
			require_once KSEF_WORDPRESS_PATH . 'includes/class-secure-storage.php';
			foreach ( $rows as $att ) {
				if ( ! empty( $att['file_path'] ) && \KSEF\WordPress\Secure_Storage::delete( (string) $att['file_path'] ) ) {
					$deleted_files++;
				}
			}
			$wpdb->delete( $wpdb->prefix . 'gksef_invoice_attachments', [ 'invoice_type' => $type, 'invoice_id' => $id ], [ '%s', '%d' ] );
			// Usuń też pusty katalog faktury (ksef-private/{type}/{id}).
			$dir = \KSEF\WordPress\Secure_Storage::full_path( $type . '/' . $id );
			if ( is_dir( $dir ) ) {
				@rmdir( $dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- lokalne operacje na wlasnych plikach wtyczki (cache/upload).
			}
		}
		return $deleted_files;
	}

	/**
	 * AJAX: masowe usuwanie faktur (sprzedaży lub zakupu) z bazy wtyczki wraz
	 * z płatnościami i załącznikami. Faktury wysłane do KSeF pozostają w urzędowym
	 * rejestrze MF — usuwamy WYŁĄCZNIE lokalny rekord (JS pokazuje ostrzeżenie).
	 *
	 * Parametry POST:
	 *  - type: 'outgoing' | 'incoming'
	 *  - ids:  tablica ID (gdy zaznaczono pojedyncze) — opcjonalnie
	 *  - all:  '1' aby usunąć WSZYSTKIE faktury danego typu (z uwzględnieniem filtrów)
	 */
	public function ajax_bulk_delete_invoices() {
		check_ajax_referer( 'ksef-nonce' );
		$type = sanitize_text_field( wp_unslash( $_POST['type'] ?? 'outgoing' ) );
		$type = in_array( $type, [ 'outgoing', 'incoming' ], true ) ? $type : 'outgoing';
		$cap  = ( 'incoming' === $type ) ? 'ksef_view_incoming' : 'ksef_view_outgoing';
		if ( ! current_user_can( $cap ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}

		global $wpdb;
		$inv_table  = ( 'incoming' === $type ) ? $wpdb->prefix . 'gksef_incoming_invoices' : $wpdb->prefix . 'gksef_outgoing_invoices';
		$pay_table  = ( 'incoming' === $type ) ? $wpdb->prefix . 'gksef_incoming_payments' : $wpdb->prefix . 'gksef_invoice_payments';
		$pay_col    = ( 'incoming' === $type ) ? 'incoming_invoice_id' : 'invoice_id';

		$all = ! empty( $_POST['all'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['all'] ) );
		$ids = [];
		if ( ! $all ) {
			$raw = map_deep( wp_unslash( $_POST['ids'] ?? [] ), 'sanitize_text_field' );
			$raw = is_array( $raw ) ? $raw : explode( ',', (string) $raw );
			foreach ( $raw as $v ) {
				$v = (int) $v;
				if ( $v > 0 ) {
					$ids[] = $v;
				}
			}
			$ids = array_values( array_unique( $ids ) );
			if ( empty( $ids ) ) {
				wp_send_json_error( [ 'message' => 'Nie wybrano żadnych faktur.' ], 400 );
			}
		}

		// Pobierz docelowe rekordy (id + czy w KSeF) zanim usuniemy.
		if ( $all ) {
			$rows = $wpdb->get_results( "SELECT id, ksef_reference_number FROM {$inv_table}", ARRAY_A );
		} else {
			$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
			$rows = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT id, ksef_reference_number FROM {$inv_table} WHERE id IN ($placeholders)", // phpcs:ignore WordPress.DB.PreparedSQL
					...$ids
				),
				ARRAY_A
			);
		}

		if ( empty( $rows ) ) {
			wp_send_json_error( [ 'message' => 'Brak faktur do usunięcia.' ], 404 );
		}

		$deleted     = 0;
		$in_ksef     = 0;
		$files       = 0;
		foreach ( $rows as $r ) {
			$rid = (int) $r['id'];
			if ( ! empty( $r['ksef_reference_number'] ) ) {
				$in_ksef++;
			}
			$files += self::delete_invoice_attachments( $type, $rid );
			$wpdb->delete( $pay_table, [ $pay_col => $rid ], [ '%d' ] );
			$wpdb->delete( $inv_table, [ 'id' => $rid ], [ '%d' ] );
			$deleted++;
		}

		wp_send_json_success( [
			'deleted'  => $deleted,
			'in_ksef'  => $in_ksef,
			'files'    => $files,
			'type'     => $type,
		] );
	}

	/**
	 * AJAX: pobierz UPO XML lub PDF (UPO PDF = wyrenderowany z UPO XML).
	 */
	public function ajax_download_upo() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_outgoing' ) ) {
			wp_die( 'Brak uprawnień.', '', [ 'response' => 403 ] );
		}
		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( 30 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,Squiz.PHP.DiscouragedFunctions.Discouraged -- bezpiecznik czasu handlera pobierania UPO.
		}
		$id     = intval( wp_unslash( $_REQUEST['invoice_id'] ?? 0 ) );
		$format = sanitize_text_field( wp_unslash( $_REQUEST['format'] ?? 'pdf' ) );
		if ( $id <= 0 ) {
			wp_die( 'Brak invoice_id.', '', [ 'response' => 400 ] );
		}
		global $wpdb;
		$inv = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}gksef_outgoing_invoices WHERE id = %d", $id ), ARRAY_A );
		if ( ! $inv ) {
			wp_die( 'Faktura nie istnieje.', '', [ 'response' => 404 ] );
		}
		// 1) NAJPIERW zapisane UPO z bazy — bez blokującego callu do KSeF (pobranie natychmiastowe).
		$upo_xml = (string) ( $inv['upo_xml'] ?? '' );
		// 2) Brak lokalnego UPO → jednorazowy fetch z KSeF, wynik zapisz do bazy (cache na przyszłość).
		if ( '' === trim( $upo_xml ) ) {
			if ( empty( $inv['ksef_session_id'] ) ) {
				wp_die( 'UPO niedostępne (brak sesji KSeF i brak zapisanego UPO).', '', [ 'response' => 400 ] );
			}
			$cred = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}gksef_credentials WHERE id = %d", (int) $inv['credential_id'] ), ARRAY_A );
			if ( ! $cred ) {
				wp_die( 'Kredencjał faktury nie istnieje.', '', [ 'response' => 404 ] );
			}
			$poller = \KSEF\WordPress\V2\Bootstrap::poller_for_credential( $cred );
			$check  = $poller->check_session( (string) $inv['ksef_session_id'], $cred );
			if ( is_wp_error( $check ) ) {
				wp_die( esc_html( 'KSeF: ' . $check->get_error_message() ), '', [ 'response' => 502 ] );
			}
			$upo_xml = (string) ( $check['upo_xml'] ?? '' );
			if ( '' !== trim( $upo_xml ) ) {
				$wpdb->update( $wpdb->prefix . 'gksef_outgoing_invoices', [ 'upo_xml' => $upo_xml ], [ 'id' => $id ] );
			}
		}
		if ( '' === trim( $upo_xml ) ) {
			wp_die( 'UPO jeszcze niegotowe — spróbuj za chwilę.', '', [ 'response' => 425 ] );
		}
		$num = $inv['invoice_number'] ?: ( '#' . $id );
		$slug = preg_replace( '/[^A-Za-z0-9_\-]+/', '_', (string) $num );
		nocache_headers();
		if ( 'xml' === $format ) {
			header( 'Content-Type: application/xml; charset=utf-8' );
			header( 'Content-Disposition: attachment; filename="UPO_' . $slug . '.xml"' );
			echo $upo_xml; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- strumien XML UPO do pobrania (Content-Type: application/xml), nie HTML.
			exit;
		}
		// PDF z UPO XML — minimalna prezentacja w HTML print-friendly (Dompdf gdy dostępne)
		require_once KSEF_WORDPRESS_PATH . 'includes/class-invoice-pdf.php';
		Invoice_PDF::stream_upo( $id, $upo_xml );
	}

	/**
	 * AJAX: ręczna synchronizacja faktur zakupowych z KSeF (przycisk "Pobierz z KSeF").
	 */
	/**
	 * AJAX: upload załącznika do faktury (sprzedaży lub zakupu).
	 * POST: invoice_type ('incoming'|'outgoing'), invoice_id, label (opt.) + plik w $_FILES['file'].
	 */
	public function ajax_attachment_upload() {
		check_ajax_referer( 'ksef-nonce' );
		$type       = sanitize_text_field( wp_unslash( $_POST['invoice_type'] ?? '' ) );
		$invoice_id = intval( wp_unslash( $_POST['invoice_id'] ?? 0 ) );
		$label      = sanitize_text_field( wp_unslash( $_POST['label'] ?? '' ) );
		if ( ! in_array( $type, [ 'incoming', 'outgoing' ], true ) || $invoice_id <= 0 ) {
			wp_send_json_error( [ 'message' => 'Brak invoice_id lub niewłaściwy invoice_type.' ], 400 );
		}
		// Per-type capability check
		$cap_view = 'incoming' === $type ? 'ksef_view_incoming' : 'ksef_view_outgoing';
		if ( ! current_user_can( $cap_view ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień do tego typu faktur.' ], 403 );
		}
		if ( empty( $_FILES['file']['tmp_name'] ) ) {
			wp_send_json_error( [ 'message' => 'Brak pliku.' ], 400 );
		}
		$original = isset( $_FILES['file']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['file']['name'] ) ) : 'attachment';
		$stored = \KSEF\WordPress\Secure_Storage::store_upload(
			sanitize_text_field( wp_unslash( $_FILES['file']['tmp_name'] ) ),
			$type,
			$invoice_id,
			$original
		);
		if ( is_wp_error( $stored ) ) {
			wp_send_json_error( [ 'message' => $stored->get_error_message() ], 400 );
		}
		global $wpdb;
		$wpdb->insert( $wpdb->prefix . 'gksef_invoice_attachments', [
			'invoice_type'  => $type,
			'invoice_id'    => $invoice_id,
			'file_path'     => $stored['path'],
			'original_name' => $original,
			'mime_type'     => $stored['mime'],
			'size_bytes'    => $stored['size'],
			'label'         => $label,
			'uploaded_by'   => get_current_user_id(),
		] );
		$id = (int) $wpdb->insert_id;
		wp_send_json_success( [
			'id'           => $id,
			'name'         => $original,
			'label'        => $label,
			'mime'         => $stored['mime'],
			'size'         => $stored['size'],
			'download_url' => self::attachment_download_url( $id ),
		] );
	}

	public function ajax_attachment_delete() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_incoming' ) && ! current_user_can( 'ksef_view_outgoing' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$id = intval( wp_unslash( $_POST['id'] ?? 0 ) );
		if ( $id <= 0 ) {
			wp_send_json_error( [ 'message' => 'Brak id.' ], 400 );
		}
		global $wpdb;
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}gksef_invoice_attachments WHERE id = %d", $id ), ARRAY_A );
		if ( ! $row ) {
			wp_send_json_error( [ 'message' => 'Nie znaleziono.' ], 404 );
		}
		// Per-type cap
		$cap = ( 'incoming' === $row['invoice_type'] ) ? 'ksef_view_incoming' : 'ksef_view_outgoing';
		if ( ! current_user_can( $cap ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień do tego załącznika.' ], 403 );
		}
		\KSEF\WordPress\Secure_Storage::delete( (string) $row['file_path'] );
		$wpdb->delete( $wpdb->prefix . 'gksef_invoice_attachments', [ 'id' => $id ] );
		wp_send_json_success();
	}

	public function ajax_attachment_list() {
		check_ajax_referer( 'ksef-nonce' );
		$type       = sanitize_text_field( wp_unslash( $_REQUEST['invoice_type'] ?? '' ) );
		$invoice_id = intval( wp_unslash( $_REQUEST['invoice_id'] ?? 0 ) );
		$cap = ( 'incoming' === $type ) ? 'ksef_view_incoming' : 'ksef_view_outgoing';
		if ( ! current_user_can( $cap ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		global $wpdb;
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}gksef_invoice_attachments WHERE invoice_type = %s AND invoice_id = %d ORDER BY created_at DESC",
				$type, $invoice_id
			),
			ARRAY_A
		);
		$out = [];
		foreach ( (array) $rows as $r ) {
			$out[] = [
				'id'           => (int) $r['id'],
				'name'         => (string) $r['original_name'],
				'label'        => (string) ( $r['label'] ?? '' ),
				'mime'         => (string) ( $r['mime_type'] ?? '' ),
				'size'         => (int) ( $r['size_bytes'] ?? 0 ),
				'date'         => (string) $r['created_at'],
				'download_url' => self::attachment_download_url( (int) $r['id'] ),
			];
		}
		wp_send_json_success( [ 'items' => $out ] );
	}

	/**
	 * AJAX: pobierz załącznik. Wymaga zalogowanego usera z odpowiednim cap.
	 * Plik nie jest dostępny po bezpośrednim URL — tylko przez ten endpoint.
	 */
	public function ajax_attachment_download() {
		// Cookie auth — nonce w query string. Bez nonce'a: 403.
		check_ajax_referer( 'ksef-nonce' );
		$id = intval( wp_unslash( $_REQUEST['id'] ?? 0 ) );
		if ( $id <= 0 ) {
			status_header( 400 );
			exit;
		}
		global $wpdb;
		$row = $wpdb->get_row(
			$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}gksef_invoice_attachments WHERE id = %d", $id ),
			ARRAY_A
		);
		if ( ! $row ) {
			status_header( 404 );
			exit;
		}
		$cap = ( 'incoming' === $row['invoice_type'] ) ? 'ksef_view_incoming' : 'ksef_view_outgoing';
		if ( ! current_user_can( $cap ) ) {
			status_header( 403 );
			exit;
		}
		\KSEF\WordPress\Secure_Storage::stream(
			(string) $row['file_path'],
			(string) $row['original_name'],
			(string) ( $row['mime_type'] ?? 'application/octet-stream' )
		);
	}

	private static function attachment_download_url( int $id ): string {
		return add_query_arg(
			[
				'action'      => 'ksef_attachment_download',
				'id'          => $id,
				'_ajax_nonce' => wp_create_nonce( 'ksef-nonce' ),
			],
			admin_url( 'admin-ajax.php' )
		);
	}

	/**
	 * AJAX: dodaj płatność do faktury zakupowej (incoming).
	 */
	public function ajax_incoming_add_payment() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_incoming' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$invoice_id = intval( wp_unslash( $_POST['invoice_id'] ?? 0 ) );
		$amount     = floatval( wp_unslash( $_POST['amount'] ?? 0 ) );
		if ( $invoice_id <= 0 || $amount <= 0 ) {
			wp_send_json_error( [ 'message' => 'Brak invoice_id lub niepoprawna kwota.' ], 400 );
		}
		global $wpdb;
		$wpdb->insert( $wpdb->prefix . 'gksef_incoming_payments', [
			'incoming_invoice_id' => $invoice_id,
			'amount'              => $amount,
			'currency'            => sanitize_text_field( wp_unslash( $_POST['currency'] ?? 'PLN' ) ),
			'payment_date'        => sanitize_text_field( wp_unslash( $_POST['payment_date'] ?? gmdate( 'Y-m-d' ) ) ),
			'payment_method'      => sanitize_text_field( wp_unslash( $_POST['payment_method'] ?? '' ) ),
			'reference'           => sanitize_text_field( wp_unslash( $_POST['reference'] ?? '' ) ),
			'note'                => sanitize_textarea_field( wp_unslash( $_POST['note'] ?? '' ) ),
			'created_by'          => get_current_user_id(),
		] );
		$payment_id = (int) $wpdb->insert_id;
		// Przelicz sumę zapłaconą + ustaw payment_status
		$this->incoming_recompute_payment_status( $invoice_id );
		wp_send_json_success( [ 'payment_id' => $payment_id, 'summary' => $this->incoming_payment_summary( $invoice_id ) ] );
	}

	public function ajax_incoming_delete_payment() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_incoming' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$payment_id = intval( wp_unslash( $_POST['payment_id'] ?? 0 ) );
		$invoice_id = intval( wp_unslash( $_POST['invoice_id'] ?? 0 ) );
		global $wpdb;
		$wpdb->delete( $wpdb->prefix . 'gksef_incoming_payments', [ 'id' => $payment_id ] );
		$this->incoming_recompute_payment_status( $invoice_id );
		wp_send_json_success( [ 'summary' => $this->incoming_payment_summary( $invoice_id ) ] );
	}

	public function ajax_incoming_list_payments() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'ksef_view_incoming' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$invoice_id = intval( wp_unslash( $_REQUEST['invoice_id'] ?? 0 ) );
		global $wpdb;
		$rows = $wpdb->get_results(
			$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}gksef_incoming_payments WHERE incoming_invoice_id = %d ORDER BY payment_date DESC, id DESC", $invoice_id ),
			ARRAY_A
		);
		wp_send_json_success( [
			'payments' => is_array( $rows ) ? $rows : [],
			'summary'  => $this->incoming_payment_summary( $invoice_id ),
		] );
	}

	private function incoming_payment_summary( int $invoice_id ): array {
		global $wpdb;
		$gross = (float) $wpdb->get_var( $wpdb->prepare( "SELECT gross_amount FROM {$wpdb->prefix}gksef_incoming_invoices WHERE id = %d", $invoice_id ) );
		$paid  = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount), 0) FROM {$wpdb->prefix}gksef_incoming_payments WHERE incoming_invoice_id = %d", $invoice_id ) );
		return [
			'gross'     => $gross,
			'paid'      => $paid,
			'remaining' => max( 0, $gross - $paid ),
			'status'    => $paid >= $gross && $gross > 0 ? 'paid' : ( $paid > 0 ? 'partial' : 'unpaid' ),
		];
	}

	private function incoming_recompute_payment_status( int $invoice_id ): void {
		global $wpdb;
		$summary = $this->incoming_payment_summary( $invoice_id );
		$update = [ 'payment_status' => $summary['status'] ];
		if ( 'paid' === $summary['status'] ) {
			$last_date = (string) $wpdb->get_var( $wpdb->prepare( "SELECT MAX(payment_date) FROM {$wpdb->prefix}gksef_incoming_payments WHERE incoming_invoice_id = %d", $invoice_id ) );
			if ( $last_date ) {
				$update['paid_date'] = $last_date;
			}
		}
		$wpdb->update( $wpdb->prefix . 'gksef_incoming_invoices', $update, [ 'id' => $invoice_id ] );
	}

	public function ajax_save_numbering_schemes() {
		check_ajax_referer( 'ksef-nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => 'Brak uprawnień.' ], 403 );
		}
		$raw = wp_unslash( $_POST['schemes'] ?? '[]' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- surowy JSON, walidowany przez json_decode i sanityzacja pol nizej.
		$schemes = json_decode( (string) $raw, true );
		if ( ! is_array( $schemes ) ) {
			wp_send_json_error( [ 'message' => 'Nieprawidłowe dane.' ], 400 );
		}
		\KSEF\WordPress\V2\Invoice_Numbering::save_schemes( $schemes );
		// Ustaw „kolejny numer" tylko dla schematów, w których użytkownik go zmienił (_next_dirty),
		// żeby zwykły zapis NIE resetował licznika do starej (wyświetlanej) wartości.
		$saved = \KSEF\WordPress\V2\Invoice_Numbering::get_schemes();
		foreach ( $schemes as $s ) {
			if ( empty( $s['_next_dirty'] ) || ! isset( $s['id'] ) ) {
				continue;
			}
			foreach ( $saved as $ss ) {
				if ( (string) $ss['id'] === (string) $s['id'] ) {
					\KSEF\WordPress\V2\Invoice_Numbering::set_next( $ss, (int) ( $s['_next'] ?? 1 ) );
					break;
				}
			}
		}
		$out = array_map( static function ( $ss ) {
			$ss['_next'] = \KSEF\WordPress\V2\Invoice_Numbering::current_next( $ss );
			return $ss;
		}, $saved );
		wp_send_json_success( [ 'schemes' => $out ] );
	}
}

// Rejestruj AJAX handlers
add_action( 'wp_ajax_ksef_get_invoice_detail', [ Plugin::instance(), 'ajax_get_invoice_detail' ] );
add_action( 'wp_ajax_ksef_retry_offline_invoice', [ Plugin::instance(), 'ajax_retry_offline_invoice' ] );
add_action( 'wp_ajax_ksef_send_test_email', [ Plugin::instance(), 'ajax_send_test_email' ] );
add_action( 'wp_ajax_ksef_export_invoices', [ Plugin::instance(), 'ajax_export_invoices' ] );
add_action( 'wp_ajax_ksef_add_payment', [ Plugin::instance(), 'ajax_add_payment' ] );
add_action( 'wp_ajax_ksef_delete_payment', [ Plugin::instance(), 'ajax_delete_payment' ] );
add_action( 'wp_ajax_ksef_set_payment_status', [ Plugin::instance(), 'ajax_set_payment_status' ] );
add_action( 'wp_ajax_ksef_invoice_pdf', [ Plugin::instance(), 'ajax_invoice_pdf' ] );
add_action( 'wp_ajax_ksef_invoice_email', [ Plugin::instance(), 'ajax_invoice_email' ] );
add_action( 'wp_ajax_ksef_invoice_email_preview', [ Plugin::instance(), 'ajax_invoice_email_preview' ] );
add_action( 'wp_ajax_ksef_search_contractors', [ Plugin::instance(), 'ajax_search_contractors' ] );
add_action( 'wp_ajax_ksef_save_contractor', [ Plugin::instance(), 'ajax_save_contractor' ] );
add_action( 'wp_ajax_ksef_search_wc_products', [ Plugin::instance(), 'ajax_search_wc_products' ] );
add_action( 'wp_ajax_ksef_fetch_nbp_rate', [ Plugin::instance(), 'ajax_fetch_nbp_rate' ] );
add_action( 'wp_ajax_ksef_list_bank_accounts', [ Plugin::instance(), 'ajax_list_bank_accounts' ] );
add_action( 'wp_ajax_ksef_save_bank_account', [ Plugin::instance(), 'ajax_save_bank_account' ] );
add_action( 'wp_ajax_ksef_delete_bank_account', [ Plugin::instance(), 'ajax_delete_bank_account' ] );
add_action( 'wp_ajax_ksef_list_extra_presets', [ Plugin::instance(), 'ajax_list_extra_presets' ] );
add_action( 'wp_ajax_ksef_save_extra_preset', [ Plugin::instance(), 'ajax_save_extra_preset' ] );
add_action( 'wp_ajax_ksef_delete_extra_preset', [ Plugin::instance(), 'ajax_delete_extra_preset' ] );
add_action( 'wp_ajax_ksef_attachment_upload',   [ Plugin::instance(), 'ajax_attachment_upload' ] );
add_action( 'wp_ajax_ksef_attachment_delete',   [ Plugin::instance(), 'ajax_attachment_delete' ] );
add_action( 'wp_ajax_ksef_attachment_list',     [ Plugin::instance(), 'ajax_attachment_list' ] );
add_action( 'wp_ajax_ksef_attachment_download', [ Plugin::instance(), 'ajax_attachment_download' ] );
add_action( 'wp_ajax_ksef_save_numbering_schemes', [ Plugin::instance(), 'ajax_save_numbering_schemes' ] );
add_action( 'wp_ajax_ksef_delete_invoice', [ Plugin::instance(), 'ajax_delete_invoice' ] );
add_action( 'wp_ajax_ksef_bulk_delete_invoices', [ Plugin::instance(), 'ajax_bulk_delete_invoices' ] );
add_action( 'wp_ajax_ksef_download_upo', [ Plugin::instance(), 'ajax_download_upo' ] );

// Hooks dla notyfikacji i auto-link
add_action( 'ksef_sync_error', function( $sync_log_id, $error_message ) {
	Email_Notifier::send_sync_error( $sync_log_id, $error_message );
}, 10, 2 );

add_action( 'ksef_invoice_linked', function( $invoice_id, $order_id ) {
	// Optional: Send notification or log
	ksef_efaktury_log( "[KSEF] Invoice $invoice_id linked to order $order_id" );
}, 10, 2 );
