<?php
/**
 * KSeF API v2 HTTP Client.
 *
 * Zgodny ze spec MF v2.5.0 z https://api.ksef.mf.gov.pl/docs/v2/openapi.json.
 *
 * @package KSEF\WordPress\V2
 */

declare(strict_types=1);

namespace KSEF\WordPress\V2;

use WP_Error;

class Ksef_Client {

	private const BASE_URLS = [
		'test' => 'https://api-test.ksef.mf.gov.pl',
		'demo' => 'https://api-demo.ksef.mf.gov.pl',
		'prod' => 'https://api.ksef.mf.gov.pl',
	];

	private const VERSION = '/api/v2';

	private string $base_url;
	private ?string $bearer_token = null;
	private int $timeout = 30;
	private bool $retry_enabled = true;

	public function __construct( string $environment = 'test' ) {
		$this->base_url = self::BASE_URLS[ $environment ] ?? self::BASE_URLS['test'];
	}

	public function with_bearer( string $token ): self {
		$this->bearer_token = $token;
		return $this;
	}

	public function with_timeout( int $seconds ): self {
		$this->timeout = $seconds;
		return $this;
	}

	/**
	 * Włącz/wyłącz ponawianie GET na błędach przejściowych.
	 * Wyłączamy w synchronicznym potwierdzaniu po wysyłce (szybki fallback do
	 * „oczekuje”; cron dobija z retry w tle).
	 */
	public function with_retry( bool $enabled ): self {
		$this->retry_enabled = $enabled;
		return $this;
	}

	/**
	 * @return array<string,mixed>|WP_Error
	 */
	public function get( string $path, array $query = [], array $headers = [] ) {
		return $this->request( 'GET', $path, null, $query, $headers );
	}

	/**
	 * @param array<string,mixed>|null $body JSON-encodable body, null = empty.
	 * @return array<string,mixed>|WP_Error
	 */
	public function post( string $path, ?array $body = null, array $headers = [] ) {
		return $this->request( 'POST', $path, $body, [], $headers );
	}

	/**
	 * POST z raw body (bez JSON encode). Używane do XAdES (XML signed).
	 *
	 * @return array<string,mixed>|WP_Error
	 */
	public function post_raw( string $path, string $raw_body, array $headers = [] ) {
		$url = $this->base_url . self::VERSION . $path;
		$req_headers = array_merge(
			[
				'Accept'       => 'application/json',
				'Content-Type' => 'application/octet-stream',
				'User-Agent'   => 'KSEF-WordPress/' . ( defined( 'KSEF_WORDPRESS_VERSION' ) ? KSEF_WORDPRESS_VERSION : 'dev' ),
			],
			$headers
		);
		if ( $this->bearer_token !== null && ! isset( $req_headers['Authorization'] ) ) {
			$req_headers['Authorization'] = 'Bearer ' . $this->bearer_token;
		}

		$response = wp_remote_request( $url, [
			'method'  => 'POST',
			'timeout' => $this->timeout,
			'headers' => $req_headers,
			'body'    => $raw_body,
		] );
		if ( is_wp_error( $response ) ) {
			return $response;
		}
		$status_code   = (int) wp_remote_retrieve_response_code( $response );
		$response_body = (string) wp_remote_retrieve_body( $response );
		$data          = json_decode( $response_body, true );

		if ( $status_code >= 400 ) {
			$msg = is_array( $data ) ? (string) ( $data['exception']['exceptionDetailList'][0]['exceptionDescription'] ?? $data['detail'] ?? $data['message'] ?? 'KSeF API error' ) : "HTTP {$status_code}";
			$code = is_array( $data ) ? (string) ( $data['exception']['exceptionDetailList'][0]['exceptionCode'] ?? $data['code'] ?? "http_{$status_code}" ) : "http_{$status_code}";
			return new WP_Error( 'ksef_api_' . $code, $msg, [ 'status' => $status_code, 'response' => $data, 'url' => $url, 'method' => 'POST' ] );
		}
		return is_array( $data ) ? $data : [ '_raw' => $response_body ];
	}

	/**
	 * @return array<string,mixed>|WP_Error
	 */
	public function delete( string $path, array $headers = [] ) {
		return $this->request( 'DELETE', $path, null, [], $headers );
	}

	/**
	 * Główna metoda HTTP — buduje URL, wysyła, parsuje JSON.
	 *
	 * @param array<string,mixed>|null $body
	 * @param array<string,string|int> $query
	 * @param array<string,string>     $headers
	 * @return array<string,mixed>|WP_Error
	 */
	private function request( string $method, string $path, ?array $body, array $query, array $headers ) {
		$url = $this->base_url . self::VERSION . $path;
		if ( ! empty( $query ) ) {
			$url .= '?' . http_build_query( $query );
		}

		$req_headers = array_merge(
			[
				'Accept'       => 'application/json',
				'Content-Type' => 'application/json',
				'User-Agent'   => 'KSEF-WordPress/' . ( defined( 'KSEF_WORDPRESS_VERSION' ) ? KSEF_WORDPRESS_VERSION : 'dev' ),
			],
			$headers
		);
		if ( $this->bearer_token !== null && ! isset( $req_headers['Authorization'] ) ) {
			$req_headers['Authorization'] = 'Bearer ' . $this->bearer_token;
		}

		$args = [
			'method'  => $method,
			'timeout' => $this->timeout,
			'headers' => $req_headers,
		];
		if ( $body !== null ) {
			$args['body'] = wp_json_encode( $body );
		}

		// Retry TYLKO dla GET (idempotentne): błędy sieci/timeout (cURL 28) i 5xx.
		// POST nigdy nie ponawiamy — ryzyko podwójnego wysłania faktury do KSeF.
		$max_attempts = ( 'GET' === $method && $this->retry_enabled ) ? 3 : 1;
		$response     = null;
		for ( $attempt = 1; $attempt <= $max_attempts; $attempt++ ) {
			$response  = wp_remote_request( $url, $args );
			$transient = is_wp_error( $response );
			if ( ! $transient ) {
				$rc        = (int) wp_remote_retrieve_response_code( $response );
				$transient = ( $rc >= 500 && $rc <= 599 );
			}
			if ( ! $transient || $attempt === $max_attempts ) {
				break;
			}
			usleep( (int) ( 0.4 * $attempt * 1000000 ) ); // 0.4 s, 0.8 s
		}
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$status_code   = (int) wp_remote_retrieve_response_code( $response );
		$response_body = (string) wp_remote_retrieve_body( $response );
		$content_type  = (string) wp_remote_retrieve_header( $response, 'content-type' );

		// Niektóre endpointy mogą zwrócić binary (np. UPO XML, PDF, paczki eksportu)
		if ( $this->is_json_content_type( $content_type ) ) {
			$data = json_decode( $response_body, true );
		} else {
			// Zwracamy raw body w polu specjalnym
			$data = [ '_raw' => $response_body, '_content_type' => $content_type ];
		}

		if ( $status_code >= 400 ) {
			$error_code    = is_array( $data ) ? (string) ( $data['exception']['exceptionDetailList'][0]['exceptionCode'] ?? $data['code'] ?? "http_{$status_code}" ) : "http_{$status_code}";
			$error_message = is_array( $data ) ? (string) ( $data['exception']['exceptionDetailList'][0]['exceptionDescription'] ?? $data['message'] ?? 'KSeF API error' ) : "HTTP {$status_code}";
			// Pełen request+response do debug.log — żeby diagnozować błędy walidacji MF (21405 itp.)
			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
				ksef_efaktury_log( '[KSEF-API ' . $status_code . '] ' . $method . ' ' . $url
					. ' | req_body=' . substr( (string) ( $args['body'] ?? '' ), 0, 1000 )
					. ' | resp=' . substr( (string) $response_body, 0, 2000 ) );
			}
			return new WP_Error(
				'ksef_api_' . $error_code,
				$error_message,
				[
					'status'   => $status_code,
					'response' => $data,
					'url'      => $url,
					'method'   => $method,
				]
			);
		}

		return is_array( $data ) ? $data : [ '_raw' => $response_body ];
	}

	private function is_json_content_type( string $ct ): bool {
		$ct = strtolower( $ct );
		return str_contains( $ct, 'application/json' ) || str_contains( $ct, 'application/problem+json' );
	}

	public function get_base_url(): string {
		return $this->base_url;
	}

	/**
	 * Bezpośrednie pobieranie binary z absolute URL (np. UPO download URLs z sesji).
	 */
	public function get_binary( string $absolute_url, array $headers = [] ): string|WP_Error {
		$req_headers = array_merge(
			[ 'User-Agent' => 'KSEF-WordPress/' . ( defined( 'KSEF_WORDPRESS_VERSION' ) ? KSEF_WORDPRESS_VERSION : 'dev' ) ],
			$headers
		);
		if ( $this->bearer_token !== null && ! isset( $req_headers['Authorization'] ) ) {
			$req_headers['Authorization'] = 'Bearer ' . $this->bearer_token;
		}
		$max  = $this->retry_enabled ? 3 : 1;
		$resp_timeout = min( 60, max( 8, $this->timeout * 2 ) );
		$response = null;
		for ( $attempt = 1; $attempt <= $max; $attempt++ ) {
			$response  = wp_remote_get( $absolute_url, [ 'timeout' => $resp_timeout, 'headers' => $req_headers ] );
			$transient = is_wp_error( $response );
			if ( ! $transient ) {
				$rc        = (int) wp_remote_retrieve_response_code( $response );
				$transient = ( $rc >= 500 && $rc <= 599 );
			}
			if ( ! $transient || $attempt === $max ) {
				break;
			}
			usleep( (int) ( 0.4 * $attempt * 1000000 ) );
		}
		if ( is_wp_error( $response ) ) {
			return $response;
		}
		$status = (int) wp_remote_retrieve_response_code( $response );
		$body   = (string) wp_remote_retrieve_body( $response );
		if ( $status >= 400 ) {
			return new WP_Error( "http_{$status}", "HTTP {$status} fetching {$absolute_url}", [ 'status' => $status ] );
		}
		return $body;
	}

	/**
	 * Mapuje kod/komunikat błędu KSeF na czytelny komunikat PL + podpowiedź naprawy.
	 *
	 * @return array{title:string,hint:string}
	 */
	public static function friendly_error( string $code, string $message ): array {
		$code = (string) $code;
		$msg  = trim( (string) $message );
		$low  = strtolower( $code . ' ' . $msg );
		$num  = '';
		if ( preg_match( '/(\d{3,5})/', $code, $m ) ) {
			$num = $m[1];
		} elseif ( preg_match( '/\b(440|429|21173|21405|21418)\b/', $msg, $m ) ) {
			// Gdy wywołane tylko z treścią błędu (np. z zapisanego offline_reason) — wyłap znany kod KSeF.
			$num = $m[1];
		}

		// Błędy sieci / timeout (cURL 28) — zwykle chwilowe.
		if ( str_contains( $low, 'timed out' ) || str_contains( $low, 'timeout' ) || str_contains( $low, 'cURL error 28' )
			|| str_contains( $low, 'could not resolve' ) || str_contains( $low, 'connection refused' ) || str_contains( $low, 'http_503' ) || str_contains( $low, 'http_502' ) ) {
			return [
				'title' => 'Brak połączenia z KSeF',
				'hint'  => 'Serwer KSeF nie odpowiedział na czas. To zwykle chwilowe — spróbuj wysłać ponownie za moment. Faktura nie została wysłana.',
			];
		}

		$map = [
			'440'   => [ 'Duplikat faktury', 'Ta faktura została już przyjęta przez KSeF. Sprawdź „Faktury sprzedaży” — prawdopodobnie jest już wystawiona. Aby wystawić nową, zmień numer faktury.' ],
			'21173' => [ 'Sesja KSeF wygasła', 'Sesja wysyłki wygasła zanim faktura została przyjęta. Spróbuj wysłać fakturę jeszcze raz.' ],
			'21405' => [ 'Błąd walidacji faktury', 'KSeF odrzucił dane faktury. Sprawdź NIP-y, stawki VAT i pola wymagane. Szczegóły z KSeF: ' . $msg ],
			'21418' => [ 'Błąd sesji', 'Wystąpił problem techniczny sesji. Spróbuj ponownie.' ],
			'429'   => [ 'Za dużo żądań do KSeF', 'Przekroczono limit zapytań MF. Odczekaj minutę i spróbuj ponownie.' ],
		];
		if ( '' !== $num && isset( $map[ $num ] ) ) {
			return [ 'title' => $map[ $num ][0], 'hint' => $map[ $num ][1] ];
		}

		// Autoryzacja / token.
		if ( str_contains( $low, 'auth' ) || str_contains( $low, 'token' ) || str_contains( $low, 'unauthorized' ) || str_contains( $low, 'http_401' ) || str_contains( $low, 'http_403' ) ) {
			return [
				'title' => 'Problem z autoryzacją w KSeF',
				'hint'  => 'Token KSeF jest nieprawidłowy lub wygasł. Wejdź w Ustawienia → Konto KSeF i podłącz token ponownie.',
			];
		}

		// Walidacja danych (inne kody 21xxx).
		if ( '' !== $num && str_starts_with( $num, '21' ) ) {
			return [ 'title' => 'KSeF odrzucił fakturę (walidacja)', 'hint' => $msg ?: 'Sprawdź dane faktury i spróbuj ponownie.' ];
		}

		return [ 'title' => $msg !== '' ? $msg : 'Błąd KSeF', 'hint' => '' ];
	}
}
