<?php

namespace KaizenCoders\MagicLink;

use function MagicLogin\Block\setup;

class Install {
	/**
	 * DB updates and callbacks that need to be run per version.
	 *
	 * @since 1.0.0
	 * @var array
	 *
	 */
	private static $db_updates = [
		'1.0.0' => [
			'kc_ml_update_100_db_version',
		],

		'1.1.0' => [
			'kc_ml_update_110_create_tables',
			'kc_ml_update_110_update_options',
		],

		'1.1.7' => [
			'kc_ml_update_117_update_options',
		],

		'1.1.9' => [
			'kc_ml_update_119_update_options',
		],
	];

	/**
	 * Init Install/ Update Process
	 *
	 * @since 1.0.0
	 */
	public function init() {
		if ( ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
			add_action( 'admin_init', [ __CLASS__, 'check_version' ], 5 );
			add_action( 'admin_init', [ __CLASS__, 'install_actions' ] );
		}
	}

	/**
	 * Install if required
	 *
	 * @since 1.0.0
	 */
	public static function check_version() {
		$current_db_version = Option::get( 'db_version', null );

		// Get latest available DB update version
		$latest_db_version_to_update = self::get_latest_db_version_to_update();

		if ( is_null( $current_db_version ) || version_compare( $current_db_version, $latest_db_version_to_update, '<' ) ) {
			self::install();
		}

	}

	/**
	 * Update
	 *
	 * @since 1.0.0
	 */
	public static function install_actions() {
		if ( ! empty( $_GET['do_update_ml'] ) ) {
			check_admin_referer( 'ml_db_update', 'ml_db_update_nonce' );
			$from_db_version = ! empty( $_GET['from_db_version'] ) ? $_GET['from_db_version'] : '';

			self::delete_update_transient();

			if ( ! empty( $from_db_version ) ) {
				self::update_db_version( $from_db_version );
			}

			self::update( true );

		}

		if ( ! empty( $_GET['force_update_ml'] ) ) {
			check_admin_referer( 'ml_force_db_update', 'ml_force_db_update_nonce' );
			self::update();
			wp_safe_redirect( admin_url( 'admin.php?page=magic-link' ) );
			exit;
		}
	}

	/**
	 * Begin Installation
	 *
	 * @since 1.0.0
	 */
	public static function install() {
		if ( ! is_blog_installed() ) {
			return;
		}

		// Check if we are not already running this routine.
		if ( 'yes' === Cache::get_transient( 'installing' ) ) {
			return;
		}

		if ( self::is_new_install() ) {
			// If we made it till here nothing is running yet, lets set the transient now.
			Cache::set_transient( 'installing', 'yes', MINUTE_IN_SECONDS * 10 );

			Helper::maybe_define_constant( 'KC_ML_INSTALLING', true );

			// Create Files
			self::create_files();

			// Create Tables
			self::create_tables();

			// Create Default Option
			self::create_options();

			self::update_db_version( KC_ML_PLUGIN_VERSION );

			Option::add( 'installed_on', time() );
		}

		self::maybe_update_db_version();

		Cache::delete_transient( 'installing' );
	}

	/**
	 * Delete Update Transient
	 *
	 * @since 1.0.0
	 */
	public static function delete_update_transient() {
		global $wpdb;

		Option::delete( 'update_processed_tasks' );
		Option::delete( 'update_tasks_to_process' );

		$transient_like               = $wpdb->esc_like( '_transient_kc_ml_update_' ) . '%';
		$updating_like                = $wpdb->esc_like( '_transient_kc_ml_updating' ) . '%';
		$last_sent_queue_like         = '%' . $wpdb->esc_like( '_last_sending_queue_batch_run' ) . '%';
		$running_migration_queue_like = '%' . $wpdb->esc_like( '_running_migration_for_' ) . '%';
		$db_migration_queue_like      = '%' . $wpdb->esc_like( 'kc_ml_updater_batch_' ) . '%';

		$query = "DELETE FROM {$wpdb->prefix}options WHERE `option_name` LIKE '{$transient_like}' OR `option_name` LIKE '{$updating_like}' OR `option_name` LIKE '{$last_sent_queue_like}' OR `option_name` LIKE '{$running_migration_queue_like}' OR `option_name` LIKE '{$db_migration_queue_like}'";

		$wpdb->query( $query );

	}

	/**
	 * Is this new Installation?
	 *
	 * @return bool
	 *
	 * @since 1.0.0
	 */
	public static function is_new_install() {
		/**
		 * We are storing us_db_version if it's new installation.
		 *
		 */
		return is_null( Option::get( 'db_version', null ) );
	}

	/**
	 * Get latest db version based on available updates.
	 *
	 * @return mixed
	 *
	 * @since 1.0.0
	 */
	public static function get_latest_db_version_to_update() {
		$updates         = self::get_db_update_callbacks();
		$update_versions = array_keys( $updates );
		usort( $update_versions, 'version_compare' );

		return end( $update_versions );
	}

	/**
	 * Require DB updates?
	 *
	 * @return bool
	 *
	 * @since 1.0.0
	 */
	private static function needs_db_update() {
		$current_db_version = Helper::get_db_version();

		$latest_db_version_to_update = self::get_latest_db_version_to_update();

		return ! is_null( $current_db_version ) && version_compare( $current_db_version, $latest_db_version_to_update, '<' );
	}

	/**
	 * Check whether database update require? If require do update.
	 *
	 * @since 1.0.0
	 */
	private static function maybe_update_db_version() {
		if ( self::needs_db_update() ) {
			if ( apply_filters( 'kc_ml_enable_auto_update_db', true ) ) {
				self::update();
			}
		}
	}

	/**
	 * Get all database updates
	 *
	 * @return array
	 *
	 * @since 1.0.0
	 */
	public static function get_db_update_callbacks() {
		return self::$db_updates;
	}

	/**
	 * Do database update.
	 *
	 * @param  bool  $force
	 *
	 * @since 1.0.0
	 *
	 */
	private static function update( $force = false ) {
		// Check if we are not already running this routine.
		if ( ! $force && 'yes' === Cache::get_transient( 'updating' ) ) {
			return;
		}

		Cache::set_transient( 'updating', 'yes', MINUTE_IN_SECONDS * 5 );

		$current_db_version = Helper::get_db_version();

		$tasks_to_process = Option::get( 'update_tasks_to_process', [] );

		// Get all tasks processed
		$processed_tasks = Option::get( 'update_processed_tasks', [] );

		// Get al tasks to process
		$tasks = self::get_db_update_callbacks();

		if ( count( $tasks ) > 0 ) {
			foreach ( $tasks as $version => $update_callbacks ) {
				if ( version_compare( $current_db_version, $version, '<' ) ) {
					foreach ( $update_callbacks as $update_callback ) {
						if ( ! in_array( $update_callback, $tasks_to_process ) && ! in_array( $update_callback, $processed_tasks ) ) {
							$tasks_to_process[] = $update_callback;
						} else {
						}
					}

					// Update db version on every update run
					self::update_db_version( $version );
				}
			}
		}

		if ( count( $tasks_to_process ) > 0 ) {
			Option::set( 'update_tasks_to_process', $tasks_to_process );

			self::dispatch();

		} else {
			Cache::delete_transient( 'updating' );
		}

	}

	/**
	 * Dispatch database updates.
	 *
	 * @since 1.0.0
	 */
	public static function dispatch() {
		$batch = Option::get( 'update_tasks_to_process', [] );

		if ( count( $batch ) > 0 ) {
			$current_memory_limit = @ini_get( 'memory_limit' );

			// We may require lots of memory
			@ini_set( 'memory_limit', '-1' );

			// It may take long time to process database update.
			// So, increase execution time
			@set_time_limit( 360 );
			@ini_set( 'max_execution_time', 360 );

			foreach ( $batch as $key => $value ) {
				$is_value_exists = true;
				//$task_transient = $value . '_processed';
				$update_processed_tasks = Option::get( 'update_processed_tasks', [] );

				$task = false; // By default it's set to false

				// Check whether the tasks is already processed? If not, process it.
				if ( ! in_array( $value, $update_processed_tasks ) ) {
					$is_value_exists = false;
					$task            = (bool) self::task( $value );
				} else {
					unset( $batch[ $key ] );
				}

				if ( false === $task ) {
					if ( ! $is_value_exists ) {
						$update_processed_tasks[] = $value;
						Option::set( 'update_processed_tasks', $update_processed_tasks );
					}

					unset( $batch[ $key ] );
				}

			}

			Option::set( 'update_tasks_to_process', $batch );

			@ini_set( 'memory_limit', $current_memory_limit );
		}

		//Delete update transient
		Cache::delete_transient( 'updating' );
	}

	/**
	 * Run individual database update.
	 *
	 * @param $callback
	 *
	 * @return bool|callable
	 *
	 * @since 1.0.0
	 *
	 */
	public static function task( $callback ) {
		include_once dirname( __FILE__ ) . '/Upgrade/update-functions.php';

		$result = false;

		if ( is_callable( $callback ) ) {
			$result = (bool) call_user_func( $callback );

			if ( $result ) {
				//$logger->info( sprintf( '%s callback needs to run again', $callback ), self::$logger_context );
			} else {
				//$logger->info( sprintf( '--- Finished Task - %s ', $callback ), self::$logger_context );
			}
		} else {
			//$logger->notice( sprintf( '--- Could not find %s callback', $callback ), self::$logger_context );
		}

		return $result ? $callback : false;
	}

	/**
	 * Update DB Version & DB Update history
	 *
	 * @param  null  $version
	 *
	 * @since 1.0.0
	 *
	 */
	public static function update_db_version( $version = null ) {
		$latest_db_version_to_update = self::get_latest_db_version_to_update();

		Option::set( 'db_version', is_null( $version ) ? $latest_db_version_to_update : $version );

		if ( ! is_null( $version ) ) {
			$db_update_history_option = 'db_update_history';

			$db_update_history_data = Option::get( $db_update_history_option, [] );

			$db_update_history_data[ $version ] = Helper::get_current_date_time();

			Option::set( $db_update_history_option, $db_update_history_data );
		}
	}

	/**
	 * Create default options while installing
	 *
	 * @since 1.0.0
	 */
	public static function create_options( $version = null ) {
		if ( is_null( $version ) ) {
			$options_fn = 'get_options';
		} else {
			$v          = str_replace( '.', '', $version );
			$options_fn = 'get_' . $v . '_options';
		}

		$options = self::$options_fn();

		if ( Helper::is_forechable( $options ) ) {
			foreach ( $options as $option => $values ) {
				$existing_value = Option::get( $option, false );

				// No existing value, add new option with default value.
				if ( $existing_value === false ) {
					Option::add( $option, $values['default'], Helper::get_data( $values, 'autoload', false ) );
				} else {
					// If existing value is array and new default is array, merge them
					if ( is_array( $existing_value ) && is_array( $values['default'] ) ) {
						//$merged_value = array_merge_recursive( $values['default'], $existing_value );
						$merged_value = self::do_deep_merge( [ $existing_value, $values['default'] ] );
						Option::set( $option, $merged_value, Helper::get_data( $values, 'autoload', false ) );
					}
				}
			}
		}
	}

	/**
	 * Get default options for version 1.1.0
	 *
	 * @return array
	 *
	 * @since 1.1.0
	 */
	public static function get_110_options() {
		$default_options = [
			'general' => [
				'settings' => [
					'enable_magic_link'   => 1,
					'token_lifespan'      => 5,
					'token_lifespan_unit' => 'minutes',
					'show_on_login_form'  => 0,
					'email_subject'       => 'Your Magic Link for {site_name}',
					'email_body'          => '<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;"> <h2>Login to {site_name}</h2> <p>Hello!</p> <p>Click the button below to securely log in to your account. This link will expire in {expiry_time} {expiry_unit}.</p> <div style="text-align: center; margin: 30px 0;"> <a href="{magic_link}" style="background-color: #0073aa; color: white; padding: 15px 30px; text-decoration: none; border-radius: 5px; display: inline-block;"> Login to {site_name} </a> </div> <p>If the button doesn\'t work, copy and paste this link into your browser:</p> <p><a href="{magic_link}">{magic_link}</a></p> <p><small>If you didn\'t request this login link, you can safely ignore this email.</small></p> </div>',
				],
			],
		];

		return [
			'settings' => [ 'default' => $default_options, 'autoload' => true ],
		];
	}

	/**
	 * Get default options for version 1.1.7
	 *
	 * @return array
	 *
	 * @since 1.1.0
	 */
	public static function get_117_options() {
		$settings = KC_ML()->get_settings();

		$redirect_urls = Helper::get_data( $settings, 'general|settings|login_redirect|urls', [] );

		$default_url     = home_url();
		$role_based_urls = [];

		if ( ! empty( $redirect_urls ) ) {
			foreach ( $redirect_urls as $key => $redirect_url ) {
				if ( 'default_url' !== $key ) {
					$role_based_urls[ $key ] = $redirect_url;
				} else {
					$default_url = $redirect_url;
				}
			}
		}

		$default_options = [
			'general'  => [
				'settings' => [
					'login_redirect'           => $default_url,
					'role_based_redirect_urls' => $role_based_urls,
					'ip_address_check'         => 0,
				],
			],
			'security' => [
				'settings' => [
					'ip_address_check'         => 0,
					'brute_force_protection'   => [
						'enable' => 0,
					],
					'login_request_throttling' => [
						'enable' => 0,
					],
				],
			],
		];

		return [
			'settings' => [ 'default' => $default_options, 'autoload' => true ],
		];
	}

	/**
	 * Get default options for version 1.1.9
	 *
	 * @return array
	 *
	 * @since 1.1.9
	 */
	public static function get_119_options() {
		$default_options = [
			'security' => [
				'settings' => [
					'ip_address_check'         => 0,
					'brute_force_protection'   => [
						'enable' => 0,
					],
					'login_request_throttling' => [
						'enable' => 0,
					],
				],
			],
		];

		return [
			'settings' => [ 'default' => $default_options, 'autoload' => true ],
		];
	}

	/**
	 * Get default options
	 *
	 * @return array
	 *
	 * @since 1.1.0
	 */
	public static function get_options() {
		// List of version methods in order of priority (oldest to newest)
		$version_methods = [
			'get_110_options',
			'get_117_options',
			'get_119_options',
		];

		// Start with empty settings
		$merged_settings = [
			'settings' => [
				'default'  => [],
				'autoload' => true,
			],
		];

		// Merge each version's settings in order
		foreach ( $version_methods as $method ) {
			if ( method_exists( get_called_class(), $method ) ) {
				$version_settings = self::$method();
				if ( isset( $version_settings['settings']['default'] ) ) {
					$merged_settings['settings']['default'] = self::do_deep_merge(
						[ $merged_settings['settings']['default'], $version_settings['settings']['default'] ]
					);
				}
			}
		}

		return $merged_settings;
	}

	/**
	 * Remove options.
	 *
	 * @param $options
	 *
	 * @return void
	 */
	public static function remove_options( $options ) {
		if ( empty( $options ) ) {
			return;
		}

		$settings = Option::get('settings');

		foreach ( $options as $option ) {
			Helper::remove_key_by_path( $settings, $option );
		}

		Option::set( 'settings', $settings, true );
	}

	/**
	 * Deep merge multiple arrays, replacing existing values instead of merging them.
	 *
	 * @param  array  $arrays  An array of arrays to be merged.
	 *
	 * @return array The deeply merged array.
	 *
	 * @since 1.7.0
	 */
	public static function do_deep_merge( $arrays ) {
		// Helper function for recursive merging that replaces existing values
		$deep_merge = function ( $array1, $array2 ) use ( &$deep_merge ) {
			foreach ( $array2 as $key => $value ) {
				if ( is_array( $value ) && isset( $array1[ $key ] ) && is_array( $array1[ $key ] ) ) {
					// Recursively merge arrays
					$array1[ $key ] = $deep_merge( $array1[ $key ], $value );
				} else {
					// Always use the new value
					$array1[ $key ] = $value;
				}
			}

			return $array1;
		};

		if ( ! Helper::is_forechable( $arrays ) ) {
			return $arrays;
		}

		$merged_arrays = [];
		foreach ( $arrays as $array ) {
			$merged_arrays = $deep_merge(
				$merged_arrays,
				$array
			);
		}

		return $merged_arrays;
	}


	/**
	 * @param  null  $version
	 *
	 * @since 1.1.0
	 *
	 */
	public static function create_tables( $version = null ) {
		global $wpdb;

		$collate = '';

		if ( $wpdb->has_cap( 'collation' ) ) {
			$collate = $wpdb->get_charset_collate();
		}

		if ( is_null( $version ) ) {
			$schema_fn = 'get_schema';
		} else {
			$v         = str_replace( '.', '', $version );
			$schema_fn = 'get_' . $v . '_schema';
		}

		$wpdb->hide_errors();
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( self::$schema_fn( $collate ) );
	}

	/**
	 * Add options.
	 *
	 * @param $version
	 *
	 * @return void
	 *
	 * @since 1.1.0
	 */
	public static function add_options( $version = null ) {
		if ( is_null( $version ) ) {
			$options_fn = 'get_options';
		} else {
			$v          = str_replace( '.', '', $version );
			$options_fn = 'get_' . $v . '_options';
		}

		self::$options_fn();
	}


	/**
	 * @param  string  $collate
	 *
	 * @return string
	 *
	 * @since 1.1.0
	 *
	 */
	private static function get_schema( $collate = '' ) {
		$tables = self::get_110_schema( $collate );

		return $tables;
	}

	/**
	 * @param  string  $collate
	 *
	 * @return string
	 *
	 * @since 1.1.0
	 *
	 */
	public static function get_110_schema( $collate = '' ) {
		global $wpdb;

		$tables = "
            CREATE TABLE `{$wpdb->prefix}kc_ml_magic_links` (
				id bigint(20) NOT NULL AUTO_INCREMENT,
				user_id bigint(20) NOT NULL,
				token varchar(64) NOT NULL,
				expires datetime NOT NULL,
				used tinyint(1) DEFAULT 0,
				ip_address varchar(45),
				created_at datetime DEFAULT CURRENT_TIMESTAMP,
				PRIMARY KEY (id),
				UNIQUE KEY token (token),
				KEY user_id (user_id),
				KEY expires (expires)
            ) $collate;
        ";

		return $tables;
	}

	/**
	 * Get files to create
	 *
	 * @return array[]
	 *
	 * @since 1.1.0
	 */
	public static function get_files() {
		// If we need to create more files/ dirs in the future
		// use following code
		return array_merge(
			[],
			self::get_110_files()
		);
	}

	/**
	 * Create logs & uploads dir
	 *
	 * @return array[]
	 *
	 * @since 1.1.0
	 */
	public static function get_110_files() {
		return [
			[
				'base'    => KC_ML_LOG_DIR,
				'file'    => '.htaccess',
				'content' => 'deny from all',
			],
			[
				'base'    => KC_ML_LOG_DIR,
				'file'    => 'index.html',
				'content' => '',
			],

			[
				'base'    => KC_ML_UPLOADS_DIR,
				'file'    => '.htaccess',
				'content' => 'deny from all',
			],

			[
				'base'    => KC_ML_UPLOADS_DIR,
				'file'    => 'index.html',
				'content' => '',
			],
		];
	}

	/**
	 * Create files
	 *
	 * @param  array  $files
	 *
	 * @since 1.1.0
	 *
	 */
	public static function create_files( $files = [] ) {
		// Want to bypass creation of files?
		if ( apply_filters( 'kc_ml_install_skip_create_files', false ) ) {
			return;
		}

		if ( empty( $files ) ) {
			$files = self::get_files();
		}

		foreach ( $files as $file ) {
			if ( wp_mkdir_p( $file['base'] ) && ! file_exists( trailingslashit( $file['base'] ) . $file['file'] ) ) {
				$file_handle = @fopen( trailingslashit( $file['base'] ) . $file['file'], 'w' );
				if ( $file_handle ) {
					fwrite( $file_handle, $file['content'] );
					fclose( $file_handle );
				}
			}
		}
	}


}
