<?php

use Inpsyde\BackWPup\MsAzureDestinationConfiguration;
use MicrosoftAzure\Storage\Blob\BlobRestProxy;
use MicrosoftAzure\Storage\Blob\Models\Blob;
use MicrosoftAzure\Storage\Blob\Models\BlockList;
use MicrosoftAzure\Storage\Blob\Models\Container;
use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions;
use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions;
use MicrosoftAzure\Storage\Blob\Models\PublicAccessType;
use BackWPup\Utils\BackWPupHelpers;

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

/**
 * Documentation: http://www.windowsazure.com/en-us/develop/php/how-to-guides/blob-service/.
 */
class BackWPup_Destination_MSAzure extends BackWPup_Destinations {

	/**
	 * Service name
	 *
	 * @var string
	 */
	private const SERVICE_NAME = 'MSAzure';

	public const MSAZUREDIR          = 'msazuredir';
	public const MSAZUREMAXBACKUPS   = 'msazuremaxbackups';
	public const MSAZURESYNCNODELETE = 'msazuresyncnodelete';
	public const NEWMSAZURECONTAINER = 'newmsazurecontainer';

	/**
	 * Get default options for Azure destination.
	 *
	 * @return array
	 */
	public function option_defaults(): array {
		return [
			MsAzureDestinationConfiguration::MSAZURE_ACCNAME => '',
			MsAzureDestinationConfiguration::MSAZURE_KEY => '',
			MsAzureDestinationConfiguration::MSAZURE_CONTAINER => '',
			self::MSAZUREDIR                             => trailingslashit( sanitize_file_name( get_bloginfo( 'name' ) ) ),
			self::MSAZUREMAXBACKUPS                      => 15,
			self::MSAZURESYNCNODELETE                    => true,
		];
	}

	/**
	 * {@inheritDoc}
	 *
	 * @param int|array $jobid The job ID or an array of job IDs.
	 *
	 * @throws \UnexpectedValueException If there is an issue with the Microsoft Azure configuration.
	 * @throws Exception If there is an issue creating the Microsoft Azure container.
	 *
	 * @return void
	 */
	public function edit_form_post_save( $jobid ): void {
		try {
				$msazure_configuration = $this->msazureConfiguration();
		} catch ( \UnexpectedValueException $exception ) {
			BackWPup_Admin::message( __( 'Microsoft Azure Configuration: ', 'backwpup' ) . $exception->getMessage(), true );
			throw $exception;
		}

		if ( $msazure_configuration->is_new() ) {
			try {
				$this->createContainer( $msazure_configuration );

				BackWPup_Admin::message(
					sprintf(
						// translators: %s is the container name.
						__( 'MS Azure container "%s" created.', 'backwpup' ),
						esc_html( sanitize_text_field( $msazure_configuration->msazurecontainer() ) )
					)
				);
			} catch ( Exception $e ) {
				// translators: %s is the error message.
				BackWPup_Admin::message( sprintf( __( 'MS Azure container create: %s', 'backwpup' ), $e->getMessage() ), true );
				throw $e;
			}
		}

		$msazure_dir = $this->msazureDir();
		$jobids      = (array) $jobid;
		foreach ( $jobids as $jobid ) {
			BackWPup_Option::update(
				$jobid,
				MsAzureDestinationConfiguration::MSAZURE_ACCNAME,
				$msazure_configuration->msazureaccname()
			);
			BackWPup_Option::update(
				$jobid,
				MsAzureDestinationConfiguration::MSAZURE_KEY,
				$msazure_configuration->msazurekey()
			);
			BackWPup_Option::update(
				$jobid,
				MsAzureDestinationConfiguration::MSAZURE_CONTAINER,
				$msazure_configuration->msazurecontainer()
			);

			BackWPup_Option::update( $jobid, self::MSAZUREDIR, $msazure_dir );

			BackWPup_Option::update(
				$jobid,
				self::MSAZUREMAXBACKUPS,
				isset( $_POST[ self::MSAZUREMAXBACKUPS ] ) && is_numeric( $_POST[ self::MSAZUREMAXBACKUPS ] ) ? absint( $_POST[ self::MSAZUREMAXBACKUPS ] ) : $this->option_defaults()[ self::MSAZUREMAXBACKUPS ] // phpcs:ignore WordPress.Security.NonceVerification.Missing
			);

			BackWPup_Option::update(
				$jobid,
				self::MSAZURESYNCNODELETE,
				filter_input( INPUT_POST, self::MSAZURESYNCNODELETE ) ?: ''
			);
		}
	}

	/**
	 * Delete a file from Azure.
	 *
	 * @param string $jobdest    Job destination string.
	 * @param string $backupfile Backup file path.
	 *
	 * @return void
	 */
	public function file_delete( string $jobdest, string $backupfile ): void {
		$files          = get_site_transient( 'backwpup_' . strtolower( $jobdest ) );
		[$jobid, $dest] = explode( '_', $jobdest );

		if ( BackWPup_Option::get( $jobid, MsAzureDestinationConfiguration::MSAZURE_ACCNAME )
			&& BackWPup_Option::get( $jobid, MsAzureDestinationConfiguration::MSAZURE_KEY )
			&& BackWPup_Option::get( $jobid, MsAzureDestinationConfiguration::MSAZURE_CONTAINER ) ) {
			try {
				$blob_client = $this->createBlobClient(
					BackWPup_Option::get( $jobid, MsAzureDestinationConfiguration::MSAZURE_ACCNAME ),
					BackWPup_Encryption::decrypt(
						BackWPup_Option::get( $jobid, MsAzureDestinationConfiguration::MSAZURE_KEY )
					)
				);

				$this->deleteBlob(
					$blob_client,
					BackWPup_Option::get(
						$jobid,
						MsAzureDestinationConfiguration::MSAZURE_CONTAINER
					),
					$backupfile
				);

				// Update file list.
				foreach ( $files as $key => $file ) {
					if ( is_array( $file ) && $backupfile === $file['file'] ) {
						unset( $files[ $key ] );
					}
				}
			} catch ( Exception $e ) {
				BackWPup_Admin::message( 'MS AZURE: ' . $e->getMessage(), true );
			}
		}

		set_site_transient( 'backwpup_' . strtolower( $jobdest ), $files, YEAR_IN_SECONDS );
	}

	/**
	 * Run archive job for Azure.
	 *
	 * @param BackWPup_Job $job_object Job object.
	 *
	 * @return bool
	 */
	public function job_run_archive( BackWPup_Job $job_object ): bool {
		$job_object->substeps_todo = $job_object->backup_filesize + 2;

		if ( $job_object->steps_data[ $job_object->step_working ]['SAVE_STEP_TRY'] !== $job_object->steps_data[ $job_object->step_working ]['STEP_TRY'] ) {
			$job_object->log(
				sprintf(
				/* translators: %d: attempt number. */
				__( '%d. Try sending backup to a Microsoft Azure (Blob)&#160;&hellip;', 'backwpup' ),
				$job_object->steps_data[ $job_object->step_working ]['STEP_TRY']
			),
				E_USER_NOTICE
				);
		}

		try {
			$blob_rest_proxy = $this->createBlobClient(
				$job_object->job[ MsAzureDestinationConfiguration::MSAZURE_ACCNAME ],
				BackWPup_Encryption::decrypt( $job_object->job[ MsAzureDestinationConfiguration::MSAZURE_KEY ] )
			);

			if ( $job_object->steps_data[ $job_object->step_working ]['SAVE_STEP_TRY'] !== $job_object->steps_data[ $job_object->step_working ]['STEP_TRY'] ) {
				// Test for existing container.
				$containers = $this->getContainers( $blob_rest_proxy );

				$job_object->steps_data[ $job_object->step_working ]['container_url'] = '';

				foreach ( $containers as $container ) {
					if ( $job_object->job[ MsAzureDestinationConfiguration::MSAZURE_CONTAINER ] === $container->getName() ) {
						$job_object->steps_data[ $job_object->step_working ]['container_url'] = $container->getUrl();
						break;
					}
				}

				if ( ! $job_object->steps_data[ $job_object->step_working ]['container_url'] ) {
					$job_object->log(
						sprintf(
						/* translators: %s: container name. */
						__( 'MS Azure container "%s" does not exist!', 'backwpup' ),
						$job_object->job[ MsAzureDestinationConfiguration::MSAZURE_CONTAINER ]
					),
						E_USER_ERROR
						);

					return true;
				}
				$job_object->log(
					sprintf(
					/* translators: %s: container name. */
					__( 'Connected to MS Azure container "%s".', 'backwpup' ),
					$job_object->job[ MsAzureDestinationConfiguration::MSAZURE_CONTAINER ]
				),
					E_USER_NOTICE
					);

				$job_object->log( __( 'Starting upload to MS Azure&#160;&hellip;', 'backwpup' ), E_USER_NOTICE );
			}

			// Prepare upload.
			$file_handel = fopen( $job_object->backup_folder . $job_object->backup_file, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
			if ( $file_handel ) {
				fseek( $file_handel, $job_object->substeps_done );

				if ( empty( $job_object->steps_data[ $job_object->step_working ]['BlockList'] ) ) {
					$job_object->steps_data[ $job_object->step_working ]['BlockList'] = [];
				}

				while ( ! feof( $file_handel ) ) {
					$data = fread( $file_handel, 1048576 * 4 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- 4MB.
					if ( 0 === strlen( $data ) ) {
						continue;
					}
					$chunk_upload_start = microtime( true );
					$block_count        = count( $job_object->steps_data[ $job_object->step_working ]['BlockList'] ) + 1;
          // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
					$block_id = base64_encode( str_pad( $block_count, 6, '0', STR_PAD_LEFT ) );

					$blob_rest_proxy->createBlobBlock(
						$job_object->job[ MsAzureDestinationConfiguration::MSAZURE_CONTAINER ],
						$job_object->job[ self::MSAZUREDIR ] . $job_object->backup_file,
						$block_id,
						$data
					);

					$job_object->steps_data[ $job_object->step_working ]['BlockList'][] = $block_id;
					$chunk_upload_time         = microtime( true ) - $chunk_upload_start;
					$job_object->substeps_done = $job_object->substeps_done + strlen( $data );
					$time_remaining            = $job_object->do_restart_time();
					if ( $time_remaining < $chunk_upload_time ) {
						$job_object->do_restart_time( true );
					}
					$job_object->update_working_data();
				}
				fclose( $file_handel ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
			} else {
				$job_object->log( __( 'Can not open source file for transfer.', 'backwpup' ), E_USER_ERROR );

				return false;
			}

			$blocklist = $this->createBlockList();

			foreach ( $job_object->steps_data[ $job_object->step_working ]['BlockList'] as $block_id ) {
				$blocklist->addUncommittedEntry( $block_id );
			}
			unset( $job_object->steps_data[ $job_object->step_working ]['BlockList'] );

			// Commit Blocks.
			$blob_rest_proxy->commitBlobBlocks( $job_object->job[ MsAzureDestinationConfiguration::MSAZURE_CONTAINER ], $job_object->job[ self::MSAZUREDIR ] . $job_object->backup_file, $blocklist->getEntries() );

			++$job_object->substeps_done;
			$job_object->log(
				sprintf(
				/* translators: %s: destination path. */
				__( 'Backup transferred to %s', 'backwpup' ),
				$job_object->steps_data[ $job_object->step_working ]['container_url'] . '/' . $job_object->job[ self::MSAZUREDIR ] . $job_object->backup_file
			),
				E_USER_NOTICE
				);
			if ( ! empty( $job_object->job['jobid'] ) ) {
				BackWPup_Option::update( $job_object->job['jobid'], 'lastbackupdownloadurl', network_admin_url( 'admin.php' ) . '?page=backwpupbackups&action=downloadmsazure&file=' . $job_object->job[ self::MSAZUREDIR ] . $job_object->backup_file . '&jobid=' . $job_object->job['jobid'] );
			}
		} catch ( Exception $e ) {
			$context = $this->error_context( $e );
			$job_object->log(
				E_USER_ERROR,
				sprintf(
				/* translators: %s: error message. */
				__( 'Microsoft Azure API: %s', 'backwpup' ),
				$e->getMessage()
			),
				$e->getFile(),
				$e->getLine(),
				$context
				);
			$job_object->substeps_done = 0;
			unset( $job_object->steps_data[ $job_object->step_working ]['BlockList'] );
			if ( isset( $file_handel ) && is_resource( $file_handel ) ) {
				fclose( $file_handel ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
			}

			return false;
		}

		try {
			$backupfilelist = [];
			$filecounter    = 0;
			$files          = [];

			$blob_options = $this->createListBlobsOptions();
			$blob_options->setPrefix( $job_object->job[ self::MSAZUREDIR ] );

			$blobs = $this->getBlobs(
				$blob_rest_proxy,
				$job_object->job[ MsAzureDestinationConfiguration::MSAZURE_CONTAINER ],
				$blob_options
			);

			if ( is_array( $blobs ) ) {
				foreach ( $blobs as $blob ) {
					$file = basename( $blob->getName() );
					if ( $this->is_backup_archive( $file ) && $this->is_backup_owned_by_job( $file, $job_object->job['jobid'] ) ) {
						$backupfilelist[ $blob->getProperties()->getLastModified()->getTimestamp() ] = $file;
					}
					$files[ $filecounter ]['folder']      = $job_object->steps_data[ $job_object->step_working ]['container_url'] . '/' . dirname( $blob->getName() ) . '/';
					$files[ $filecounter ]['file']        = $blob->getName();
					$files[ $filecounter ]['filename']    = basename( $blob->getName() );
					$files[ $filecounter ]['downloadurl'] = network_admin_url( 'admin.php' ) . '?page=backwpupbackups&action=downloadmsazure&file=' . $blob->getName() . '&jobid=' . $job_object->job['jobid'];
					$files[ $filecounter ]['filesize']    = $blob->getProperties()->getContentLength();
					$files[ $filecounter ]['time']        = $blob->getProperties()->getLastModified()->getTimestamp();
					++$filecounter;
				}
			}
			// Delete old backups.
			if ( ! empty( $job_object->job[ self::MSAZUREMAXBACKUPS ] ) && $job_object->job[ self::MSAZUREMAXBACKUPS ] > 0 ) {
				if ( count( $backupfilelist ) > $job_object->job[ self::MSAZUREMAXBACKUPS ] ) {
					ksort( $backupfilelist );
					$numdeltefiles = 0;
					$deleted_files = [];

					while ( ! empty( $backupfilelist ) ) {
						$file = array_shift( $backupfilelist );
						if ( count( $backupfilelist ) < $job_object->job[ self::MSAZUREMAXBACKUPS ] ) {
							break;
						}
						$blob_rest_proxy->deleteBlob( $job_object->job[ MsAzureDestinationConfiguration::MSAZURE_CONTAINER ], $job_object->job[ self::MSAZUREDIR ] . $file );

						foreach ( $files as $key => $filedata ) {
							if ( $filedata['file'] === $job_object->job[ self::MSAZUREDIR ] . $file ) {
								$deleted_files[] = $filedata['filename'];
								unset( $files[ $key ] );
							}
						}
						++$numdeltefiles;
					}
					if ( $numdeltefiles > 0 ) {
						$job_object->log(
							sprintf(
								// translators: %d: number of files.
								_n(
									'%d file deleted on Microsoft Azure container.',
									'%d files deleted on Microsoft Azure container.',
									$numdeltefiles,
									'backwpup'
								),
								$numdeltefiles
							),
							E_USER_NOTICE
						);
					}

					parent::remove_file_history_from_database( $deleted_files, 'MSAZURE' );
				}
			}
			set_site_transient( 'backwpup_' . $job_object->job['jobid'] . '_msazure', $files, YEAR_IN_SECONDS );
		} catch ( Exception $e ) {
			$context = $this->error_context( $e );
			$job_object->log(
				E_USER_ERROR,
				sprintf(
				/* translators: %s: error message. */
				__( 'Microsoft Azure API: %s', 'backwpup' ),
				$e->getMessage()
			),
				$e->getFile(),
				$e->getLine(),
				$context
				);

			return false;
		}

		$job_object->substeps_done = $job_object->backup_filesize + 2;

		return true;
	}

	/**
	 * Check if Azure destination can run.
	 *
	 * @param array $job_settings
	 * @param bool  $test_connection Job settings.
	 *
	 * @return bool
	 */
	public function can_run( array $job_settings, bool $test_connection = true ): bool {
		if ( empty( $job_settings[ MsAzureDestinationConfiguration::MSAZURE_ACCNAME ] ) ) {
			return false;
		}

		if ( empty( $job_settings[ MsAzureDestinationConfiguration::MSAZURE_KEY ] ) ) {
			return false;
		}

		return ! ( empty( $job_settings[ MsAzureDestinationConfiguration::MSAZURE_CONTAINER ] ) );
	}

	/**
	 * Output inline JavaScript for Azure settings.
	 *
	 * @return void
	 */
	public function edit_inline_js(): void {
		?>
		<script type="text/javascript">
			jQuery(document).ready(function ($) {
				function msazuregetcontainer() {
					var data = {
						action: 'backwpup_dest_msazure',
						msazureaccname: $('#msazureaccname').val(),
						msazurekey: $('#msazurekey').val(),
						msazureselected: $('#msazurecontainer').val(),
						_ajax_nonce: $('input[name="backwpupajaxnonce"]').val()
					};
					$.post(ajaxurl, data, function (response) {
						$('#msazureBucketContainer').html(response);
					});
				}

				$('#msazureaccname').backwpupDelayKeyup(function () {
					msazuregetcontainer();
				});
				$('#msazurekey').backwpupDelayKeyup(function () {
					msazuregetcontainer();
				});
			});
		</script>
		<?php
	}

	/**
	 * Handle AJAX requests for Azure settings.
	 *
	 * @param array $args Request arguments.
	 *
	 * @return void
	 */
	public function edit_ajax( array $args = [] ): void {
		$error = '';
		$ajax  = false;

		$msazure_name     = filter_input(
			INPUT_POST,
			MsAzureDestinationConfiguration::MSAZURE_ACCNAME
		) ?: '';
		$msazure_key      = filter_input(
			INPUT_POST,
			MsAzureDestinationConfiguration::MSAZURE_KEY
		) ?: '';
		$msazure_selected = filter_input(
			INPUT_POST,
			'msazureselected'
		) ?: '';

		if ( $msazure_name || $msazure_key ) {
			if ( ! current_user_can( 'backwpup_jobs_edit' ) ) {
				wp_die( -1 );
			}
			check_ajax_referer( 'backwpup_ajax_nonce' );
			$args[ MsAzureDestinationConfiguration::MSAZURE_ACCNAME ] = $msazure_name;
			$args[ MsAzureDestinationConfiguration::MSAZURE_KEY ]     = $msazure_key;
			$args['msazureselected']                                  = $msazure_selected;
			$ajax = true;
		}
		echo '<span id="msazurecontainererror" class="bwu-message-error">';

		$containers = null;

		if ( ! empty( $args[ MsAzureDestinationConfiguration::MSAZURE_ACCNAME ] ) && ! empty( $args[ MsAzureDestinationConfiguration::MSAZURE_KEY ] ) ) {
			try {
				$blob_client = $this->createBlobClient(
					$args[ MsAzureDestinationConfiguration::MSAZURE_ACCNAME ],
					BackWPup_Encryption::decrypt( $args[ MsAzureDestinationConfiguration::MSAZURE_KEY ] )
				);

				$containers = $blob_client->listContainers()->getContainers();
			} catch ( Exception $e ) {
				$error = $e->getMessage();
			}
		}

		if ( empty( $args[ MsAzureDestinationConfiguration::MSAZURE_ACCNAME ] ) ) {
			esc_html_e( 'Missing account name!', 'backwpup' );
		} elseif ( empty( $args[ MsAzureDestinationConfiguration::MSAZURE_KEY ] ) ) {
			esc_html_e( 'Missing access key!', 'backwpup' );
		} elseif ( ! empty( $error ) ) {
			echo esc_html( $error );
		} elseif ( empty( $containers ) ) {
			esc_html_e( 'No container found!', 'backwpup' );
		}
		echo '</span>';

		if ( ! empty( $containers ) ) {
			$containers_list = [];
			foreach ( $containers as $container ) {
				$containers_list[ $container->getName() ] = $container->getName();
			}
			echo BackWPupHelpers::component( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
				'form/select',
				[
					'name'       => 'msazurecontainer',
					'identifier' => 'msazurecontainer',
					'label'      => esc_html__( 'Bucket selection', 'backwpup' ),
					'withEmpty'  => false,
					'value'      => $args['msazureselected'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
					'options'    => $containers_list, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
				]
				);
		}
		if ( $ajax ) {
			exit();
		}
	}

	/**
	 * Creates the service used to access the blob.
	 *
	 * @param string $account_name Account name.
	 * @param string $account_key  Account key.
	 *
	 * @return BlobRestProxy
	 */
	public function createBlobClient( string $account_name, string $account_key ): BlobRestProxy {
		$connection_string = 'DefaultEndpointsProtocol=https;AccountName='
			. $account_name . ';AccountKey=' . $account_key;

		return BlobRestProxy::createBlobService( $connection_string );
	}

	/**
	 * Build Azure configuration from request data.
	 *
	 * @return MsAzureDestinationConfiguration
	 */
	protected function msazureConfiguration(): MsAzureDestinationConfiguration {
		$msazureaccname   = filter_input( INPUT_POST, MsAzureDestinationConfiguration::MSAZURE_ACCNAME );
		$msazurekey       = filter_input( INPUT_POST, MsAzureDestinationConfiguration::MSAZURE_KEY );
		$msazurecontainer = filter_input(
			INPUT_POST,
			MsAzureDestinationConfiguration::MSAZURE_CONTAINER
		);

		if ( ! $msazurecontainer ) {
			$newmsazurecontainer = filter_input(
				INPUT_POST,
				self::NEWMSAZURECONTAINER
			);

			return MsAzureDestinationConfiguration::with_new_container(
				$msazureaccname,
				$msazurekey,
				$newmsazurecontainer
			);
		}

		return new MsAzureDestinationConfiguration(
			$msazureaccname,
			$msazurekey,
			$msazurecontainer
		);
	}

	/**
	 * Create container if needed.
	 *
	 * @param MsAzureDestinationConfiguration $configuration Configuration.
	 *
	 * @return void
	 */
	protected function createContainer( MsAzureDestinationConfiguration $configuration ): void {
		$blob_client = $this->createBlobClient(
			$configuration->msazureaccname(),
			$configuration->msazurekey()
		);

		$create_container_options = $this->createContainerOptionsFactory();
		$create_container_options->setPublicAccess( PublicAccessType::NONE );

		$blob_client->createContainer(
			$configuration->msazurecontainer(),
			$create_container_options
		);
	}

	/**
	 * Create container options factory.
	 *
	 * @return CreateContainerOptions
	 */
	protected function createContainerOptionsFactory(): CreateContainerOptions {
		return new CreateContainerOptions();
	}

	/**
	 * Delete a blob from a container.
	 *
	 * @param BlobRestProxy $blob_client Blob client.
	 * @param string        $container   Container name.
	 * @param string        $backupfile  Backup file name.
	 *
	 * @return void
	 */
	protected function deleteBlob( BlobRestProxy $blob_client, string $container, string $backupfile ): void {
		$blob_client->deleteBlob(
			$container,
			$backupfile
		);
	}

	/**
	 * Get blobs for a container.
	 *
	 * @param BlobRestProxy    $blob_client Blob client.
	 * @param string           $container   Container name.
	 * @param ListBlobsOptions $options     List options.
	 *
	 * @return Blob[]
	 */
	protected function getBlobs( BlobRestProxy $blob_client, string $container, ListBlobsOptions $options ): array {
		return $blob_client->listBlobs(
			$container,
			$options
		)->getBlobs();
	}

	/**
	 * Get containers for the account.
	 *
	 * @param BlobRestProxy $blob_client Blob client.
	 *
	 * @return Container[]
	 */
	protected function getContainers( BlobRestProxy $blob_client ): array {
		return $blob_client->listContainers()->getContainers();
	}

	/**
	 * Create a block list.
	 *
	 * @return BlockList
	 */
	protected function createBlockList(): BlockList {
		return new BlockList();
	}

	/**
	 * Create list blobs options.
	 *
	 * @return ListBlobsOptions
	 */
	protected function createListBlobsOptions(): ListBlobsOptions {
		return new ListBlobsOptions();
	}

	/**
	 * Build Azure directory path.
	 *
	 * @return string
	 */
	protected function msazureDir(): string {
		$msazure_dir = trailingslashit(
			str_replace(
				'//',
				'/',
				str_replace(
					'\\',
					'/',
					trim(
						filter_input( INPUT_POST, self::MSAZUREDIR ) ?: ''
					)
				)
			)
		);

		if ( '/' === substr( $msazure_dir, 0, 1 ) ) {
			$msazure_dir = substr( $msazure_dir, 1 );
		}

		if ( '/' === $msazure_dir ) {
			$msazure_dir = '';
		}

		return $msazure_dir;
	}

	/**
	 * It extracts the job id from a job destination string.
	 *
	 * @param string $job_destination String containing a job destination, ex. 1_SOME_DESTINATION.
	 *
	 * @return int
	 * @throws RuntimeException If the job id could not be extracted.
	 */
	protected function extractJobIdFromDestination( string $job_destination ) {
		$job_id = (int) substr( $job_destination, 0, strpos( $job_destination, '_', 1 ) );

		if ( ! $job_id ) {
			throw new RuntimeException(
				// translators: %s: Destination string.
				sprintf( esc_html__( 'Could not extract job id from destination %s.', 'backwpup' ), esc_html( $job_destination ) )
			);
		}

		return $job_id;
	}

	/**
	 * Build error context for Azure errors.
	 *
	 * @param Exception $exception Exception instance.
	 * @return array
	 */
	protected function error_context( Exception $exception ): array {
		$message     = strtolower( $exception->getMessage() );
		$status      = (int) $exception->getCode();
		$error_code  = '';
		$error_lower = '';

		if ( method_exists( $exception, 'getErrorCode' ) ) {
			$error_code  = (string) $exception->getErrorCode();
			$error_lower = strtolower( $error_code );
		}

		return $this->timeout_or_network_context( $status, $message, $error_code )
			?? $this->http_status_context( $status, $error_code )
			?? $this->permissions_context_from_keywords( $message, $error_lower, $error_code )
			?? $this->auth_context_from_keywords( $message, $error_lower, $error_code )
			?? $this->storage_context_from_keywords( $message, $error_lower, $error_code )
			?? [];
	}

	/**
	 * Returns a context for timeout or network errors
	 *
	 * @param int    $status     HTTP status code or cURL error code.
	 * @param string $message    Lowercased exception message.
	 * @param string $error_code Azure SDK error code, empty string if unavailable.
	 * @return array|null
	 */
	private function timeout_or_network_context( int $status, string $message, string $error_code ): ?array {
		if (
			! in_array( $status, [ 408, 504 ], true )
			&& false === strpos( $message, 'timed out' )
			&& false === strpos( $message, 'could not resolve' )
			&& false === strpos( $message, 'failed to connect' )
		) {
			return null;
		}

		$context = [
			'reason_code'   => \WPMedia\BackWPup\Backup\ReasonCode::REASON_TIMEOUT_OR_NETWORK,
			'destination'   => 'MSAZURE',
			'provider_code' => $error_code ?: 'network_error',
		];
		if ( in_array( $status, [ 408, 504 ], true ) ) {
			$context['http_status'] = $status;
		}
		return $context;
	}

	/**
	 * Returns a context for known HTTP status codes
	 *
	 * @param int    $status     HTTP status code.
	 * @param string $error_code Azure SDK error code, empty string if unavailable.
	 * @return array|null
	 */
	private function http_status_context( int $status, string $error_code ): ?array {
		switch ( $status ) {
			case 401:
			case 403:
				$reason_code      = \WPMedia\BackWPup\Backup\ReasonCode::REASON_INCORRECT_LOGIN;
				$default_provider = 'auth_failed';
				break;
			case 404:
				$reason_code      = \WPMedia\BackWPup\Backup\ReasonCode::REASON_NOT_FOUND;
				$default_provider = 'not_found';
				break;
			case 409:
				$reason_code      = \WPMedia\BackWPup\Backup\ReasonCode::REASON_CONFLICT;
				$default_provider = 'conflict';
				break;
			case 413:
				$reason_code      = \WPMedia\BackWPup\Backup\ReasonCode::REASON_FILE_TOO_LARGE;
				$default_provider = 'file_too_large';
				break;
			case 429:
				$reason_code      = \WPMedia\BackWPup\Backup\ReasonCode::REASON_RATE_LIMITED;
				$default_provider = 'rate_limited';
				break;
			case 507:
				$reason_code      = \WPMedia\BackWPup\Backup\ReasonCode::REASON_NOT_ENOUGH_STORAGE;
				$default_provider = 'insufficient_storage';
				break;
			default:
				// We cannot provide a reasonable catch-all for other statuses < 500.
				if ( $status < 500 ) {
					return null;
				}
				$reason_code      = \WPMedia\BackWPup\Backup\ReasonCode::REASON_SERVICE_UNAVAILABLE;
				$default_provider = 'service_unavailable';
		}

		return [
			'reason_code'   => $reason_code,
			'destination'   => 'MSAZURE',
			'provider_code' => $error_code ?: $default_provider,
			'http_status'   => $status,
		];
	}

	/**
	 * Returns a context for insufficient-permissions errors
	 *
	 * @param string $message     Lowercased exception message.
	 * @param string $error_lower Lowercased Azure SDK error code, empty string if unavailable.
	 * @param string $error_code  Azure SDK error code, empty string if unavailable.
	 * @return array|null
	 */
	private function permissions_context_from_keywords( string $message, string $error_lower, string $error_code ): ?array {
		if (
			false === strpos( $error_lower, 'permission' )
			&& false === strpos( $message, 'permission' )
		) {
			return null;
		}

		return [
			'reason_code'   => \WPMedia\BackWPup\Backup\ReasonCode::REASON_INSUFFICIENT_PERMISSIONS,
			'destination'   => 'MSAZURE',
			'provider_code' => $error_code ?: 'insufficient_permissions',
		];
	}

	/**
	 * Returns an incorrect-login context
	 *
	 * @param string $message     Lowercased exception message.
	 * @param string $error_lower Lowercased Azure SDK error code, empty string if unavailable.
	 * @param string $error_code  Azure SDK error code, empty string if unavailable.
	 * @return array|null
	 */
	private function auth_context_from_keywords( string $message, string $error_lower, string $error_code ): ?array {
		if (
			false === strpos( $error_lower, 'auth' )
			&& false === strpos( $message, 'auth' )
			&& false === strpos( $message, 'account key' )
			&& false === strpos( $message, 'not a valid connection string' )
		) {
			return null;
		}

		return [
			'reason_code'   => \WPMedia\BackWPup\Backup\ReasonCode::REASON_INCORRECT_LOGIN,
			'destination'   => 'MSAZURE',
			'provider_code' => $error_code ?: 'auth_failed',
		];
	}

	/**
	 * Returns a not-enough-storage context
	 *
	 * @param string $message     Lowercased exception message.
	 * @param string $error_lower Lowercased Azure SDK error code, empty string if unavailable.
	 * @param string $error_code  Azure SDK error code, empty string if unavailable.
	 * @return array|null
	 */
	private function storage_context_from_keywords( string $message, string $error_lower, string $error_code ): ?array {
		if (
			false === strpos( $error_lower, 'insufficient' )
			&& false === strpos( $error_lower, 'quota' )
			&& false === strpos( $message, 'insufficient' )
			&& false === strpos( $message, 'quota' )
			&& false === strpos( $message, 'not enough' )
		) {
			return null;
		}

		return [
			'reason_code'   => \WPMedia\BackWPup\Backup\ReasonCode::REASON_NOT_ENOUGH_STORAGE,
			'destination'   => 'MSAZURE',
			'provider_code' => $error_code ?: 'quota_exceeded',
		];
	}

	/**
	 * Get service name.
	 *
	 * @return string
	 */
	public function get_service_name(): string {
		return self::SERVICE_NAME;
	}
}
