<?php
/**
 * Polling statusu sesji + pobieranie UPO z KSeF v2.
 *
 * Endpointy:
 *   GET /sessions/{ref}                         → { status: {code, description}, invoiceCount, successfulInvoiceCount, failedInvoiceCount, upo: { pages: [{downloadUrl}] } }
 *   GET /sessions/{ref}/invoices                → lista faktur z sesji
 *   GET /sessions/{ref}/invoices/{invRef}/upo   → XML UPO (Content-Type: application/xml)
 *
 * Cron: `ksef_v2_poll_status_cron` — co 15 min sprawdza sesje w statusie 'sent' / 'InProgress'.
 *
 * @package KSEF\WordPress\V2
 */

declare(strict_types=1);

namespace KSEF\WordPress\V2;

// 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).

use WP_Error;

class Status_Poller {

	public function __construct( private Ksef_Client $client, private Auth $auth, private Upo_Parser $upo_parser ) {}

	/**
	 * Sprawdź status sesji + per-faktura, pobierz UPO gdy zaakceptowana, wykryj odrzucenie.
	 *
	 * Zwraca:
	 *  - ['status'=>'pending']                                            — MF jeszcze przetwarza
	 *  - ['status'=>'confirmed','ksef_number'=>..,'upo_xml'=>..]          — zaakceptowana + UPO
	 *  - ['status'=>'rejected','code'=>..,'reason'=>..,'details'=>[..]]   — MF odrzucił (np. duplikat)
	 *  - WP_Error                                                         — błąd transportu/auth (przejściowy)
	 *
	 * @param array<string,mixed> $credential
	 * @param string              $our_invoice_ref  referenceNumber naszej faktury (do dopasowania w sesji)
	 * @return array<string,mixed>|WP_Error
	 */
	public function check_session( string $session_ref, array $credential, string $our_invoice_ref = '' ): array|WP_Error {
		$token = $this->auth->get_access_token( $credential );
		if ( is_wp_error( $token ) ) {
			return $token;
		}
		$this->client->with_bearer( $token );

		// 1. Status sesji (czy MF skończył przetwarzać).
		$session = $this->client->get( "/sessions/{$session_ref}" );
		if ( is_wp_error( $session ) ) {
			return $session;
		}
		$session_code = (int) ( $session['status']['code'] ?? 0 );

		// 2. Per-faktura — autorytatywny status (200=Sukces, 4xx=odrzucona).
		$invoices = $this->client->get( "/sessions/{$session_ref}/invoices" );
		if ( is_wp_error( $invoices ) ) {
			// Gdy sesja jeszcze biegnie, lista bywa niedostępna — to nie błąd trwały.
			if ( 100 === $session_code || 0 === $session_code ) {
				return [ 'status' => 'pending' ];
			}
			return $invoices;
		}
		$invoice_list = $invoices['invoices'] ?? $invoices['items'] ?? [];

		// Dopasuj naszą fakturę po referenceNumber (w naszym flow zwykle 1 w sesji).
		$item = null;
		if ( is_array( $invoice_list ) && ! empty( $invoice_list ) ) {
			if ( '' !== $our_invoice_ref ) {
				foreach ( $invoice_list as $cand ) {
					if ( (string) ( $cand['referenceNumber'] ?? '' ) === $our_invoice_ref ) {
						$item = $cand;
						break;
					}
				}
			}
			if ( null === $item ) {
				$item = $invoice_list[0];
			}
		}

		// Brak jeszcze pozycji → sesja w toku.
		if ( null === $item ) {
			return [ 'status' => 'pending' ];
		}

		$inv_code    = (int) ( $item['status']['code'] ?? 0 );
		$ksef_number = (string) ( $item['ksefNumber'] ?? $item['ksefReferenceNumber'] ?? '' );

		// 3a. ODRZUCONA — kod >= 400 (np. 440 duplikat, 21xxx walidacja).
		if ( $inv_code >= 400 ) {
			$details = (array) ( $item['status']['details'] ?? [] );
			$reason  = (string) ( $item['status']['description'] ?? 'Faktura odrzucona przez KSeF' );
			return [
				'status'  => 'rejected',
				'code'    => $inv_code,
				'reason'  => $reason,
				'details' => $details,
			];
		}

		// 3b. ZAAKCEPTOWANA — kod 200 (lub mamy już numer KSeF + UPO url).
		$upo_url = (string) ( $item['upoDownloadUrl'] ?? '' );
		$accepted = ( 200 === $inv_code ) || ( '' !== $ksef_number && '' !== $upo_url );
		if ( ! $accepted ) {
			// Jeszcze bez finalnego statusu → czekamy.
			return [ 'status' => 'pending' ];
		}

		// Pobierz UPO: preferuj upoDownloadUrl (storage), fallback do endpointu /upo.
		$upo_xml = '';
		if ( '' !== $upo_url ) {
			$bin = $this->client->get_binary( $upo_url );
			if ( ! is_wp_error( $bin ) ) {
				$upo_xml = (string) $bin;
			}
		}
		if ( '' === $upo_xml ) {
			$inv_ref = (string) ( $item['referenceNumber'] ?? $our_invoice_ref );
			if ( '' !== $inv_ref ) {
				$upo_response = $this->client->get( "/sessions/{$session_ref}/invoices/{$inv_ref}/upo" );
				if ( ! is_wp_error( $upo_response ) ) {
					$upo_xml = (string) ( $upo_response['_raw'] ?? '' );
				}
			}
		}

		// Numer KSeF z UPO, jeśli nie było w liście.
		if ( '' === $ksef_number && '' !== $upo_xml ) {
			$parsed = $this->upo_parser->parse( $upo_xml );
			if ( ! is_wp_error( $parsed ) ) {
				$ksef_number = (string) ( $parsed['ksef_number'] ?? '' );
			}
		}

		// Zaakceptowana, ale UPO jeszcze nie wygenerowane → potwierdź z numerem, UPO dociągnie kolejny poll.
		return [
			'status'      => 'confirmed',
			'ksef_number' => $ksef_number ?: null,
			'upo_xml'     => $upo_xml ?: null,
		];
	}

	/**
	 * Cron: odpytaj wszystkie faktury w stanie sent/pending i sfinalizuj
	 * (confirmed + UPO / rejected + powód). Wywoływany też natychmiast po wysyłce.
	 */
	public function cron_poll(): void {
		global $wpdb;
		$rows = $wpdb->get_results(
			"SELECT * FROM {$wpdb->prefix}gksef_outgoing_invoices
			 WHERE status IN ('sent','pending') AND ksef_session_id != ''
			 ORDER BY id DESC LIMIT 50",
			ARRAY_A
		);
		foreach ( $rows as $row ) {
			$credential = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT * FROM {$wpdb->prefix}gksef_credentials WHERE id = %d",
					(int) $row['credential_id']
				),
				ARRAY_A
			);
			if ( ! $credential ) {
				continue;
			}
			$this->finalize_invoice( $row, $credential );
		}
	}

	/**
	 * Sprawdź jedną fakturę i zaktualizuj jej stan w DB (confirmed/rejected/pending).
	 * Zwraca finalny stan ('confirmed'|'rejected'|'pending'|'error').
	 *
	 * @param array<string,mixed> $row        wiersz gksef_outgoing_invoices
	 * @param array<string,mixed> $credential
	 */
	public function finalize_invoice( array $row, array $credential ): string {
		global $wpdb;
		$table = $wpdb->prefix . 'gksef_outgoing_invoices';
		$id    = (int) $row['id'];

		$result = $this->check_session( (string) $row['ksef_session_id'], $credential, (string) ( $row['upo_reference'] ?? '' ) );

		if ( is_wp_error( $result ) ) {
			$wpdb->update( $table, [
				'status'            => 'pending',
				'offline_reason'    => substr( $result->get_error_message(), 0, 500 ),
				'last_status_check' => current_time( 'mysql', true ),
			], [ 'id' => $id ] );
			return 'error';
		}

		$state = (string) ( $result['status'] ?? 'pending' );

		if ( 'rejected' === $state ) {
			$reason = trim( (string) ( $result['reason'] ?? 'Odrzucona' )
				. ( ! empty( $result['details'] ) ? ' — ' . implode( '; ', (array) $result['details'] ) : '' ) );
			$wpdb->update( $table, [
				'status'            => 'rejected',
				'offline_reason'    => substr( '[' . (int) ( $result['code'] ?? 0 ) . '] ' . $reason, 0, 500 ),
				'last_status_check' => current_time( 'mysql', true ),
			], [ 'id' => $id ] );

			$order = wc_get_order( (int) $row['wc_order_id'] );
			if ( $order ) {
				$order->update_meta_data( '_ksef_invoice_status', 'rejected' );
				$order->add_order_note( 'KSeF: faktura ODRZUCONA — ' . $reason );
				$order->save();
			}
			return 'rejected';
		}

		if ( 'confirmed' === $state && ! empty( $result['ksef_number'] ) ) {
			$update = [
				'status'                => 'confirmed',
				'ksef_reference_number' => (string) $result['ksef_number'],
				'upo_received_at'       => current_time( 'mysql', true ),
				'last_status_check'     => current_time( 'mysql', true ),
				'offline_reason'        => null,
			];
			if ( ! empty( $result['upo_xml'] ) ) {
				$update['upo_xml'] = \KSEF\WordPress\Crypto::encrypt_for_storage( (string) $result['upo_xml'] );
			}
			$wpdb->update( $table, $update, [ 'id' => $id ] );

			$order = wc_get_order( (int) $row['wc_order_id'] );
			if ( $order ) {
				$order->update_meta_data( '_ksef_invoice_status', 'confirmed' );
				$order->update_meta_data( '_ksef_reference_number', (string) $result['ksef_number'] );
				$order->add_order_note( sprintf( 'KSeF: faktura potwierdzona, numer KSeF: %s', (string) $result['ksef_number'] ) );
				$order->save();
			}
			return 'confirmed';
		}

		// Nadal w toku.
		$wpdb->update( $table, [
			'status'            => 'pending',
			'last_status_check' => current_time( 'mysql', true ),
		], [ 'id' => $id ] );
		return 'pending';
	}
}
