<?php
/**
 * Klasa szyfrowania — AES-256-GCM dla tokenów i kluczy
 *
 * @package KSEF\WordPress
 */

namespace KSEF\WordPress;

/**
 * Crypto class — szyfrowanie wrażliwych danych
 */
class Crypto {

	/**
	 * Algorytm szyfrowania
	 */
	const CIPHER = 'aes-256-gcm';

	/**
	 * Długość nonce'a (IV)
	 */
	const NONCE_LENGTH = 12;

	/**
	 * Długość tagu autentykacji
	 */
	const TAG_LENGTH = 16;

	/**
	 * Pobierz klucz szyfrowania
	 *
	 * Używa AUTH_KEY z wp-config.php, jeśli niedostępny — generuje losowy
	 *
	 * @return string
	 */
	public static function get_encryption_key() {
		// Użyj AUTH_KEY z WordPress
		if ( defined( 'AUTH_KEY' ) && AUTH_KEY ) {
			return hash( 'sha256', AUTH_KEY, true );
		}

		// Fallback: sól per-instalacja z wp_options
		$stored_key = get_option( '_gksef_encryption_salt' );
		if ( ! $stored_key ) {
			$stored_key = bin2hex( openssl_random_pseudo_bytes( 32 ) );
			update_option( '_gksef_encryption_salt', $stored_key, false );
		}

		return hash( 'sha256', $stored_key, true );
	}

	/**
	 * Zaszyfruj dane
	 *
	 * @param string $plaintext Dane do szyfrowania.
	 * @return string Base64-encoded: nonce + tag + ciphertext
	 * @throws \Exception
	 */
	public static function encrypt( $plaintext ) {
		if ( ! extension_loaded( 'openssl' ) ) {
			throw new \Exception( 'OpenSSL extension is required for encryption.' );
		}

		$key = self::get_encryption_key();

		// Generuj losowy nonce (IV)
		$nonce = openssl_random_pseudo_bytes( self::NONCE_LENGTH );

		// Zaszyfruj
		$tag = '';
		$ciphertext = openssl_encrypt(
			$plaintext,
			self::CIPHER,
			$key,
			OPENSSL_RAW_DATA,
			$nonce,
			$tag,
			'',
			self::TAG_LENGTH
		);

		if ( false === $ciphertext ) {
			throw new \Exception( esc_html( 'Encryption failed: ' . openssl_error_string() ) );
		}

		// Zwróć: nonce + tag + ciphertext (base64)
		return base64_encode( $nonce . $tag . $ciphertext );
	}

	/**
	 * Rozszyfruj dane
	 *
	 * @param string $encrypted Base64-encoded: nonce + tag + ciphertext.
	 * @return string
	 * @throws \Exception
	 */
	public static function decrypt( $encrypted ) {
		if ( ! extension_loaded( 'openssl' ) ) {
			throw new \Exception( 'OpenSSL extension is required for decryption.' );
		}

		$key = self::get_encryption_key();

		// Dekoduj base64
		$binary = base64_decode( $encrypted, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- dekodowanie zaszyfrowanego blobu (ciphertext), nie obfuskacja kodu.
		if ( false === $binary ) {
			throw new \Exception( 'Invalid encrypted data (base64 decode failed).' );
		}

		// Rozdziel: nonce + tag + ciphertext
		if ( strlen( $binary ) < self::NONCE_LENGTH + self::TAG_LENGTH ) {
			throw new \Exception( 'Encrypted data is too short.' );
		}

		$nonce      = substr( $binary, 0, self::NONCE_LENGTH );
		$tag        = substr( $binary, self::NONCE_LENGTH, self::TAG_LENGTH );
		$ciphertext = substr( $binary, self::NONCE_LENGTH + self::TAG_LENGTH );

		// Rozszyfruj
		$plaintext = openssl_decrypt(
			$ciphertext,
			self::CIPHER,
			$key,
			OPENSSL_RAW_DATA,
			$nonce,
			$tag
		);

		if ( false === $plaintext ) {
			throw new \Exception( 'Decryption failed (invalid tag or corrupted data).' );
		}

		return $plaintext;
	}

	/**
	 * Zaszyfruj token do przechowywania w bazie
	 *
	 * @param string $token Token KSeF.
	 * @return string
	 */
	public static function encrypt_token( $token ) {
		return self::encrypt( $token );
	}

	/**
	 * Rozszyfruj token z bazy
	 *
	 * @param string $encrypted_token Zaszyfrowany token.
	 * @return string
	 */
	public static function decrypt_token( $encrypted_token ) {
		return self::decrypt( $encrypted_token );
	}

	/**
	 * Zaszyfruj klucz prywatny
	 *
	 * @param string $private_key Klucz prywatny (PEM).
	 * @return string
	 */
	public static function encrypt_private_key( $private_key ) {
		return self::encrypt( $private_key );
	}

	/**
	 * Rozszyfruj klucz prywatny
	 *
	 * @param string $encrypted_key Zaszyfrowany klucz.
	 * @return string
	 */
	public static function decrypt_private_key( $encrypted_key ) {
		return self::decrypt( $encrypted_key );
	}

	/**
	 * Sentinel oznaczający zaszyfrowaną zawartość kolumny.
	 * Czytniki używają go do odróżnienia rekordów zaszyfrowanych od starych
	 * (plaintext) — dzięki temu szyfrowanie wraca kompatybilne wstecz.
	 */
	const STORAGE_SENTINEL = 'KSEF_ENC1:';

	/**
	 * Zaszyfruj wartość do zapisu w kolumnie longtext (payload_json,
	 * generated_xml, raw_xml itp.). Pusty/null zostaje bez zmian.
	 */
	public static function encrypt_for_storage( $value ) {
		if ( $value === null || $value === '' ) {
			return $value;
		}
		try {
			return self::STORAGE_SENTINEL . self::encrypt( (string) $value );
		} catch ( \Throwable $e ) {
			// Jeśli szyfrowanie zawiedzie (brak openssl) — zapisz plaintext,
			// żeby nie blokować wystawienia faktury. Lepszy fallback niż utrata danych.
			// Loguj — to stan wymagający uwagi (dane wrażliwe trafiają bez szyfrowania, audyt L-2).
			ksef_efaktury_log( '[KSEF] Crypto::encrypt_for_storage fallback do PLAINTEXT (brak OpenSSL?): ' . $e->getMessage() );
			return $value;
		}
	}

	/**
	 * Odszyfruj wartość zapisaną przez encrypt_for_storage. Jeśli wartość
	 * nie ma sentinela — zwróć ją bez zmian (legacy plaintext).
	 */
	public static function decrypt_from_storage( $value ) {
		if ( $value === null || $value === '' ) {
			return $value;
		}
		$value = (string) $value;
		if ( 0 === strpos( $value, self::STORAGE_SENTINEL ) ) {
			try {
				return self::decrypt( substr( $value, strlen( self::STORAGE_SENTINEL ) ) );
			} catch ( \Throwable $e ) {
				return '';
			}
		}
		return $value;
	}

	// ================================================================
	// KSeF API v2 — kryptografia zgodna ze spec MF
	// (RFC 8017 §7.1 OAEP, FIPS 180-4 SHA-256, NIST SP 800-38A CBC + PKCS#7)
	// ================================================================

	/**
	 * RSA-OAEP-SHA256 (MGF1+SHA-256) — wymagane przez KSeF API v2.
	 *
	 * PHP natywne `openssl_public_encrypt` z `OPENSSL_PKCS1_OAEP_PADDING` używa SHA-1
	 * (twardo zakodowane w OpenSSL przez większość lat). KSeF wymaga SHA-256, więc
	 * implementujemy OAEP encoding ręcznie i podajemy do raw RSA przez OPENSSL_NO_PADDING.
	 *
	 * Used in:
	 *  - Flow B autoryzacji: encrypt `{tokenKSeF}|{timestampMs}` kluczem `KsefTokenEncryption`
	 *  - Sesja online: encrypt klucz symetryczny AES kluczem `SymmetricKeyEncryption`
	 *
	 * @param string $plaintext        Dane do zaszyfrowania (UTF-8 bytes, max n_bytes - 66).
	 * @param string $pem_public_key   Publiczny klucz MF w formacie PEM (X.509 cert lub SPKI).
	 * @return string Surowe bajty szyfrogramu (długość = rozmiar modulus klucza).
	 * @throws \RuntimeException
	 */
	public static function ksef_rsa_oaep_sha256( string $plaintext, string $pem_public_key ): string {
		$key = openssl_pkey_get_public( $pem_public_key );
		if ( false === $key ) {
			throw new \RuntimeException( esc_html( 'Invalid public key (PEM): ' . (string) openssl_error_string() ) );
		}
		$details = openssl_pkey_get_details( $key );
		if ( ! is_array( $details ) || empty( $details['bits'] ) ) {
			throw new \RuntimeException( 'Cannot read RSA key details.' );
		}
		$n_bytes = (int) ceil( $details['bits'] / 8 );

		$hash_len = 32; // SHA-256
		$mlen     = strlen( $plaintext );
		$max_len  = $n_bytes - 2 * $hash_len - 2;
		if ( $mlen > $max_len ) {
			throw new \RuntimeException( esc_html( sprintf( 'Plaintext too long for RSA-OAEP: %d > %d bytes', $mlen, $max_len ) ) );
		}

		// EME-OAEP encoding (RFC 8017 §7.1.1)
		$lhash       = hash( 'sha256', '', true );
		$ps_len      = $n_bytes - $mlen - 2 * $hash_len - 2;
		$ps          = str_repeat( "\x00", $ps_len );
		$db          = $lhash . $ps . "\x01" . $plaintext;
		$seed        = random_bytes( $hash_len );
		$db_mask     = self::ksef_mgf1_sha256( $seed, $n_bytes - $hash_len - 1 );
		$masked_db   = $db ^ $db_mask;
		$seed_mask   = self::ksef_mgf1_sha256( $masked_db, $hash_len );
		$masked_seed = $seed ^ $seed_mask;
		$em          = "\x00" . $masked_seed . $masked_db;

		// RSA raw encrypt (OPENSSL_NO_PADDING) — wymaga EM o długości równej rozmiarowi modulus
		$ciphertext = '';
		if ( ! openssl_public_encrypt( $em, $ciphertext, $key, OPENSSL_NO_PADDING ) ) {
			throw new \RuntimeException( esc_html( 'RSA encrypt failed: ' . (string) openssl_error_string() ) );
		}
		return $ciphertext;
	}

	/**
	 * MGF1 — Mask Generation Function 1 z SHA-256 (RFC 8017 §B.2.1).
	 */
	private static function ksef_mgf1_sha256( string $seed, int $length ): string {
		$output  = '';
		$counter = 0;
		while ( strlen( $output ) < $length ) {
			$output .= hash( 'sha256', $seed . pack( 'N', $counter ), true );
			$counter++;
		}
		return substr( $output, 0, $length );
	}

	/**
	 * Wygeneruj kryptograficznie bezpieczny klucz sesji AES-256-CBC + IV.
	 *
	 * @return array{key:string,iv:string} Raw bytes (32B key + 16B IV).
	 */
	public static function ksef_generate_session_key(): array {
		return [
			'key' => random_bytes( 32 ),
			'iv'  => random_bytes( 16 ),
		];
	}

	/**
	 * AES-256-CBC z PKCS#7 padding — szyfrowanie ładunku.
	 */
	public static function ksef_aes_256_cbc_encrypt( string $plaintext, string $key, string $iv ): string {
		if ( strlen( $key ) !== 32 ) {
			throw new \InvalidArgumentException( 'AES-256 key must be 32 bytes.' );
		}
		if ( strlen( $iv ) !== 16 ) {
			throw new \InvalidArgumentException( 'AES-256-CBC IV must be 16 bytes.' );
		}
		$ciphertext = openssl_encrypt( $plaintext, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv );
		if ( false === $ciphertext ) {
			throw new \RuntimeException( esc_html( 'AES encrypt failed: ' . (string) openssl_error_string() ) );
		}
		return $ciphertext;
	}

	/**
	 * AES-256-CBC z PKCS#7 padding — deszyfrowanie ładunku (faktury kosztowe z paczek eksportu).
	 */
	public static function ksef_aes_256_cbc_decrypt( string $ciphertext, string $key, string $iv ): string {
		$plaintext = openssl_decrypt( $ciphertext, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv );
		if ( false === $plaintext ) {
			throw new \RuntimeException( esc_html( 'AES decrypt failed: ' . (string) openssl_error_string() ) );
		}
		return $plaintext;
	}

	/**
	 * SHA-256 hash w postaci surowych bajtów (do podpisów / referencji w XML).
	 */
	public static function ksef_sha256( string $data ): string {
		return hash( 'sha256', $data, true );
	}

	/**
	 * SHA-256 hash zakodowany base64 — format pola `invoiceHash` w sesji online.
	 */
	public static function ksef_sha256_base64( string $data ): string {
		return base64_encode( self::ksef_sha256( $data ) );
	}
}
