<?php

declare(strict_types=1);

namespace SiteFort\Includes;

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

/**
 * Shared WordPress filesystem credential negotiation for admin-triggered writes.
 *
 * Returns the core credentials form when direct local writes are unavailable,
 * validates the submitted credentials, bootstraps WP_Filesystem, and lets the
 * original write path proceed in the same request.
 */
class AdminFilesystemCredentials {

	public const RESPONSE_CODE = 'filesystem_credentials_required';

	/**
	 * Guard a REST mutation that may need filesystem credentials.
	 *
	 * @param \WP_REST_Request $request            Current REST request.
	 * @param string           $purpose            Friendly purpose key.
	 * @param callable         $direct_access_test Returns true when direct writes are already available.
	 * @return \WP_REST_Response|null
	 */
	public static function ensure_rest_access( \WP_REST_Request $request, string $purpose, callable $direct_access_test ): ?\WP_REST_Response {
		$allow_relaxed_file_ownership = self::allow_relaxed_file_ownership( $purpose );

		if ( self::has_write_access( $direct_access_test, $allow_relaxed_file_ownership ) ) {
			return null;
		}

		$credentials = self::extract_credentials( $request->get_params() );
		if ( null === $credentials ) {
			return new \WP_REST_Response( self::build_response_payload( $purpose, false, $allow_relaxed_file_ownership ), 409 );
		}

		if (
			! self::bootstrap_filesystem( $credentials, $allow_relaxed_file_ownership )
			|| ( ! (bool) call_user_func( $direct_access_test ) && ! self::has_usable_non_direct_transport() )
		) {
			return new \WP_REST_Response( self::build_response_payload( $purpose, true, $allow_relaxed_file_ownership ), 409 );
		}

		return null;
	}

	/**
	 * Guard an AJAX mutation that may need filesystem credentials.
	 *
	 * @param string   $purpose            Friendly purpose key.
	 * @param callable $direct_access_test Returns true when direct writes are already available.
	 * @return bool True when a JSON response has already been sent.
	 */
	public static function ensure_ajax_access( string $purpose, callable $direct_access_test ): bool {
		$allow_relaxed_file_ownership = self::allow_relaxed_file_ownership( $purpose );

		if ( self::has_write_access( $direct_access_test, $allow_relaxed_file_ownership ) ) {
			return false;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Each AJAX entrypoint verifies the sitefort_nonce before invoking this shared helper.
		$credentials = self::extract_credentials( $_POST );
		if ( null === $credentials ) {
			wp_send_json_error( self::build_response_payload( $purpose, false, $allow_relaxed_file_ownership ) );
			return true;
		}

		if (
			! self::bootstrap_filesystem( $credentials, $allow_relaxed_file_ownership )
			|| ( ! (bool) call_user_func( $direct_access_test ) && ! self::has_usable_non_direct_transport() )
		) {
			wp_send_json_error( self::build_response_payload( $purpose, true, $allow_relaxed_file_ownership ) );
			return true;
		}

		return false;
	}

	/**
	 * Determine whether the write path is already available.
	 */
	private static function has_write_access( callable $direct_access_test, bool $allow_relaxed_file_ownership ): bool {
		if ( (bool) call_user_func( $direct_access_test ) ) {
			return true;
		}

		if ( ! self::bootstrap_filesystem( null, $allow_relaxed_file_ownership ) ) {
			return false;
		}

		return self::has_usable_non_direct_transport();
	}

	/**
	 * Extract a credential payload from a request array.
	 *
	 * @param mixed $params Request parameters.
	 * @return array<string, string>|null
	 */
	private static function extract_credentials( $params ): ?array {
		if ( ! is_array( $params ) ) {
			return null;
		}

		$submitted = isset( $params['request_filesystem_credentials'], $params['connection_type'] )
			|| isset( $params['request_filesystem_credentials'], $params['hostname'] )
			|| isset( $params['request_filesystem_credentials'], $params['username'] )
			|| isset( $params['request_filesystem_credentials'], $params['password'] )
			|| isset( $params['request_filesystem_credentials'], $params['public_key'] )
			|| isset( $params['request_filesystem_credentials'], $params['private_key'] );

		if ( ! $submitted ) {
			return null;
		}

		$credentials = [];
		$raw_fields  = [
			'password'    => true,
			'public_key'  => true,
			'private_key' => true,
		];
		$fields = [ 'hostname', 'username', 'password', 'public_key', 'private_key', 'connection_type' ];

		foreach ( $fields as $field ) {
			if ( ! isset( $params[ $field ] ) ) {
				continue;
			}

			$value = $params[ $field ];
			if ( is_array( $value ) ) {
				continue;
			}

			$value = wp_unslash( (string) $value );
			$value = isset( $raw_fields[ $field ] )
				? trim( $value )
				: sanitize_text_field( $value );

			if ( $value !== '' ) {
				$credentials[ $field ] = $value;
			}
		}

		if ( ! isset( $credentials['connection_type'] ) && isset( $params['connection_type'] ) && ! is_array( $params['connection_type'] ) ) {
			$credentials['connection_type'] = sanitize_text_field( wp_unslash( (string) $params['connection_type'] ) );
		}

		return $credentials;
	}

	/**
	 * Bootstrap WP_Filesystem, optionally with submitted credentials.
	 *
	 * @param array<string, string>|null $credentials Filesystem credential payload.
	 * @return bool
	 */
	private static function bootstrap_filesystem( ?array $credentials = null, bool $allow_relaxed_file_ownership = false ): bool {
		global $wp_filesystem;

		if ( $wp_filesystem instanceof \WP_Filesystem_Base && ! FilesystemMutation::is_filesystem_usable( $wp_filesystem ) ) {
			$wp_filesystem = null;
		}

		if ( null === $credentials && $wp_filesystem instanceof \WP_Filesystem_Base ) {
			return FilesystemMutation::is_filesystem_usable( $wp_filesystem );
		}

		if ( ! function_exists( 'WP_Filesystem' ) || ! function_exists( 'request_filesystem_credentials' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		try {
			$bootstrapped = null === $credentials
				? (bool) WP_Filesystem( false, ABSPATH, $allow_relaxed_file_ownership )
				: (bool) WP_Filesystem( $credentials, ABSPATH, $allow_relaxed_file_ownership );
		} catch ( \Throwable $exception ) {
			$wp_filesystem = null;
			return false;
		}

		return $bootstrapped
			&& $wp_filesystem instanceof \WP_Filesystem_Base
			&& FilesystemMutation::is_filesystem_usable( $wp_filesystem );
	}

	private static function has_usable_non_direct_transport(): bool {
		global $wp_filesystem;

		return $wp_filesystem instanceof \WP_Filesystem_Base
			&& ! ( $wp_filesystem instanceof \WP_Filesystem_Direct )
			&& FilesystemMutation::is_filesystem_usable( $wp_filesystem );
	}

	/**
	 * Build the structured response payload consumed by the admin UI.
	 *
	 * @return array<string, mixed>
	 */
	private static function build_response_payload( string $purpose, bool $invalid_credentials, bool $allow_relaxed_file_ownership ): array {
		$meta    = self::purpose_meta( $purpose );
		$message = $invalid_credentials
			? 'The supplied filesystem credentials could not be verified. Review the connection details and try again.'
			: $meta['message'];

		return [
			'code'       => self::RESPONSE_CODE,
			'message'    => $message,
			'filesystem' => [
				'required' => true,
				'purpose'  => $purpose,
				'title'    => $meta['title'],
				'message'  => $message,
				'form'     => self::render_credentials_form( $invalid_credentials, $allow_relaxed_file_ownership ),
			],
		];
	}

	/**
	 * Render the core WordPress filesystem credential form as HTML.
	 */
	private static function render_credentials_form( bool $invalid_credentials, bool $allow_relaxed_file_ownership ): string {
		if ( ! function_exists( 'request_filesystem_credentials' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		ob_start();
		request_filesystem_credentials(
			admin_url( 'admin.php?page=sitefort-settings' ),
			'',
			$invalid_credentials,
			ABSPATH,
			[],
			$allow_relaxed_file_ownership
		);
		$html = (string) ob_get_clean();

		if ( $html !== '' ) {
			return $html;
		}

		return '<p>' . esc_html__( 'Filesystem credentials are required to complete this operation.', 'sitefort' ) . '</p>';
	}

	/**
	 * @return array{title:string,message:string}
	 */
	private static function purpose_meta( string $purpose ): array {
		$map = [
			'license_activation' => [
				'title'   => 'Filesystem access is required to save the SiteFort encryption key.',
				'message' => 'WordPress cannot write the encryption key directly on this host. Provide filesystem credentials so SiteFort can store the key securely before activation continues.',
			],
			'console_connect'    => [
				'title'   => 'Filesystem access is required to connect this site.',
				'message' => 'WordPress cannot write the encryption key directly on this host. Provide filesystem credentials so SiteFort can store the key securely before the Console connection is finalized.',
			],
			'key_restore'        => [
				'title'   => 'Filesystem access is required to restore the encryption key.',
				'message' => 'WordPress cannot rewrite the local key store directly on this host. Provide filesystem credentials so SiteFort can restore the encryption key safely.',
			],
			'key_generate'       => [
				'title'   => 'Filesystem access is required to replace the encryption key.',
				'message' => 'WordPress cannot rewrite the local key store directly on this host. Provide filesystem credentials so SiteFort can replace the encryption key safely.',
			],
			'key_rotation'       => [
				'title'   => 'Filesystem access is required to rotate the encryption key.',
				'message' => 'WordPress cannot rewrite the local key store directly on this host. Provide filesystem credentials so SiteFort can rotate the encryption key and re-encrypt secrets safely.',
			],
			'salts_regeneration' => [
				'title'   => 'Filesystem access is required to regenerate WordPress salts.',
				'message' => 'WordPress cannot rewrite wp-config.php directly on this host. Provide filesystem credentials so SiteFort can rotate the authentication salts safely.',
			],
			'database_prefix_change' => [
				'title'   => 'Filesystem access is required to change the database prefix.',
				'message' => 'WordPress cannot rewrite wp-config.php directly on this host. Provide filesystem credentials so SiteFort can update the table prefix safely after the database rename completes.',
			],
			'wp_config_rules'    => [
				'title'   => 'Filesystem access is required to update wp-config.php hardening rules.',
				'message' => 'WordPress cannot rewrite wp-config.php directly on this host. Provide filesystem credentials so SiteFort can persist the managed hardening rule safely.',
			],
			'extended_protection' => [
				'title'   => 'Filesystem access is required to update server-level WAF files.',
				'message' => 'WordPress cannot rewrite the server-level WAF files directly on this host. Provide filesystem credentials so SiteFort can update the bootstrap and runtime configuration safely.',
			],
		];

		return $map[ $purpose ] ?? [
			'title'   => 'Filesystem access is required to complete this operation.',
			'message' => 'WordPress cannot write the required files directly on this host. Provide filesystem credentials so SiteFort can finish the requested change safely.',
		];
	}

	private static function allow_relaxed_file_ownership( string $purpose ): bool {
		return $purpose === 'extended_protection';
	}
}
