<?php

/**
 * Class responsible for handling plugin settings.
 */

namespace BoostsiteAIContentGenerator\Api;

use Exception;
use BoostsiteAIContentGenerator\Boostsite;

if ( ! class_exists( 'SettingController' ) ) {
	/**
	 * Backend settings controller.
	 */
	class SettingController extends \WP_REST_Controller {

		/**
		 * The application domain.
		 *
		 * @var string
		 */
		private $prefix;

		/**
		 * Initialize this class.
		 */
		public function __construct() {
			$this->prefix    = \BoostsiteAIContentGenerator\Main::PREFIX;
			$this->namespace = $this->prefix . '/v1';
			$this->rest_base = 'settings';
		}

		/**
		 * Register the routes.
		 *
		 * @return void
		 */
		public function register_routes() {
			// Register get and update settings route.
			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base,
				array(
					array(
						'methods'             => \WP_REST_Server::READABLE,
						'callback'            => array( $this, 'get_settings' ),
						'permission_callback' => array( $this, 'get_items_permissions_check' ),
						'args'                => array(),
					),
					array(
						'methods'             => \WP_REST_Server::CREATABLE,
						'callback'            => array( $this, 'update_settings' ),
						'permission_callback' => array( $this, 'get_items_permissions_check' ),
						'args'                => array(),
					),
				)
			);

			// Register update plugin route.
			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base . '/update_plugin',
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'update_plugin' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => array(
						'rest_url' => array(
							'required'          => false,
							'validate_callback' => function ( $param, $request, $key ) {
								return is_string( $param );
							},
						),
					),
				)
			);

			// Register check connection route.
			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base . '/check_connection',
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'check_connection' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => array(),
				)
			);

			// Register  reset token route.
			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base . '/request_api_key_reset',
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'request_api_key_reset' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => array(),
				)
			);

			// Register reset api key route.
			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base . '/reset_api_key',
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'reset_api_key' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => array(),
				)
			);
		}

		/**
		 * Get the endpoint.
		 *
		 * @return string the full endpoint
		 */
		public function get_endpoint() {
			// example: myplugin/v1/settings.
			return $this->namespace . '/' . $this->rest_base;
		}

		/**
		 * Retrieves settings data only.
		 *
		 * @param bool $with_api_key Whether to include the API key in the response.
		 * @return array settings data
		 */
		public function get_settings_raw( $with_api_key = true ) {
			$settings = get_option( $this->prefix . '_settings', $this->get_setting_defaults() );

			if ( ! $with_api_key ) {
				$settings['api_key'] = '';
			}

			return $settings;
		}

		/**
		 * Retrieves a single setting data.
		 *
		 * @param string $key Options key to retrieve.
		 */
		public function get_single_setting( $key ) {
			return get_option( $this->prefix . '_settings', $this->get_setting_defaults() )[ $key ];
		}

		/**
		 * Retrieves settings.
		 *
		 * @param WP_REST_Request $request Full details about the request.
		 *
		 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
		 */
		public function get_settings( $request ) {
			$data     = $this->get_settings_raw( false );
			$response = $this->create_rest_response( array( 'data' => $data ), true );

			return rest_ensure_response( $response );
		}

		/**
		 * Update settings.
		 *
		 * @param \WP_REST_Request $request Full details about the request.
		 *
		 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
		 */
		public function update_settings( \WP_REST_Request $request ) {
			// Attempt to parse the json parameter.
			$params = $request->get_json_params();

			// Get the settings from params.
			$params_settings = $params['settings'];

			// Update correct response.
			$response = $this->create_rest_response( array( 'data' => $params_settings ) );

			if ( isset( $params_settings ) ) {
				try {
					// Add new users.
					$this->handle_user_creation( $params['settings']['schedules'] );

					// Set the data.
					$data             = $this->set_settings( $params_settings );
					$response['data'] = $data;

					// Set success parameter.
					$response['success'] = null !== $data;

					if ( null !== $data && $params['update_boostsite'] ) {
						$boostsite_response  = $this->update_boostsite_api( $data );
						$response['success'] = true;

						// Save the API key in the settings.
						$new_data            = $this->get_settings_raw();
						$new_data['api_key'] = $boostsite_response['data']['api_key'];
						$this->set_settings( $new_data );

						// Plan first batch of articles after successful settings update.
						$this->plan_first_batch_articles_api();
					}
				} catch ( \Exception $e ) {
					return new \WP_Error( 'settings_update_exception', $e->getMessage(), array( 'status' => 500 ) );
				} catch ( \Throwable $th ) {
					return $this->handle_throwable_error( $th, 'settings_update_error' );
				}
			} else {
				return new \WP_Error( 'invalid_parameters', __( 'Invalid parameters provided.', 'boostsite-ai-content-generator' ), array( 'status' => 400 ) );
			}

			return rest_ensure_response( $response );
		}

		/**
		 * Handle user creation based on schedules.
		 *
		 * @param array $schedules Schedules to process.
		 */
		private function handle_user_creation( $schedules ) {
			foreach ( $schedules as $schedule ) {
				$schedule = (object) $schedule;
				$author   = $schedule->author;

				if ( $author && ! username_exists( $author ) ) {
					// Generate a random username, password and email.
					$username = sanitize_user( strtolower( $author ) );
					$password = wp_generate_password( 12 );
					$email    = $username . '@example.com';

					// Prepare user data.
					$user_data = array(
						'user_login'    => $username,
						'user_nicename' => $author,
						'nickname'      => $author,
						'user_pass'     => $password,
						'user_email'    => $email,
						'display_name'  => $author,
						'role'          => 'author',
					);

					// Insert the user into the database.
					$user_id = wp_insert_user( $user_data );

					// Handle error.
					if ( is_wp_error( $user_id ) ) {
						return new \WP_Error( 'user_insert', $user_id, array( 'status' => 500 ) );
					}
				}
			}
		}

		/**
		 * Set plugin settings.
		 *
		 * @param  array $params Parameters with options to update.
		 * @return array Updated settings data or null on failure.
		 */
		public function set_settings( $params ) {
			if ( ! is_array( $params ) ) {
				return null;
			}

			$settings     = $params;
			$setting_key  = $this->prefix . '_settings';
			$new_settings = $this->sanitize_settings( $settings );
			$old_settings = get_option( $setting_key );

			$data = apply_filters( $this->prefix . '_settings_update', $new_settings, $old_settings );

			// Update options.
			update_option( $setting_key, $data );

			return $data;
		}

		/**
		 * Update plugin.
		 *
		 * @param \WP_REST_Request $request Full details about the request.
		 *
		 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
		 */
		public function update_plugin( \WP_REST_Request $request ) {
			// Attempt to parse the json parameter.
			$params = $request->get_json_params();

			// Update correct response.
			$response = $this->create_rest_response();

			try {
				// Prepare data to update boostsite plugin and in settings.
				$data     = array();
				$settings = $this->get_settings_raw();

				// Update the rest URL if its set.
				if ( isset( $params['rest_url'] ) && ! empty( $params['rest_url'] ) && ( str_starts_with( $params['rest_url'], 'http://' ) || str_starts_with( $params['rest_url'], 'https://' ) ) ) {
					$rest_url = sanitize_text_field( $params['rest_url'] );

					// Set the rest URL.
					$settings['rest_url'] = $rest_url;

					// Update settings.
					$data['rest_url'] = $rest_url;
				}

				// Update the enable category creation if its set.
				if ( array_key_exists( 'enable_category_creation', $params ) ) {

					$enable_category_creation = sanitize_text_field( $params['enable_category_creation'] );

					// Set the enable category creation.
					$settings['enable_category_creation'] = $enable_category_creation;

					// Update settings.
					$data['enable_category_creation'] = $enable_category_creation;
				}

				// Update the category creation limit if its set.
				if ( isset( $params['category_creation_limit'] ) && ! empty( $params['category_creation_limit'] ) ) {
					$category_creation_limit = sanitize_text_field( $params['category_creation_limit'] );

					// Set the category creation limit.
					$settings['category_creation_limit'] = $category_creation_limit;

					// Update settings.
					$data['category_creation_limit'] = $category_creation_limit;
				}

				if ( null !== $data && null !== $settings ) {
					// First update in plugin in Boostsite.
					$this->update_boostsite_api_plugin( $data );

					// Then update the settings.
					$this->set_settings( $settings );
				}
			} catch ( \Throwable $th ) {
				return $this->handle_throwable_error( $th, 'plugin_update_error' );
			}

			return rest_ensure_response( $response );
		}

		/**
		 * Check API connection.
		 *
		 * @param \WP_REST_Request $request Full details about the request.
		 *
		 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
		 */
		public function check_connection( \WP_REST_Request $request ) {
			$response = $this->create_rest_response();

			try {
				$boostsite_api      = new Boostsite();
				$boostsite_response = $boostsite_api->check_connection();

				$this->handle_boostsite_response( $boostsite_response );

				$response['success'] = true;
			} catch ( \Throwable $th ) {
				return $this->handle_throwable_error( $th, 'connection_check_error' );
			}

			return rest_ensure_response( $response );
		}

		/**
		 * Request API key reset.
		 *
		 * @param \WP_REST_Request $request Full details about the request.
		 *
		 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
		 */
		public function request_api_key_reset( \WP_REST_Request $request ) {
			$response = $this->create_rest_response();

			try {
				$boostsite_api      = new Boostsite();
				$boostsite_response = $boostsite_api->request_api_key_reset();

				$this->handle_boostsite_response( $boostsite_response );

				$response['success'] = true;
			} catch ( \Throwable $th ) {
				return $this->handle_throwable_error( $th, 'request_api_key_reset_error' );
			}

			return rest_ensure_response( $response );
		}

		/**
		 * Reset API key.
		 *
		 * @param \WP_REST_Request $request Full details about the request.
		 *
		 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
		 */
		public function reset_api_key( \WP_REST_Request $request ) {
			$response = $this->create_rest_response();

			try {
				$boostsite_api      = new Boostsite();
				$boostsite_response = $boostsite_api->api_key_reset( $request->get_param( 'token' ) );

				$this->handle_boostsite_response( $boostsite_response );

				if ( null !== $boostsite_response && isset( $boostsite_response['data'] ) && isset( $boostsite_response['data']['api_key'] ) ) {
					// Save the API key in the settings.
					$new_data            = $this->get_settings_raw();
					$new_data['api_key'] = $boostsite_response['data']['api_key'];
					$this->set_settings( $new_data );

					$response['success'] = true;
				} else {
					$response['success'] = false;
				}
			} catch ( \Throwable $th ) {
				return $this->handle_throwable_error( $th, 'api_key_reset_error' );
			}

			return rest_ensure_response( $response );
		}

		/**
		 * Update Boostsite API with new settings.
		 *
		 * @param array $data Settings data.
		 * @throws \Exception If Boostsite API update fails.
		 */
		private function update_boostsite_api( $data ) {
			$boostsite_api      = new Boostsite();
			$boostsite_response = $boostsite_api->update_settings( $data );

			$this->handle_boostsite_response( $boostsite_response );

			return $boostsite_response;
		}

		/**
		 * Update Boostsite API plugin with new settings.
		 *
		 * @param array $data Settings data.
		 * @throws \Exception If Boostsite API update fails.
		 */
		private function update_boostsite_api_plugin( $data ) {
			$boostsite_api      = new Boostsite();
			$boostsite_response = $boostsite_api->update_plugin( $data );

			$this->handle_boostsite_response( $boostsite_response );
		}

		/**
		 * Plan first batch of articles via Boostsite API.
		 *
		 * @throws \Exception If Boostsite API call fails.
		 */
		private function plan_first_batch_articles_api() {
			$boostsite_api      = new Boostsite();
			$boostsite_response = $boostsite_api->plan_first_batch_articles();

			$this->handle_boostsite_response( $boostsite_response );
		}

		/**
		 * Checks if a given request has access to read the items.
		 *
		 * @param  WP_REST_Request $request Full details about the request.
		 *
		 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
		 */
		public function get_items_permissions_check( $request ) {
			// optional: check nonce
			// https://via.studio/journal/wordpress-rest-api-secure-ajax-calls-custom-endpoints
			// example: /wp-json/me/v1/endpoint/?_wpnonce=${nonce}
			// check_ajax_referer('wp_rest', '_wpnonce', true)
			// 3rd parameter (die=true) to kill rest of execution.

			// Check the nonce for security.
			$nonce = $request->get_header( 'X-WP-Nonce' );
			if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
				return new \WP_Error( 'rest_forbidden', __( 'Invalid nonce.', 'boostsite-ai-content-generator' ), array( 'status' => 403 ) );
			}

			if ( ! current_user_can( 'manage_options' ) ) {
				return new \WP_Error( 'rest_forbidden', __( 'Sorry, you cannot update settings.', 'boostsite-ai-content-generator' ), array( 'status' => 403 ) );
			}

			// since success, we response with next noonce.
			header( 'X-WP-Nonce: ' . wp_create_nonce( 'wp_rest' ) );

			return true;
		}

		/**
		 * Settings structure goes here.
		 *
		 * @param  bool $run_options_callback Options.
		 * @return array settings structure definition
		 */
		public function get_settings_structure( $run_options_callback = false ) {
			$options = include \BoostsiteAIContentGenerator\Main::$plugin_dir . '/config/settings.php';

			if ( $run_options_callback ) {
				$settings_details = $options['options'];

				foreach ( $settings_details as $id => $details ) {
					if ( isset( $details['optionsCallback'] ) ) {
						$options['options'][ $id ]['options'] = call_user_func( $details['optionsCallback'], $details );
					}
				}
			}

			return $options;
		}

		/**
		 * Get setting defaults.
		 *
		 * @return array setting defaults
		 */
		public function get_setting_defaults() {
			$options = include \BoostsiteAIContentGenerator\Main::$plugin_dir . '/config/settings.php';
			$result  = array();

			$settings_details = $options['options'];
			foreach ( $settings_details as $id => $details ) {
				$result[ $id ] = $details['default'];
			}

			return $result;
		}

		/**
		 * Sanitize specific setting value.
		 *
		 * @param  array  $details Details.
		 * @param  array  $sanitized_settings Sanitized settings.
		 * @param  string $id ID.
		 * @param  mixed  $value Value.
		 * @return void
		 */
		private function sanitize_value( $details, &$sanitized_settings, $id, $value ) {
			$sanitized_value = null;

			// Check for custom sanitization function.
			if ( isset( $details['sanitize'] ) && is_callable( $details['sanitize'] ) ) {
				$sanitized_value = call_user_func( $details['sanitize'], $value );
			}

			// Options callback.
			if ( isset( $details['optionsCallback'] ) ) {
				$details['options'] = call_user_func( $details['optionsCallback'], $details );
			}

			// Default sanitization based on type.
			if ( is_null( $sanitized_value ) && isset( $details['type'] ) ) {
				switch ( $details['type'] ) {
					case 'email':
						$sanitized_value = trim( sanitize_email( $value ) );
						break;
					case 'schedule':
						// Santizie the schedule object.
						$sanitized_value = $this->sanitize_schedule( $value );
						break;
					case 'code':
						$sanitized_value = trim( wp_kses_post( $value ) );

						// Fix for CSS code.
						$sanitized_value = str_replace( '&gt;', '>', $sanitized_value );
						break;
					case 'number':
					case 'text':
					case 'color':
					case 'time':
						$sanitized_value = trim( sanitize_text_field( $value ) );
						break;
					case 'dropdown':
					case 'dropdownMultiselect':
						$sanitized_value = array();

						if ( ! is_array( $value ) ) {
							$value = explode( ',', $value );
						}

						foreach ( $value as $option ) {
							if ( array_key_exists( $option, $details['options'] ) ) {
								$sanitized_value[] = $option;
							}
						}

						// only one option for single dropdown.
						if ( 'drodown' === $details['type'] ) {
							$sanitized_value = $sanitized_value[0];
						}

						break;
					case 'textarea':
						$sanitized_value = trim( stripslashes( wp_kses_post( $value ) ) );
						break;
					case 'url':
						$sanitized_value = trim( esc_url( $value ) );
						break;
					case 'toggle':
						$sanitized_value = $value ? true : false;
						break;
				}
			}

			$sanitized_value = apply_filters( $this->prefix . '_settings_sanitized', $sanitized_value, $value, $id, $details );

			if ( is_null( $sanitized_value ) ) {
				$sanitized_settings[ $id ] = $details[ $id ]['default'];
			} else {
				$sanitized_settings[ $id ] = $sanitized_value;
			}
		}

		/**
		 * Santize schedules
		 *
		 * @param  mixed $data Schedules.
		 * @return array
		 */
		private function sanitize_schedule( $data ) {
			// Check if the input is an array.
			if ( ! is_array( $data ) ) {
				return array();
			}

			// Helper function for text arrays.
			$sanitize_text_array = function ( $array ) {
				if ( ! is_array( $array ) ) {
					return array();
				}
				return array_map( 'sanitize_text_field', $array );
			};

			// Main sanitization logic.
			foreach ( $data as $index => $entry ) {
				if ( is_array( $entry ) ) {
					$data[ $index ] = array(
						'id'                    => isset( $entry['id'] ) ? sanitize_text_field( $entry['id'] ) : wp_rand( 1, 9999 ),
						'amount'                => isset( $entry['amount'] ) ? sanitize_text_field( $entry['amount'] ) : '',
						'topic'                 => isset( $entry['topic'] ) ? sanitize_text_field( $entry['topic'] ) : '',
						'days'                  => isset( $entry['days'] ) && is_array( $entry['days'] )
							? array_map( 'sanitize_text_field', $entry['days'] )
							: array(),
						'time'                  => isset( $entry['time'] ) ? sanitize_text_field( $entry['time'] ) : '',
						'time_zone'             => isset( $entry['time_zone'] ) ? sanitize_text_field( $entry['time_zone'] ) : '',
						'author'                => isset( $entry['author'] ) ? sanitize_text_field( $entry['author'] ) : '',
						'lang'                  => isset( $entry['lang'] ) ? sanitize_text_field( $entry['lang'] ) : '',
						'advSchedule'           => isset( $entry['advSchedule'] ) && is_array( $entry['advSchedule'] )
							? array_map( $sanitize_text_array, $entry['advSchedule'] )
							: null,
						'articles_keywords'     => isset( $entry['articles_keywords'] ) && is_array( $entry['articles_keywords'] )
							? array_map(
								function ( $keywords_group ) use ( $sanitize_text_array ) {
									return array(
										'keywords' => isset( $keywords_group['keywords'] ) && is_array( $keywords_group['keywords'] )
											? $sanitize_text_array( $keywords_group['keywords'] )
											: array(),
									);
								},
								$entry['articles_keywords']
							)
							: array(),
						'activity_date_enabled' => isset( $entry['activity_date_enabled'] ) ? $entry['activity_date_enabled'] : false,
						'activity_date'         => isset( $entry['activity_date'] ) && is_array( $entry['activity_date'] )
							? array_map( 'sanitize_text_field', $entry['activity_date'] )
							: array(),
					);
				} else {
					// Remove invalid entries.
					unset( $data[ $index ] );
				}
			}

			return $data;
		}

		/**
		 * Sanitize the settings.
		 *
		 * @param    array $settings Settings to sanitize.
		 */
		public function sanitize_settings( $settings ) {
			$sanitized_settings = array();
			$settings_details   = $this->get_settings_structure()['options'];

			foreach ( $settings_details as $id => $details ) {
				$value = isset( $settings[ $id ] ) ? $settings[ $id ] : $details['default'];

				$this->sanitize_value( $details, $sanitized_settings, $id, $value );
			}

			return $sanitized_settings;
		}

		/**
		 * Log error details.
		 *
		 * @param \Throwable $th Throwable instance.
		 */
		private function log_error( $th ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG && defined( 'BOOSTSITE_DEBUG' ) && BOOSTSITE_DEBUG ) {
				error_log( 'Error: ' . $th->getMessage() . ' in file: ' . $th->getFile() . ' on line: ' . $th->getLine() );
			}
		}

		/**
		 * Handle Boostsite API response and throw exception if error exists.
		 *
		 * @param mixed $boostsite_response Response from Boostsite API.
		 * @throws \Exception If API response contains an error.
		 */
		private function handle_boostsite_response( $boostsite_response ) {
			if ( is_wp_error( $boostsite_response ) ) {
				throw new \Exception( esc_attr( $boostsite_response->get_error_message() ) );
			}

			if ( isset( $boostsite_response['info'] ) && 'error_exist' === $boostsite_response['info'] && 'licence_already_enabled' !== $boostsite_response['data'] ) {
				throw new \Exception( esc_attr( $boostsite_response['data'] ) );
			}
		}

		/**
		 * Create standardized REST response structure.
		 *
		 * @param array $additional_data Additional data to include in response.
		 * @param bool  $success Success status.
		 * @return array Response array.
		 */
		private function create_rest_response( $additional_data = array(), $success = false ) {
			$response = array(
				'success' => $success,
				'nonce'   => wp_create_nonce( 'wp_rest' ),
			);

			return array_merge( $response, $additional_data );
		}

		/**
		 * Handle throwable errors and return standardized WP_Error.
		 *
		 * @param \Throwable $th Throwable instance.
		 * @param string     $error_code Error code for WP_Error.
		 * @param string     $error_message Error message for WP_Error.
		 * @return \WP_Error Standardized error response.
		 */
		private function handle_throwable_error( $th, $error_code = 'unexpected_error', $error_message = null ) {
			$this->log_error( $th );

			if ( ! $error_message ) {
				$error_message = __( 'An unexpected error occurred.', 'boostsite-ai-content-generator' );
			}

			return new \WP_Error( $error_code, $error_message, array( 'status' => 500 ) );
		}
	}
}
