<?php
/**
 * Handles Swish certificate functions.
 *
 * @package flo-checkout
 */

namespace Flo_Checkout\Gateways\Swish;

use Flo_Checkout\Gateways\Swish;
use Flo_Checkout\Gateways\Swish_Pay;
use Flo_Checkout\Shared;
use WP_Error;

defined( 'ABSPATH' ) || exit;

/**
 * Class Swish_Certificates
 */
class Swish_Certificates {

	/**
	 * Sets up the necessary actions for Swish Pay functionality.
	 *
	 * This method registers the necessary actions for generating new certificates
	 * and rendering the certificate form modal in the WooCommerce checkout settings.
	 *
	 * @return void
	 */
	public static function setup(): void {
		add_action(
			'wp_ajax_swish_pay_generate_new_certificate',
			array( self::class, 'generate_new_certificate' )
		);

		add_action(
			'woocommerce_settings_checkout',
			array( self::class, 'render_certificate_form_modal' ),
			20
		);

		add_action( 'admin_post_download_cert', array( self::class, 'download_cert' ) );

		self::check_user_cert_upload_dir();
	}

	/**
	 * Generate html for certificate generation button.
	 *
	 * @return string HTML for button.
	 */
	public static function get_certificate_generation_html(): string {
		$certificate_download_link = self::generate_cert_download_link( 'csr', false );
		ob_start();
		require_once Shared::instance()->package_info->paths->get_path() . 'views/swish/admin/certificate_generation.php';

		return ob_get_clean();
	}

	/**
	 * Generate html for certificate download link.
	 *
	 * @param string $type          Type of certificate to download.
	 * @param bool   $add_br_before Whether to add line break before the link or not.
	 *
	 * @return string HTML for link.
	 */
	public static function generate_cert_download_link( string $type, bool $add_br_before = true ): string {

		switch ( $type ) {
			case 'public':
				$cert = Swish_Settings::get( self::get_cert_info_by_type( 'public', 'option_name' ) );
				$text = __( 'Download current public certificate', 'flo-checkout-plugin' );
				$url  = admin_url( 'admin-post.php?action=download_cert&type=public' );
				break;

			case 'private':
				$cert = Swish_Settings::get( self::get_cert_info_by_type( 'private', 'option_name' ) );
				$text = __( 'Download current private certificate', 'flo-checkout-plugin' );
				$url  = admin_url( 'admin-post.php?action=download_cert&type=private' );
				break;

			case 'csr':
				$cert = self::get_cert_info_by_type( 'csr', 'filepath' );
				$text = __( 'Download previous .csr file', 'flo-checkout-plugin' );
				$url  = admin_url( 'admin-post.php?action=download_cert&type=csr' );
				break;

			case 'sandbox_public':
				$cert = Swish_Settings::get( self::get_cert_info_by_type( 'sandbox_public', 'option_name' ) );
				$text = __( 'Download current public certificate', 'flo-checkout-plugin' );
				$url  = admin_url( 'admin-post.php?action=download_cert&type=sandbox_public' );
				break;

			case 'sandbox_private':
				$cert = Swish_Settings::get( self::get_cert_info_by_type( 'sandbox_private', 'option_name' ) );
				$text = __( 'Download current private certificate', 'flo-checkout-plugin' );
				$url  = admin_url( 'admin-post.php?action=download_cert&type=sandbox_private' );
				break;

			default:
				return '';
		}

		if ( ! $cert || ! file_exists( $cert ) ) {
			return '';
		}

		$link = sprintf(
			'<a href="%s" data-current="%s" target="_blank">%s</a>',
			$url,
			$cert,
			esc_html( $text ),
		);

		return $add_br_before ? '<br>' . $link : $link;
	}

	/**
	 * Retrieves certificate information based on the specified type and key.
	 *
	 * @param string      $type The type of certificate. Possible values: public, private, sandbox_public, sandbox_private, csr.
	 * @param string|null $key  The array key to retrieve specific information. Default is null.
	 *
	 * @return mixed The certificate information if $key is provided and exists in the $info array. Otherwise, it returns an empty string.
	 */
	public static function get_cert_info_by_type( string $type, string|null $key = null ): mixed {
		$info = array();

		$cert_uploads_path = self::get_user_certs_path();

		switch ( $type ) {
			case 'public':
				$info['option_name'] = 'live_certificate';
				$info['filename']    = 'prod_cert_file';
				$info['filepath']    = $cert_uploads_path . '/' . $info['filename'];
				break;

			case 'private':
				$info['option_name'] = 'live_private_certificate';
				$info['filename']    = 'prod_priv_cert_file';
				$info['filepath']    = $cert_uploads_path . '/' . $info['filename'];
				break;

			case 'sandbox_public':
				$info['option_name'] = 'sandbox_certificate';
				$info['filename']    = 'sandbox_cert_file';
				$info['filepath']    = $cert_uploads_path . '/' . $info['filename'];
				break;

			case 'sandbox_private':
				$info['option_name'] = 'sandbox_private_certificate';
				$info['filename']    = 'sandbox_priv_cert_file';
				$info['filepath']    = $cert_uploads_path . '/' . $info['filename'];
				break;

			case 'csr':
				$info['option_name'] = '';
				$info['filename']    = 'prod_swish.csr';
				$info['filepath']    = $cert_uploads_path . '/' . $info['filename'];
				break;
		}

		if ( ! $key ) {
			return $info;
		}

		return ! empty( $info[ $key ] ) ? $info[ $key ] : '';
	}

	/**
	 * Renders certificate form modal.
	 *
	 * @return void
	 */
	public static function render_certificate_form_modal(): void {
		$email_address = get_option( 'admin_email' );
		$blog_url      = get_bloginfo( 'url' );
		$domain        = preg_replace( '#^https?://#i', '', $blog_url );
		require_once Shared::instance()->package_info->paths->get_path() . 'views/swish/admin/certificate_form_modal.php';
	}

	/**
	 * Get the certificate file path.
	 *
	 * @param string $type Type of certificate.
	 *
	 * @return false|string
	 */
	public static function get_stored_cert_file_path( string $type ): false|string {

		// Get this gateway.
		$gateways = WC()->payment_gateways->payment_gateways();
		$gateway  = $gateways[ Swish_Pay::get_id() ] ?? false;

		if ( ! $gateway ) {
			wp_send_json_error( __( 'Swish payment gateway not found', 'flo-checkout-plugin' ) );
		}

		$cert_info = self::get_cert_info_by_type( $type );

		return ! empty( $cert_info['option_name'] ) ? $gateway->get_option( $cert_info['option_name'] ) : false;
	}

	/**
	 * Retrieves the path to the user's certificates directory.
	 *
	 * @return string The path to the user's certificates directory.
	 */
	public static function get_user_certs_path(): string {
		$wp_uploads = wp_upload_dir( null, false );

		return $wp_uploads['basedir'] . '/northmill-flo-checkout/swish-certs';
	}

	/**
	 * Function called by AJAX to generate new certificate request, and private key.
	 *
	 * @return void
	 *
	 * @phpcs:disable WordPress.Security.NonceVerification.Missing
	 * @phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
	 */
	public static function generate_new_certificate(): void {
		if ( ! wp_verify_nonce( $_POST['nonce'], 'flo_swish_settings_generate_certificate' ) ) {
			wp_send_json_error( 'Invalid nonce' );
		}

		// For SSL server certificates, the commonName is the domain name to be secured.
		// For S/MIME email certificates, the commonName is the owner of the email address.
		// Location and identification fields refer to the owner of domain or email subject to be secured.

		$city = wp_unslash( sanitize_text_field( $_POST['city'] ?? '' ) );
		$dn   = array(
			'countryName'            => 'SE',
			'stateOrProvinceName'    => $city,
			'localityName'           => $city,
			'organizationName'       => wp_unslash( sanitize_text_field( $_POST['organization'] ?? '' ) ),
			'organizationalUnitName' => 'Web',
			'commonName'             => wp_unslash( sanitize_text_field( $_POST['common_name'] ?? '' ) ),
			'emailAddress'           => wp_unslash( sanitize_text_field( $_POST['email'] ?? '' ) ),
		);

		// Generate a new private (and public) key pair.
		$private_key = openssl_pkey_new(
			array(
				'private_key_bits' => 4096,
				'private_key_type' => OPENSSL_KEYTYPE_RSA,
			)
		);

		// Generate a certificate signing request.
		$csr = openssl_csr_new(
			$dn,
			$private_key,
			array( 'digest_alg' => 'sha256' )
		);

		// Generate a self-signed cert, valid for 365 days.
		openssl_csr_sign(
			$csr,
			null,
			$private_key,
			365,
			array( 'digest_alg' => 'sha256' )
		);

		$certificate_private_key = wp_generate_password( 12, false );

		// Save your private key, CSR and self-signed cert for later use.
		openssl_csr_export( $csr, $csr_out );
		openssl_pkey_export( $private_key, $pkey_out, $certificate_private_key );

		$errors = array();

		/**
		 * Openssl error messages to ignore.
		 * Error messages will be checked if they contain any of the strings in this array.
		 * The string doesn't have to be the full error message, it can be a part of it.
		 */
		$errors_to_ignore = array(
			'NCONF_get_string:no value',
		);

		// Show any errors that occurred here.
		while ( ( $e = openssl_error_string() ) !== false ) {
			$contains_error_to_ignore = false;
			foreach ( $errors_to_ignore as $error_to_ignore ) {
				if ( str_contains( $e, $error_to_ignore ) ) {
					$contains_error_to_ignore = true;
					break;
				}
			}

			if ( ! $contains_error_to_ignore ) {
				$errors[] = $e;
			}
		}

		if ( ! empty( $errors ) ) {
			wp_send_json_error( $errors );
		}

		$stored_csr_file   = self::get_cert_info_by_type( 'csr', 'filepath' );
		$csr_upload_status = file_put_contents( $stored_csr_file, $csr_out );

		if ( ! $csr_upload_status ) {
			$errors[] = __( 'Failed to store CSR file.', 'flo-checkout-plugin' );

			wp_send_json_error( $errors );
		}

		$stored_private_certificate_file = self::get_cert_info_by_type( 'private', 'filepath' ) . 'key';
		$private_upload_status           = file_put_contents( $stored_private_certificate_file, $pkey_out );

		if ( ! $private_upload_status ) {
			$errors[] = __( 'Failed to store private key file.', 'flo-checkout-plugin' );

			wp_send_json_error( $errors );
		}

		Swish_Settings::update( 'live_certificate', '' ); // Clear public certificate, since it has to be uploaded manually.
		Swish_Settings::update( 'live_private_certificate', $stored_private_certificate_file );
		Swish_Settings::update( 'live_csr_file', $stored_csr_file );
		Swish_Settings::update( 'live_private_key', $certificate_private_key );

		wp_send_json_success(
			array(
				'private_key' => Swish_Settings::get( 'live_private_key' ),
				'message'     => __( 'Certificate generated', 'flo-checkout-plugin' ),
				'download'    => get_admin_url( null, 'admin-post.php?action=download_cert&type=csr' ),
			)
		);
	}

	/**
	 * Checks if the necessary certificate files exist.
	 *
	 * @return bool True if the certificate files exist, false otherwise.
	 * @noinspection PhpUndefinedConstantInspection -- Constants are optionally defined.
	 */
	public static function nmf_certs_exist(): bool {
		if (
			defined( 'NMF_CHECKOUT_SWISH_PEM_PATH' ) &&
			defined( 'NMF_CHECKOUT_SWISH_KEY_PATH' ) &&
			file_exists( NMF_CHECKOUT_SWISH_PEM_PATH ) &&
			file_exists( NMF_CHECKOUT_SWISH_KEY_PATH )
		) {
			return true;
		}

		return false;
	}

	/**
	 * This is hooked onto the admin_post_download_cert action.
	 * The admin_post_[action] handle is triggered for logged-in users, when visiting /wp-admin/admin-post.php?action=[action].
	 *
	 * We use this to download the certificate files.
	 */
	public static function download_cert(): never {
		$type     = stripslashes( sanitize_text_field( $_GET['type'] ?? '' ) );
		$filepath = false;

		switch ( $type ) {
			case 'csr':
				$filepath = self::get_cert_info_by_type( 'csr', 'filepath' );
				break;

			case 'private':
			case 'public':
			case 'sandbox_private':
			case 'sandbox_public':
				$filepath = self::get_stored_cert_file_path( $type );
				break;
		}

		if ( ! $filepath ) {
			wp_die( esc_html__( 'Invalid certificate type', 'flo-checkout-plugin' ) );
		}

		$filename = basename( $filepath );
		$filetype = filetype( $filepath );
		header( 'Content-Disposition: attachment; filename=' . $filename );
		header( 'Content-type: ' . $filetype );
		ob_clean();
		flush();
		readfile( $filepath );
		exit;
	}

	/**
	 * Checks if the user certificate upload directory exists and creates it if necessary.
	 * Also copies the .htaccess file to the directory if it doesn't exist.
	 *
	 * @return bool Returns true if the directory exists or is successfully created, false otherwise.
	 */
	public static function check_user_cert_upload_dir(): bool {
		$dir        = self::get_user_certs_path();
		$upload_dir = wp_mkdir_p( $dir ) ? $dir : false;

		if ( ! $upload_dir ) {
			return false;
		}

		if ( ! file_exists( $dir . '/.htaccess' ) ) {
			return copy( Shared::instance()->package_info->paths->get_path() . 'certs/.htaccess', $dir . '/.htaccess' );
		}

		return true;
	}

	/**
	 * Handles uploaded certificate files.
	 *
	 * @param array  $file                $_FILES array.
	 * @param string $filename            Name to use for new file. Without extension.
	 * @param array  $accepted_extensions Accepted file extensions.
	 * @param bool   $is_private_key      Whether the file is a private key.
	 *
	 * @return string|WP_Error            Path to file, or empty string if no file was uploaded, or WP_Error on fail.
	 */
	public static function handle_uploaded_cert_file(
		array $file,
		string $filename,
		array $accepted_extensions,
		bool $is_private_key = false
	): string|WP_Error {
		if ( empty( $file['name'] ) ) {
			if ( ! empty( $file['error'] ) && 4 !== $file['error'] ) {
				return new WP_Error(
					'file_error',
					sprintf(
						/* translators: %s: error message */
						__( 'File error: %s', 'flo-checkout-plugin' ),
						$file['error']
					)
				);
			} else {
				return '';
			}
		}

		$upload_dir = Swish\Swish_Certificates::get_user_certs_path();

		if ( ! $upload_dir ) {
			return new WP_Error(
				'upload_dir_error',
				__( 'Upload directory error.', 'flo-checkout-plugin' )
			);
		}

		$file_extension = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );

		if ( ! in_array( $file_extension, $accepted_extensions, true ) ) {
			return new WP_Error(
				'file_extension_error',
				__( 'Certificate file extension incorrect', 'flo-checkout-plugin' )
			);
		}

		// Max size 1MB.
		if ( $file['size'] > 1000000 ) {
			return new WP_Error(
				'file_size_error',
				__( 'Certificate file is too large', 'flo-checkout-plugin' )
			);
		}

		if ( $is_private_key && 'p12' === $file_extension ) {
			return self::handle_private_p12_file( $upload_dir, $file, $filename );
		}

		$filename      = sanitize_file_name( $filename . '.' . $file_extension );
		$uploaded_file = $upload_dir . '/' . $filename;

		// If the file already exists, delete it.
		if ( file_exists( $uploaded_file ) ) {
			unlink( $uploaded_file );
		}

		// Move the uploaded file to the upload dir.
		if ( move_uploaded_file( $file['tmp_name'], $uploaded_file ) ) {
			return $uploaded_file;
		}

		return new WP_Error(
			'file_upload_error',
			__( 'Failed to store certificate file.', 'flo-checkout-plugin' )
		);
	}

	/**
	 * Handles uploaded private key files in p12 format.
	 *
	 * @param string $upload_dir  Upload directory.
	 * @param array  $file        $_FILES array.
	 * @param string $filename    Name to use for new file. Without extension.
	 *
	 * @return string|WP_Error       Path to file, or WP_Error on fail.
	 */
	private static function handle_private_p12_file(
		string $upload_dir,
		array $file,
		string $filename
	): string|WP_Error {
		$p12_content = file_get_contents( $file['tmp_name'] );

		if ( ! $p12_content ) {
			return new WP_Error(
				'file_upload_error',
				__( 'Unable to read private .p12 file.', 'flo-checkout-plugin' )
			);
		}

		$is_p12_valid = openssl_pkcs12_read( $p12_content, $cert_info, 'swish' );

		if ( ! $is_p12_valid ) {
			return new WP_Error(
				'file_upload_error',
				__( 'Invalid private .p12 file.', 'flo-checkout-plugin' )
			);
		}

		if ( empty( $cert_info['pkey'] ) ) {
			return new WP_Error(
				'file_upload_error',
				__( ".p12 file doesn't contain private key.", 'flo-checkout-plugin' )
			);
		}

		$filename      = sanitize_file_name( $filename . '.key' );
		$uploaded_file = $upload_dir . '/' . $filename;
		$upload_status = file_put_contents( $uploaded_file, $cert_info['pkey'] );

		if ( ! $upload_status ) {
			return new WP_Error(
				'file_upload_error',
				__( 'Failed to store private key file.', 'flo-checkout-plugin' )
			);
		}

		return $uploaded_file;
	}
}
