<?php
/**
 * Pobieranie i caching publicznych kluczy MF.
 *
 * Endpoint: GET /api/v2/security/public-key-certificates
 * Response: tablica JSON obiektów [{ certificate, certificateId, publicKeyId, validFrom, validTo, usage:[] }]
 *
 * Cache: 24h w transient.
 *
 * @package KSEF\WordPress\V2
 */

declare(strict_types=1);

namespace KSEF\WordPress\V2;

use WP_Error;

class Public_Keys {

	private const TRANSIENT_PREFIX = 'ksef_v2_pubkeys_';
	private const TTL              = DAY_IN_SECONDS;

	public function __construct( private Ksef_Client $client, private string $environment = 'test' ) {}

	/**
	 * Zwraca aktualny klucz publiczny MF dla danego usage.
	 *
	 * Filtruje po: usage zawiera target, validFrom <= now <= validTo.
	 * Gdy kilka pasuje — preferuje najnowszy validFrom.
	 *
	 * @param string $usage 'KsefTokenEncryption' | 'SymmetricKeyEncryption'
	 * @return array{publicKeyId:string,certificatePem:string,certificateId:string,validTo:string}|WP_Error
	 */
	public function get_active_key( string $usage ): array|WP_Error {
		$keys = $this->get_all_keys();
		if ( is_wp_error( $keys ) ) {
			return $keys;
		}

		$now    = time();
		$best   = null;
		$best_t = 0;
		foreach ( $keys as $key ) {
			$usages = (array) ( $key['usage'] ?? [] );
			if ( ! in_array( $usage, $usages, true ) ) {
				continue;
			}
			$valid_from = isset( $key['validFrom'] ) ? (int) strtotime( (string) $key['validFrom'] ) : 0;
			$valid_to   = isset( $key['validTo'] )   ? (int) strtotime( (string) $key['validTo'] )   : PHP_INT_MAX;
			if ( $valid_from > $now || $valid_to < $now ) {
				continue;
			}
			if ( $valid_from > $best_t ) {
				$best   = $key;
				$best_t = $valid_from;
			}
		}

		if ( $best === null ) {
			return new WP_Error( 'ksef_no_active_key', sprintf( 'Brak ważnego klucza publicznego MF dla usage: %s', $usage ) );
		}

		return [
			'publicKeyId'    => (string) $best['publicKeyId'],
			'certificatePem' => self::der_b64_to_pem( (string) $best['certificate'] ),
			'certificateId'  => (string) ( $best['certificateId'] ?? '' ),
			'validTo'        => (string) ( $best['validTo'] ?? '' ),
		];
	}

	/**
	 * Forsuj odświeżenie cache (np. po błędzie 21470 z API).
	 */
	public function refresh(): void {
		delete_transient( self::TRANSIENT_PREFIX . $this->environment );
	}

	/**
	 * Top-level response API to tablica obiektów (NIE {keys:[]}).
	 * Fallback do bundled keys gdy network/error.
	 *
	 * @return array<int,array<string,mixed>>|WP_Error
	 */
	private function get_all_keys(): array|WP_Error {
		$cache_key = self::TRANSIENT_PREFIX . $this->environment;
		$cached    = get_transient( $cache_key );
		if ( is_array( $cached ) && ! empty( $cached ) ) {
			return $cached;
		}

		$response = $this->client->get( '/security/public-key-certificates' );
		if ( is_wp_error( $response ) ) {
			$fallback = $this->load_bundled_keys();
			return $fallback !== [] ? $fallback : $response;
		}

		// API zwraca array bezpośrednio. wp_remote_request + json_decode robi z tego list array.
		// Ksef_Client::request gdy widzi że response NIE jest assoc, woła `is_array($data) ? $data : ['_raw' => $body]`.
		// Sprawdzamy obie ścieżki dla bezpieczeństwa.
		$keys = $response;
		if ( isset( $response['_raw'] ) ) {
			$decoded = json_decode( (string) $response['_raw'], true );
			if ( is_array( $decoded ) ) {
				$keys = $decoded;
			}
		}
		// Niektóre dokumentacje sugerują że może być {keys:[]} albo {publicKeys:[]} — fallback
		if ( isset( $keys['keys'] ) && is_array( $keys['keys'] ) ) {
			$keys = $keys['keys'];
		} elseif ( isset( $keys['publicKeys'] ) && is_array( $keys['publicKeys'] ) ) {
			$keys = $keys['publicKeys'];
		}

		if ( ! is_array( $keys ) || ! isset( $keys[0] ) ) {
			$fallback = $this->load_bundled_keys();
			if ( $fallback !== [] ) {
				return $fallback;
			}
			return new WP_Error( 'ksef_keys_malformed', 'Niepoprawny format /security/public-key-certificates' );
		}

		set_transient( $cache_key, $keys, self::TTL );
		return $keys;
	}

	/**
	 * Konwertuje base64-DER (jak zwraca API) na PEM.
	 */
	private static function der_b64_to_pem( string $b64_der ): string {
		$b64_der = (string) preg_replace( '/\s+/', '', $b64_der );
		if ( str_starts_with( $b64_der, '-----BEGIN' ) ) {
			return $b64_der;
		}
		$wrapped = chunk_split( $b64_der, 64, "\n" );
		return "-----BEGIN CERTIFICATE-----\n" . $wrapped . "-----END CERTIFICATE-----\n";
	}

	/**
	 * Fallback z bundled keys (`includes/v2/keys/public-keys-{env}.json`).
	 *
	 * @return array<int,array<string,mixed>>
	 */
	private function load_bundled_keys(): array {
		$env  = $this->environment === 'prod' ? 'prod' : 'test';
		$path = dirname( __DIR__ ) . '/../includes/v2/keys/public-keys-' . $env . '.json';
		$path = __DIR__ . '/keys/public-keys-' . $env . '.json'; // includes/v2/keys/
		if ( ! is_file( $path ) ) {
			return [];
		}
		$decoded = json_decode( (string) file_get_contents( $path ), true );
		if ( is_array( $decoded ) && isset( $decoded[0] ) ) {
			return $decoded;
		}
		if ( is_array( $decoded ) && isset( $decoded['keys'] ) ) {
			return (array) $decoded['keys'];
		}
		return [];
	}
}
