<?php
/**
 * Calendar credential encryption and signed member invitations.
 *
 * @package Niwa_AI
 */

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

class Niwa_AI_Calendar_Credentials {

	const SITE_IDENTITY_OPTION = 'niwa_ai_calendar_site_identity';

	/** Encrypt one secret with the WordPress installation salts. */
	public static function encrypt( $plain_text ) {
		$plain_text = (string) $plain_text;
		if ( '' === $plain_text ) {
			return '';
		}
		if ( ! function_exists( 'openssl_encrypt' ) || ! in_array( 'aes-256-gcm', openssl_get_cipher_methods(), true ) ) {
			return new WP_Error( 'calendar_crypto_unavailable', __( 'Secure calendar credential storage is unavailable on this server.', 'niwa-ai' ) );
		}

		$nonce  = random_bytes( 12 );
		$tag    = '';
		$cipher = openssl_encrypt( $plain_text, 'aes-256-gcm', self::encryption_key(), OPENSSL_RAW_DATA, $nonce, $tag, '', 16 );
		return false === $cipher || 16 !== strlen( $tag )
			? new WP_Error( 'calendar_crypto_failed', __( 'Calendar credential encryption failed.', 'niwa-ai' ) )
			: 'v2.' . self::base64url_encode( $nonce . $tag . $cipher );
	}

	/** Decrypt one stored secret. */
	public static function decrypt( $encoded ) {
		$encoded = (string) $encoded;
		if ( '' === $encoded ) {
			return '';
		}
		if ( ! function_exists( 'openssl_decrypt' ) || 0 !== strpos( $encoded, 'v2.' ) ) {
			return new WP_Error( 'calendar_crypto_unavailable', __( 'Secure calendar credential storage is unavailable on this server.', 'niwa-ai' ) );
		}

		$decoded = self::base64url_decode( substr( $encoded, 3 ) );
		if ( false === $decoded || strlen( $decoded ) <= 28 ) {
			return new WP_Error( 'calendar_secret_invalid', __( 'The stored calendar credential is invalid. Reconnect the calendar.', 'niwa-ai' ) );
		}

		$nonce = substr( $decoded, 0, 12 );
		$tag   = substr( $decoded, 12, 16 );
		$plain = openssl_decrypt( substr( $decoded, 28 ), 'aes-256-gcm', self::encryption_key(), OPENSSL_RAW_DATA, $nonce, $tag );
		return false === $plain
			? new WP_Error( 'calendar_secret_invalid', __( 'The stored calendar credential cannot be decrypted. Reconnect the calendar.', 'niwa-ai' ) )
			: $plain;
	}

	/** Build a seven-day bearer invitation for one configured team member. */
	public static function create_invite_token( $member_id, $expires_at = 0 ) {
		$payload = array(
			'member_id' => sanitize_key( (string) $member_id ),
			'exp'       => $expires_at > 0 ? (int) $expires_at : time() + ( 7 * DAY_IN_SECONDS ),
		);
		$encoded   = self::base64url_encode( wp_json_encode( $payload ) );
		$signature = hash_hmac( 'sha256', $encoded, self::invite_key(), true );
		return $encoded . '.' . self::base64url_encode( $signature );
	}

	/** Validate an invitation and return its member ID. */
	public static function validate_invite_token( $token ) {
		$parts = explode( '.', (string) $token, 2 );
		if ( 2 !== count( $parts ) ) {
			return new WP_Error( 'calendar_invite_invalid', __( 'This calendar connection link is invalid.', 'niwa-ai' ) );
		}

		$provided = self::base64url_decode( $parts[1] );
		$expected = hash_hmac( 'sha256', $parts[0], self::invite_key(), true );
		if ( false === $provided || ! hash_equals( $expected, $provided ) ) {
			return new WP_Error( 'calendar_invite_invalid', __( 'This calendar connection link is invalid.', 'niwa-ai' ) );
		}

		$decoded = self::base64url_decode( $parts[0] );
		$payload = false !== $decoded ? json_decode( $decoded, true ) : null;
		$member  = is_array( $payload ) ? sanitize_key( (string) ( $payload['member_id'] ?? '' ) ) : '';
		if ( '' === $member || empty( $payload['exp'] ) || (int) $payload['exp'] < time() ) {
			return new WP_Error( 'calendar_invite_expired', __( 'This calendar connection link has expired. Ask the administrator for a new link.', 'niwa-ai' ) );
		}
		return $member;
	}

	/** Generate a URL-safe random bearer token. */
	public static function random_token( $bytes = 32 ) {
		return self::base64url_encode( random_bytes( max( 16, (int) $bytes ) ) );
	}

	/** Return this installation's persistent P-256 signing identity. */
	public static function site_identity() {
		if ( ! function_exists( 'openssl_pkey_new' ) || ! function_exists( 'openssl_sign' ) ) {
			return new WP_Error( 'calendar_crypto_unavailable', __( 'Secure calendar connection is unavailable on this server.', 'niwa-ai' ) );
		}

		$stored   = get_option( self::SITE_IDENTITY_OPTION, array() );
		$identity = self::decode_site_identity( $stored );
		if ( ! is_wp_error( $identity ) ) {
			return $identity;
		}

		$key_options = self::openssl_key_options();
		$keypair     = openssl_pkey_new( $key_options );
		$secret_key = '';
		$details    = false !== $keypair ? openssl_pkey_get_details( $keypair ) : false;
		if ( false === $keypair || ! openssl_pkey_export( $keypair, $secret_key, null, $key_options ) || ! is_array( $details ) || empty( $details['key'] ) ) {
			return new WP_Error( 'calendar_identity_failed', __( 'Calendar connection identity could not be created.', 'niwa-ai' ) );
		}
		$public_key = (string) $details['key'];
		$encrypted  = self::encrypt( $secret_key );
		if ( is_wp_error( $encrypted ) ) {
			return $encrypted;
		}

		$new_value = array(
			'public_key' => self::base64url_encode( $public_key ),
			'secret_key' => $encrypted,
		);
		if ( ! add_option( self::SITE_IDENTITY_OPTION, $new_value, '', false ) ) {
			update_option( self::SITE_IDENTITY_OPTION, $new_value, false );
		}
		return array(
			'public_key' => $public_key,
			'secret_key' => $secret_key,
		);
	}

	/** URL-safe binary encoding shared with the OAuth relay client. */
	public static function encode_binary( $value ) {
		return self::base64url_encode( $value );
	}

	/** Decode URL-safe binary data shared with the OAuth relay client. */
	public static function decode_binary( $value ) {
		return self::base64url_decode( $value );
	}

	private static function decode_site_identity( $stored ) {
		if ( ! is_array( $stored ) || empty( $stored['public_key'] ) || empty( $stored['secret_key'] ) ) {
			return new WP_Error( 'calendar_identity_missing', __( 'Calendar connection identity is missing.', 'niwa-ai' ) );
		}
		$public_key = self::base64url_decode( $stored['public_key'] );
		$secret_key = self::decrypt( $stored['secret_key'] );
		$private_resource = ! is_wp_error( $secret_key ) ? openssl_pkey_get_private( $secret_key ) : false;
		$details         = false !== $private_resource ? openssl_pkey_get_details( $private_resource ) : false;
		if ( false === $public_key || false === $private_resource || ! is_array( $details ) || empty( $details['key'] ) || ! hash_equals( $public_key, (string) $details['key'] ) ) {
			return new WP_Error( 'calendar_identity_invalid', __( 'Calendar connection identity is invalid.', 'niwa-ai' ) );
		}
		return array(
			'public_key' => $public_key,
			'secret_key' => $secret_key,
		);
	}

	private static function encryption_key() {
		return hash( 'sha256', wp_salt( 'secure_auth' ) . '|niwa-calendar-credentials', true );
	}

	private static function openssl_key_options() {
		$options    = array(
			'private_key_type' => OPENSSL_KEYTYPE_EC,
			'curve_name'       => 'prime256v1',
		);
		$candidates = array(
			getenv( 'OPENSSL_CONF' ),
			dirname( PHP_BINARY ) . '/extras/ssl/openssl.cnf',
			'/etc/ssl/openssl.cnf',
			'/usr/lib/ssl/openssl.cnf',
		);
		foreach ( $candidates as $candidate ) {
			if ( is_string( $candidate ) && '' !== $candidate && is_readable( $candidate ) ) {
				$options['config'] = $candidate;
				break;
			}
		}
		return $options;
	}

	private static function invite_key() {
		return hash( 'sha256', wp_salt( 'nonce' ) . '|niwa-calendar-invitations', true );
	}

	private static function base64url_encode( $value ) {
		return rtrim( strtr( base64_encode( (string) $value ), '+/', '-_' ), '=' );
	}

	private static function base64url_decode( $value ) {
		$value = (string) $value;
		if ( '' === $value || 1 !== preg_match( '/^[A-Za-z0-9_-]+$/', $value ) ) {
			return false;
		}
		$padded = strtr( $value, '-_', '+/' );
		$padded .= str_repeat( '=', ( 4 - strlen( $padded ) % 4 ) % 4 );
		$decoded = base64_decode( $padded, true );
		return false !== $decoded && hash_equals( self::base64url_encode( $decoded ), $value ) ? $decoded : false;
	}
}
