<?php
/**
 * Stores validated uploads in the plugin's private uploads directory.
 *
 * @package EmberPrice
 */

namespace EmberPrice\Ingestion;

use EmberPrice\Core\Crypto;

defined( 'ABSPATH' ) || exit;

/**
 * Encrypts a validated file into `wp-content/uploads/emberprice/` under a
 * hashed `.embp` filename. Server deny rules are written as defense in
 * depth, but confidentiality does not depend on the web server honoring
 * them: a direct URL can only return authenticated ciphertext.
 */
final class UploadStorage {

	private const UPLOAD_SUBDIR = 'emberprice';

	/**
	 * Site-keyed encryption.
	 *
	 * @var Crypto
	 */
	private Crypto $crypto;

	/**
	 * Builds encrypted upload storage.
	 *
	 * @param Crypto|null $crypto Defaults to the current site's key.
	 */
	public function __construct( ?Crypto $crypto = null ) {
		$this->crypto = $crypto ?? Crypto::for_site();
	}

	/**
	 * Stores a file and returns where it ended up.
	 *
	 * @param string $tmp_path Path to the file to store (e.g. a PHP upload tmp_name).
	 *
	 * @return array{path: string, hash: string} Destination path and the original file's SHA-256 hash.
	 *
	 * @throws \RuntimeException If the file's hash can't be computed or it can't be moved into place.
	 */
	public function store( string $tmp_path ): array {
		$hash = hash_file( 'sha256', $tmp_path );

		if ( false === $hash ) {
			throw new \RuntimeException( __( 'Could not hash the uploaded file.', 'emberprice' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- server-side diagnostic message for a caught exception, never echoed to a browser.
		}

		$dir       = $this->ensure_directory();
		$filename  = $hash . '.embp';
		$dest_path = trailingslashit( $dir['path'] ) . $filename;
		$plaintext = file_get_contents( $tmp_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local validated upload, not a URL.

		if ( false === $plaintext ) {
			throw new \RuntimeException( __( 'Could not read the uploaded file.', 'emberprice' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

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

		WP_Filesystem();

		global $wp_filesystem;

		$stored = $wp_filesystem && $wp_filesystem->put_contents(
			$dest_path,
			$this->crypto->encrypt( $plaintext ),
			FS_CHMOD_FILE
		);

		if ( ! $stored ) {
			throw new \RuntimeException( __( 'Could not store the uploaded file.', 'emberprice' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- server-side diagnostic message for a caught exception, never echoed to a browser.
		}

		return array(
			'path' => $dest_path,
			'hash' => $hash,
		);
	}

	/**
	 * Deletes the encrypted source file and any legacy plaintext file with
	 * the same hash.
	 *
	 * @param string $hash SHA-256 hash.
	 */
	public function delete_by_hash( string $hash ): void {
		if ( 1 !== preg_match( '/^[a-f0-9]{64}$/', $hash ) ) {
			return;
		}

		$dir = $this->ensure_directory()['path'];

		$matches = glob( trailingslashit( $dir ) . $hash . '.*' );

		foreach ( false !== $matches ? $matches : array() as $path ) {
			if ( is_file( $path ) ) {
				wp_delete_file( $path );
			}
		}
	}

	/**
	 * Removes source files written by 1.0.0 before encryption was added.
	 * Job audit records retain their original filename and SHA-256 hash.
	 */
	public function purge_legacy_unencrypted(): void {
		$dir = $this->ensure_directory()['path'];

		foreach ( array( 'csv', 'xlsx', 'xls', 'pdf' ) as $extension ) {
			$matches = glob( trailingslashit( $dir ) . '*.' . $extension );

			foreach ( false !== $matches ? $matches : array() as $path ) {
				if ( is_file( $path ) ) {
					wp_delete_file( $path );
				}
			}
		}
	}

	/**
	 * Ensures the plugin's upload directory and defense-in-depth deny rules
	 * exist.
	 *
	 * @return array{path: string, url: string}
	 */
	private function ensure_directory(): array {
		$upload_dir = wp_get_upload_dir();
		$path       = trailingslashit( $upload_dir['basedir'] ) . self::UPLOAD_SUBDIR;
		$url        = trailingslashit( $upload_dir['baseurl'] ) . self::UPLOAD_SUBDIR;

		if ( ! file_exists( $path ) ) {
			wp_mkdir_p( $path );
		}

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

		WP_Filesystem();

		global $wp_filesystem;

		$index_file = trailingslashit( $path ) . 'index.php';

		if ( ! file_exists( $index_file ) && $wp_filesystem ) {
			$wp_filesystem->put_contents( $index_file, "<?php\n// Silence is golden.\n", FS_CHMOD_FILE );
		}

		$htaccess_file = trailingslashit( $path ) . '.htaccess';

		if ( ! file_exists( $htaccess_file ) && $wp_filesystem ) {
			$wp_filesystem->put_contents( $htaccess_file, "Require all denied\nDeny from all\n", FS_CHMOD_FILE );
		}

		$web_config_file = trailingslashit( $path ) . 'web.config';

		if ( ! file_exists( $web_config_file ) && $wp_filesystem ) {
			$wp_filesystem->put_contents(
				$web_config_file,
				"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration><system.webServer><security><authorization><remove users=\"*\" roles=\"\" verbs=\"\" /><add accessType=\"Deny\" users=\"*\" /></authorization></security></system.webServer></configuration>\n",
				FS_CHMOD_FILE
			);
		}

		return array(
			'path' => $path,
			'url'  => $url,
		);
	}
}
