<?php
/**
 * File: Goodmeet_Meet.php
 *
 * Manages Google Meet integration for the goodmeet plugin, including handling Google API credentials,
 * tokens, and Calendar API interactions. Provides methods for user authentication and meeting management.
 *
 * @package SOVLIX\GOODMEET
 */

namespace SOVLIX\GOODMEET\Googlemeet;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'Goodmeet_Meet' ) ) {
	/**
	 * Class Goodmeet_Meet
	 *
	 * This class is responsible for integrating Google Meet with the goodmeet plugin. It handles the
	 * initialization of Google Client, managing API credentials and tokens, and facilitating OAuth flows.
	 */
	class Goodmeet_Meet {
		/**
		 * Holds the single instance of the class.
		 *
		 * This property ensures that the class follows the singleton pattern,
		 * allowing only one instance to be created and reused throughout the application.
		 *
		 * @var null|self The single instance of the class, or null if not yet instantiated.
		 */
		private static $instance = null;

		/**
		 * Option name for storing credentials in database
		 *
		 * @var string
		 */
		private $credential_option_name = 'goodmeet_google_credentials';

		/**
		 * Option name for storing tokens in database
		 *
		 * @var string
		 */
		private $token_option_name = 'goodmeet_google_token';

		/**
		 * Username for authentication.
		 *
		 * @var string
		 */
		public $username;

		/**
		 * Google API client instance.
		 *
		 * @var \Google_Client
		 */
		public $client;

		/**
		 * Google API service instance.
		 *
		 * @var \Google_Service_Calendar
		 */
		public $service;

		/**
		 * Application name for the Google API client.
		 *
		 * @var string
		 */
		private $app_name = 'goodmeet';

		/**
		 * Google API callback URL for authentication.
		 *
		 * @var string
		 */
		private $google_callback_url;

		/**
		 * List of required Google API scopes.
		 *
		 * @var array
		 */
		private $required_scopes = array(
			\Google_Service_Calendar::CALENDAR,
			\Google_Service_Calendar::CALENDAR_EVENTS,
			'https://www.googleapis.com/auth/meetings.space.settings',
			'https://www.googleapis.com/auth/meetings.space.created',
			'https://www.googleapis.com/auth/meetings.space.readonly',
			'https://www.googleapis.com/auth/userinfo.email',
			'openid',
		);

		/**
		 * Current calendar type
		 *
		 * @var string
		 */
		public $current_calendar;

		/**
		 * Constructor initializes paths and hooks.
		 */
		public function __construct() {
			$owner_id               = null;
			$current_calendar       = 'primary';
			$this->current_calendar = $current_calendar;

			$this->username            = md5( wp_get_current_user()->user_login );
			$this->google_callback_url = admin_url() . 'admin.php?page=goodmeet-settings&platform=google-meet';

			$this->initialize_google_client();

			// Register AJAX for uploading credentials.
			add_action( 'wp_ajax_goodmeet_upload_google_meet_credential', array( $this, 'upload_credentials' ) );
		}

		/**
		 * Returns the single instance of the class.
		 */
		public static function get_instance(): self {
			if ( null === self::$instance ) {
				self::$instance = new self();
			}
			return self::$instance;
		}

		/**
		 * Initializes the Google client and service if credentials are available.
		 */
		private function initialize_google_client() {
			if ( $this->is_credential_loaded() ) {
				try {
					$credentials = $this->get_credentials_from_database();
					$this->validate_credentials_array( $credentials );

					$this->client = new \Google_Client();
					$this->client->setApplicationName( $this->app_name );
					$this->client->setAuthConfig( $credentials );
					$this->client->setRedirectUri( $this->google_callback_url );
					$this->client->addScope( $this->required_scopes );
					$this->client->setAccessType( 'offline' );
					$this->client->setApprovalPrompt( 'force' );

					if ( $this->assign_token_to_client() ) {
						$this->service = new \Google_Service_Calendar( $this->client );
					}
				} catch ( \Throwable $th ) {
					$this->handle_initialization_error( $th );
				}
			}
		}

		/**
		 * Get credentials from database
		 *
		 * @return array|false Credentials array or false if not found
		 */
		private function get_credentials_from_database() {
			$credentials = get_option( $this->credential_option_name, false );

			if ( $credentials && is_string( $credentials ) ) {
				$decoded = json_decode( $credentials, true );
				return $decoded ? $decoded : false;
			}

			return $credentials;
		}

		/**
		 * Save credentials to database
		 *
		 * @param array $credentials Credentials array.
		 * @return bool True on success, false on failure
		 */
		private function save_credentials_to_database( $credentials ) {
			if ( is_array( $credentials ) ) {
				$credentials = wp_json_encode( $credentials );
			}

			return update_option( $this->credential_option_name, $credentials );
		}

		/**
		 * Get token from database
		 *
		 * @return array|false Token array or false if not found
		 */
		private function get_token_from_database() {
			$token = get_option( $this->token_option_name, false );

			if ( $token && is_string( $token ) ) {
				$decoded = json_decode( $token, true );
				return $decoded ? $decoded : false;
			}

			return $token;
		}

		/**
		 * Save token to database
		 *
		 * @param array $token Token array.
		 * @return bool True on success, false on failure
		 */
		private function save_token_to_database( $token ) {
			if ( is_array( $token ) ) {
				$token = wp_json_encode( $token );
			}

			return update_option( $this->token_option_name, $token );
		}

		/**
		 * Delete credentials from database
		 *
		 * @return bool True on success, false on failure
		 */
		private function delete_credentials_from_database() {
			return delete_option( $this->credential_option_name );
		}

		/**
		 * Delete token from database
		 *
		 * @return bool True on success, false on failure
		 */
		private function delete_token_from_database() {
			return delete_option( $this->token_option_name );
		}

		/**
		 * Handles initialization errors and clears invalid credentials if necessary.
		 *
		 * Removes stored credentials if they exist and displays an admin notice with the error
		 * message when in the admin area.
		 *
		 * @param \Throwable $th The exception or error that caused the initialization failure.
		 *
		 * @return void
		 */
		private function handle_initialization_error( $th ) {
			$this->delete_credentials_from_database();

			if ( is_admin() ) {
				add_action(
					'admin_notices',
					function () use ( $th ) {
						printf(
							'<div class="%1$s"><p>%2$s</p></div>',
							esc_attr( 'notice notice-error is-dismissible' ),
							esc_html( $th->getMessage() )
						);
					}
				);
			}
		}

		/**
		 * Validates the credentials array.
		 *
		 * @param array $credentials Credentials array to validate.
		 *
		 * @return bool True if the credentials are valid.
		 * @throws \Exception If the credentials are invalid or missing required keys.
		 */
		public function validate_credentials_array( $credentials ) {
			if ( ! is_array( $credentials ) ) {
				throw new \Exception(
					esc_html__( 'Invalid credentials format', 'goodmeet' )
				);
			}

			$required_keys = array(
				'client_id',
				'client_secret',
				'project_id',
				'auth_uri',
				'token_uri',
			);

			$creds_to_check = isset( $credentials['web'] ) ? $credentials['web'] : $credentials;

			foreach ( $required_keys as $key ) {
				if ( ! array_key_exists( $key, $creds_to_check ) ) {
					throw new \Exception(
						sprintf(
							'%1$s %2$s',
							esc_html( $key ),
							esc_html__( 'does not exist in your credentials', 'goodmeet' )
						)
					);
				}
			}

			return true;
		}

		/**
		 * Validates the JSON service account configuration file.
		 *
		 * @param string $file_path Path to the JSON configuration file.
		 *
		 * @return bool True if the file is valid.
		 * @throws \Exception If the file does not exist, contains invalid JSON, or is missing required keys.
		 */
		public function validate_json_service_account_file( $file_path ) {
			if ( ! file_exists( $file_path ) ) {
				throw new \Exception(
					esc_html__( 'File does not exist', 'goodmeet' )
				);
			}

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

			if ( ! WP_Filesystem() ) {
				throw new \Exception(
					esc_html__( 'Unable to initialize WP_Filesystem', 'goodmeet' )
				);
			}

			global $wp_filesystem;

			$data = $wp_filesystem->get_contents( $file_path );
			$json = json_decode( $data, true );

			if ( json_last_error() !== JSON_ERROR_NONE ) {
				throw new \Exception(
					esc_html__( 'Invalid JSON file', 'goodmeet' )
				);
			}

			$this->validate_credentials_array( $json );

			return true;
		}

		/**
		 * Return consent screen url
		 *
		 * @since v2.1.0
		 *
		 * @return string  consent screen URL
		 */
		public function get_consent_screen_url() {
			return $this->client->createAuthUrl();
		}

		/**
		 * Handles AJAX upload of Google API credentials JSON file.
		 *
		 * Verifies user capability and nonce, validates uploaded JSON file,
		 * reads its contents, and saves credentials to the database.
		 *
		 * @since 1.0.0
		 * @throws \Exception If JSON is invalid or saving to database fails.
		 * @return void Outputs JSON success or error response.
		 */
		public function upload_credentials() {
			if ( ! current_user_can( 'manage_options' ) ) {
				wp_send_json_error(
					esc_html__( 'You are not authorized to perform this operation', 'goodmeet' )
				);
				return;
			}

			if ( ! isset( $_POST['_ajax_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ), 'goodmeet_admin_nonce' ) ) {
				wp_send_json_error(
					esc_html__( 'Invalid nonce. Operation not allowed.', 'goodmeet' )
				);
				return;
			}

			if ( empty( $_FILES['credential'] ) ) {
				wp_send_json_error(
					esc_html__( 'No file uploaded.', 'goodmeet' )
				);
				return;
			}

			// Setup the allowed mime types for upload.
			$allowed_mime_types = array(
				'json' => 'application/json',
			);

			add_filter(
				'upload_mimes',
				function ( $mimes ) use ( $allowed_mime_types ) {
					return array_merge( $mimes, $allowed_mime_types );
				},
				10,
				1
			);

			$uploaded = wp_handle_upload(
				$_FILES['credential'],
				array(
					'test_form' => false,
					'mimes'     => $allowed_mime_types,
				)
			);

			// Remove the MIME filter immediately after.
			remove_filter( 'upload_mimes', '__return_empty_array' );

			if ( isset( $uploaded['error'] ) ) {
				wp_send_json_error(
					esc_html__( 'Upload error: ', 'goodmeet' ) . esc_html( $uploaded['error'] )
				);
				return;
			}

			$file_path = $uploaded['file'];

			// Use WP_Filesystem API for file operations.
			global $wp_filesystem;
			if ( empty( $wp_filesystem ) ) {
				require_once ABSPATH . '/wp-admin/includes/file.php';
				WP_Filesystem();
			}

			// Read the file contents.
			$file_contents = $wp_filesystem->get_contents( $file_path );
			if ( false === $file_contents ) {
				wp_send_json_error(
					esc_html__( 'Failed to read uploaded file.', 'goodmeet' )
				);
				return;
			}

			// Delete the temp uploaded file immediately after reading.
			$wp_filesystem->delete( $file_path );

			try {
				// Validate the JSON content.
				$credentials = json_decode( $file_contents, true );
				if ( json_last_error() !== JSON_ERROR_NONE ) {
					throw new \Exception( esc_html__( 'Invalid JSON file', 'goodmeet' ) );
				}

				$this->validate_credentials_array( $credentials );

				// Save credentials to database instead of file.
				if ( $this->save_credentials_to_database( $credentials ) ) {
					wp_send_json_success(
						esc_html__( 'Credential uploaded successfully!', 'goodmeet' )
					);
				} else {
					throw new \Exception( esc_html__( 'Failed to save credentials to database', 'goodmeet' ) );
				}
			} catch ( \Throwable $th ) {
				wp_send_json_error(
					esc_html( $th->getMessage() )
				);
			}
		}

		/**
		 * Checks if credentials are loaded.
		 */
		public function is_credential_loaded() {
			$credentials = $this->get_credentials_from_database();
			return ! empty( $credentials );
		}

		/**
		 * Assigns the existing token, or tries to refresh if expired.
		 */
		public function assign_token_to_client() {
			$access_token = $this->get_token_from_database();

			if ( ! $access_token ) {
				return false;
			}

			$this->client->setAccessToken( $access_token );

			if ( $this->client->isAccessTokenExpired() ) {
				$refresh_token = $this->client->getRefreshToken();
				if ( ! $refresh_token ) {
					return false;
				}

				try {
					$new_token = $this->client->fetchAccessTokenWithRefreshToken( $refresh_token );
					return $this->save_token( null, $new_token );
				} catch ( \Exception $e ) {
					return false;
				}
			}

			return true;
		}

		/**
		 * Saves the Google API token.
		 *
		 * @param string|null $code  The authorization code.
		 * @param array|null  $token The token data. If null, it will fetch using the code.
		 *
		 * @return bool True if the token is saved successfully, false otherwise.
		 * @throws \InvalidArgumentException If the authorization code is missing.
		 * @throws \RuntimeException If saving the token fails.
		 */
		public function save_token( $code = null, $token = null ) {
			// Optional: Validate if the user has the required permissions.
			if ( ! current_user_can( 'manage_options' ) ) {
				return false;
			}

			try {
				// Fetch the token using the code if not provided.
				if ( ! $token ) {
					if ( ! $code ) {
						throw new \InvalidArgumentException( 'Authorization code is required.' );
					}
					$token = $this->client->fetchAccessTokenWithAuthCode( $code );
					$this->client->setAccessToken( $token );
					$token = $this->client->getAccessToken();
				}

				// Save token to database instead of file.
				$saved = $this->save_token_to_database( $token );

				if ( false === $saved ) {
					throw new \RuntimeException( 'Failed to save token to database.' );
				}

				return true;
			} catch ( \Throwable $th ) {
				// Optional: Log the error for debugging.
				return false;
			}
		}

		/**
		 * Reset credentials and tokens by deleting from database
		 *
		 * @return array Array with success/error messages
		 */
		public function reset_credentials() {
			$messages = array();
			$success  = true;

			// Delete credentials.
			if ( $this->is_credential_loaded() ) {
				if ( $this->delete_credentials_from_database() ) {
					$messages[] = __( 'Credentials reset successfully!', 'goodmeet' );
				} else {
					$messages[] = __( 'Failed to reset credentials.', 'goodmeet' );
					$success    = false;
				}
			} else {
				$messages[] = __( 'No credentials found to reset.', 'goodmeet' );
			}

			// Delete token.
			$token = $this->get_token_from_database();
			if ( $token ) {
				if ( $this->delete_token_from_database() ) {
					$messages[] = __( 'Token reset successfully!', 'goodmeet' );
				} else {
					$messages[] = __( 'Failed to reset token.', 'goodmeet' );
					$success    = false;
				}
			} else {
				$messages[] = __( 'No token found to reset.', 'goodmeet' );
			}

			return array(
				'success'  => $success,
				'messages' => $messages,
			);
		}

		/**
		 * Checks if the application is permitted via the consent screen.
		 */
		public function is_app_permitted() {
			if ( ! $this->is_credential_loaded() ) {
				return false;
			}
			return $this->assign_token_to_client() !== false;
		}
	}
}
