<?php

declare(strict_types=1);

namespace SiteFort\Includes;

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

/**
 * Shared verified file mutations for admin-triggered configuration writes.
 */
class FilesystemMutation {

	/**
	 * Return true when a credentialed, non-direct filesystem transport is active.
	 */
	public static function has_configured_write_transport(): bool {
		global $wp_filesystem;

		if (
			$wp_filesystem instanceof \WP_Filesystem_Base
			&& ! ( $wp_filesystem instanceof \WP_Filesystem_Direct )
			&& self::is_filesystem_usable( $wp_filesystem )
		) {
			return true;
		}

		$filesystem = self::get_filesystem();

		return $filesystem instanceof \WP_Filesystem_Base
			&& ! ( $filesystem instanceof \WP_Filesystem_Direct )
			&& self::is_filesystem_usable( $filesystem );
	}

	/**
	 * Create a directory through direct writes first, then WP_Filesystem.
	 */
	public static function ensure_directory( string $dir, ?int $mode = null ): bool {
		if ( is_dir( $dir ) ) {
			return true;
		}

		if ( wp_mkdir_p( $dir ) ) {
			if ( is_int( $mode ) ) {
				self::apply_file_permissions( $dir, $mode );
			}

			return is_dir( $dir );
		}

		$filesystem = self::get_filesystem();
		if ( ! $filesystem ) {
			return false;
		}

		$target_mode = is_int( $mode ) && $mode > 0
			? $mode
			: ( defined( 'FS_CHMOD_DIR' ) ? (int) FS_CHMOD_DIR : 0755 );

		try {
			return $filesystem->mkdir( $dir, $target_mode, true ) && $filesystem->is_dir( $dir );
		} catch ( \Throwable $exception ) {
			return false;
		}
	}

	/**
	 * Read a local file, using WP_Filesystem only as a credentialed fallback.
	 *
	 * @return string|false
	 */
	public static function read_file_contents( string $path ) {
		$contents = @file_get_contents( $path );
		if ( is_string( $contents ) ) {
			return $contents;
		}

		$filesystem = self::get_filesystem();
		if ( ! $filesystem ) {
			return false;
		}

		try {
			$contents = $filesystem->get_contents( $path );
		} catch ( \Throwable $exception ) {
			return false;
		}

		return is_string( $contents ) ? $contents : false;
	}

	/**
	 * Write file contents through the best available transport.
	 */
	public static function write_bytes_to_file( string $path, string $contents, ?int $mode = null ): bool {
		$bytes = @file_put_contents( $path, $contents, LOCK_EX );
		if ( is_int( $bytes ) && $bytes === strlen( $contents ) ) {
			if ( is_int( $mode ) && $mode > 0 ) {
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- Permission normalization after a successful direct write.
				@chmod( $path, $mode );
			}

			return true;
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Locked stream fallback is used when simpler writes are unavailable.
		$handle = @fopen( $path, 'c+b' );
		$ok = false;

		if ( false !== $handle ) {
			try {
				if ( flock( $handle, LOCK_EX ) && ftruncate( $handle, 0 ) && 0 === fseek( $handle, 0 ) ) {
					$ok = self::write_all( $handle, $contents );
					fflush( $handle );
				}
			} finally {
				flock( $handle, LOCK_UN );
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing locked stream after the fallback write.
				fclose( $handle );
			}
		}

		if ( $ok ) {
			if ( is_int( $mode ) && $mode > 0 ) {
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- Permission normalization after a successful direct write.
				@chmod( $path, $mode );
			}

			return true;
		}

		$filesystem = self::get_filesystem();
		if ( ! $filesystem ) {
			return false;
		}

		$target_mode = is_int( $mode ) && $mode > 0
			? $mode
			: ( defined( 'FS_CHMOD_FILE' ) ? (int) FS_CHMOD_FILE : 0644 );

		try {
			return (bool) $filesystem->put_contents( $path, $contents, $target_mode );
		} catch ( \Throwable $exception ) {
			return false;
		}
	}

	/**
	 * Rewrite a file with verification, preferring atomic replacement when possible.
	 *
	 * @return true|\WP_Error
	 */
	public static function rewrite_existing_file( string $path, string $contents, ?int $mode = null, bool $prefer_atomic = true ) {
		$dir           = dirname( $path );
		$file_exists   = file_exists( $path );
		$file_writable = $file_exists && wp_is_writable( $path );
		$dir_writable  = is_dir( $dir ) && wp_is_writable( $dir );
		$filesystem    = self::get_filesystem();

		if ( $prefer_atomic && $dir_writable ) {
			$temp = tempnam( $dir, '.sitefort-' );
			if ( is_string( $temp ) && $temp !== '' ) {
				if ( is_int( $mode ) ) {
					self::apply_file_permissions( $temp, $mode );
				}

				if ( ! self::write_bytes_to_file( $temp, $contents, $mode ) ) {
					wp_delete_file( $temp );
					return new \WP_Error( 'write_failed', 'Could not write updated file contents.' );
				}

				if ( ! self::move_path( $temp, $path ) ) {
					wp_delete_file( $temp );
					return new \WP_Error( 'rename_failed', 'Atomic replacement of the file failed.' );
				}

				return true;
			}
		}

		if ( $filesystem && ( ! $file_writable || ( $prefer_atomic && ! $dir_writable ) ) ) {
			return self::write_existing_file_via_filesystem( $filesystem, $path, $contents, $mode, $prefer_atomic );
		}

		return self::direct_write_existing_file( $path, $contents, $mode );
	}

	/**
	 * Verify file contents after a write.
	 */
	public static function verify_written_contents( string $path, string $expected ): bool {
		$read = self::read_file_contents( $path );
		return is_string( $read ) && $read === $expected;
	}

	/**
	 * Delete a path, using WP_Filesystem only as a credentialed fallback.
	 *
	 * Do not replace the direct directory removal with WP_Filesystem-first logic.
	 * This helper intentionally protects SiteFort from stale or failed global
	 * WordPress filesystem transports during local cleanup.
	 */
	public static function delete_path( string $path ): bool {
		if ( ! file_exists( $path ) && ! is_link( $path ) ) {
			return true;
		}

		if ( is_dir( $path ) && ! is_link( $path ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- Direct local fallback is required when WP_Filesystem contains a stale or failed transport.
			if ( @rmdir( $path ) || ! file_exists( $path ) ) {
				return true;
			}
		} else {
			wp_delete_file( $path );
			if ( ! file_exists( $path ) && ! is_link( $path ) ) {
				return true;
			}
		}

		$filesystem = self::get_filesystem();
		if ( ! $filesystem ) {
			return ! file_exists( $path ) && ! is_link( $path );
		}

		try {
			if ( $filesystem->exists( $path ) && $filesystem->delete( $path, false ) ) {
				return ! $filesystem->exists( $path );
			}
		} catch ( \Throwable $exception ) {
			return ! file_exists( $path ) && ! is_link( $path );
		}

		return ! file_exists( $path ) && ! is_link( $path );
	}

	/**
	 * Delete a directory tree safely for plugin-owned runtime data.
	 */
	public static function delete_tree( string $path ): bool {
		if ( ! file_exists( $path ) && ! is_link( $path ) ) {
			return true;
		}

		if ( ! is_dir( $path ) || is_link( $path ) ) {
			return self::delete_path( $path );
		}

		if ( self::delete_tree_direct( $path ) ) {
			return true;
		}

		$filesystem = self::get_filesystem();
		if ( ! $filesystem ) {
			return ! file_exists( $path );
		}

		try {
			return $filesystem->delete( $path, true ) && ! $filesystem->exists( $path );
		} catch ( \Throwable $exception ) {
			return ! file_exists( $path );
		}
	}

	/**
	 * Copy a file, using WP_Filesystem only as a credentialed fallback.
	 */
	public static function copy_path( string $source, string $destination, ?int $mode = null ): bool {
		if ( @copy( $source, $destination ) ) {
			if ( is_int( $mode ) ) {
				self::apply_file_permissions( $destination, $mode );
			}

			return true;
		}

		$filesystem = self::get_filesystem();
		if ( ! $filesystem ) {
			return false;
		}

		try {
			$contents = $filesystem->get_contents( $source );
			if ( ! is_string( $contents ) ) {
				return false;
			}

			$target_mode = is_int( $mode ) && $mode > 0
				? $mode
				: ( defined( 'FS_CHMOD_FILE' ) ? (int) FS_CHMOD_FILE : 0644 );

			return (bool) $filesystem->put_contents( $destination, $contents, $target_mode );
		} catch ( \Throwable $exception ) {
			return false;
		}
	}

	/**
	 * Move a file into place, using WP_Filesystem only as a credentialed fallback.
	 */
	public static function move_path( string $source, string $destination ): bool {
		// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- Atomic rename is preferred when available.
		if ( @rename( $source, $destination ) ) {
			return file_exists( $destination );
		}

		if ( @copy( $source, $destination ) ) {
			wp_delete_file( $source );
			if ( file_exists( $destination ) && ! file_exists( $source ) ) {
				return true;
			}
		}

		$filesystem = self::get_filesystem();
		if ( ! $filesystem ) {
			return false;
		}

		try {
			return $filesystem->move( $source, $destination, true ) && file_exists( $destination );
		} catch ( \Throwable $exception ) {
			return false;
		}
	}

	/**
	 * Apply permissions without assuming direct chmod is available.
	 */
	public static function apply_file_permissions( string $path, int $permissions ): bool {
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod
		if ( @chmod( $path, $permissions ) ) {
			return true;
		}

		$filesystem = self::get_filesystem();
		if ( ! $filesystem ) {
			return false;
		}

		try {
			return (bool) $filesystem->chmod( $path, $permissions );
		} catch ( \Throwable $exception ) {
			return false;
		}
	}

	public static function temporary_suffix(): string {
		return self::random_suffix();
	}

	/**
	 * Return the active WordPress filesystem adapter when available.
	 */
	public static function get_filesystem(): ?\WP_Filesystem_Base {
		global $wp_filesystem;

		if ( $wp_filesystem instanceof \WP_Filesystem_Base && self::is_filesystem_usable( $wp_filesystem ) ) {
			return $wp_filesystem;
		}

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

		// WordPress can leave a transport object in $wp_filesystem even when the
		// connection/bootstrap step fails. Re-bootstrap before trusting an adapter
		// that cannot safely answer a basic local-path probe.
		try {
			if ( ! WP_Filesystem() ) {
				return null;
			}
		} catch ( \Throwable $exception ) {
			return null;
		}

		if ( ! ( $wp_filesystem instanceof \WP_Filesystem_Base ) ) {
			return null;
		}

		return self::is_filesystem_usable( $wp_filesystem ) ? $wp_filesystem : null;
	}

	/**
	 * Confirm that a WordPress filesystem adapter is connected enough to operate.
	 */
	public static function is_filesystem_usable( \WP_Filesystem_Base $filesystem ): bool {
		if ( $filesystem instanceof \WP_Filesystem_Direct ) {
			return true;
		}

		try {
			$remote_abspath = method_exists( $filesystem, 'abspath' ) ? $filesystem->abspath() : false;
			if ( is_string( $remote_abspath ) && '' !== $remote_abspath ) {
				return true;
			}

			return (bool) $filesystem->is_dir( ABSPATH );
		} catch ( \Throwable $exception ) {
			return false;
		}
	}

	/**
	 * Direct-write fallback for an existing file.
	 *
	 * @return true|\WP_Error
	 */
	private static function direct_write_existing_file( string $path, string $contents, ?int $mode ) {
		return self::with_temporary_file_permissions(
			$path,
			static function () use ( $path, $contents, $mode ) {
				if ( ! self::write_bytes_to_file( $path, $contents, $mode ) ) {
					return new \WP_Error( 'write_failed', 'Could not write updated file contents.' );
				}

				return true;
			}
		);
	}

	/**
	 * Filesystem-transport rewrite for hosts where direct writes are blocked.
	 *
	 * @return true|\WP_Error
	 */
	private static function write_existing_file_via_filesystem( \WP_Filesystem_Base $filesystem, string $path, string $contents, ?int $mode, bool $prefer_atomic ) {
		$target_mode = is_int( $mode ) && $mode > 0
			? $mode
			: ( defined( 'FS_CHMOD_FILE' ) ? (int) FS_CHMOD_FILE : 0644 );

		try {
			if ( $prefer_atomic ) {
				$temp_path = $path . '.tmp.' . self::random_suffix();
				if ( $filesystem->put_contents( $temp_path, $contents, $target_mode ) ) {
					if ( self::move_path( $temp_path, $path ) ) {
						return true;
					}

					if ( $filesystem->exists( $temp_path ) ) {
						$filesystem->delete( $temp_path, false );
					}
				}
			}

			if ( $filesystem->put_contents( $path, $contents, $target_mode ) ) {
				return true;
			}
		} catch ( \Throwable $exception ) {
			return new \WP_Error( 'write_failed', 'The configured WordPress filesystem transport failed during the write.' );
		}

		return new \WP_Error( 'write_failed', 'Could not write updated file contents through the configured WordPress filesystem transport.' );
	}

	private static function delete_tree_direct( string $path ): bool {
		/*
		 * Keep this recursive cleanup direct-first. Plugin Check recommends
		 * WP_Filesystem for generic plugins, but this class is the fallback layer
		 * used when WP_Filesystem itself may be poisoned or unavailable.
		 */
		if ( ! is_dir( $path ) || is_link( $path ) ) {
			return self::delete_path( $path );
		}

		$entries = @scandir( $path );
		if ( ! is_array( $entries ) ) {
			return false;
		}

		$ok = true;
		foreach ( $entries as $entry ) {
			if ( $entry === '.' || $entry === '..' ) {
				continue;
			}

			$child = rtrim( $path, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR . $entry;
			if ( is_dir( $child ) && ! is_link( $child ) ) {
				$ok = self::delete_tree_direct( $child ) && $ok;
				continue;
			}

			wp_delete_file( $child );
			if ( file_exists( $child ) || is_link( $child ) ) {
				$ok = false;
			}
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- Direct local fallback is required when WP_Filesystem contains a stale or failed transport.
		if ( ! @rmdir( $path ) && file_exists( $path ) ) {
			$ok = false;
		}

		return $ok && ! file_exists( $path );
	}

	/**
	 * Temporarily add owner-write permission for direct overwrite fallback.
	 *
	 * @return true|\WP_Error
	 */
	private static function with_temporary_file_permissions( string $file, callable $writer ) {
		$original_perms = is_file( $file ) ? @fileperms( $file ) : false;
		$changed        = false;

		if ( is_int( $original_perms ) && ! wp_is_writable( $file ) ) {
			$mode = $original_perms & 0777;
			if ( self::apply_file_permissions( $file, $mode | 0200 ) ) {
				$changed = true;
			}
		}

		try {
			return $writer();
		} finally {
			if ( $changed && is_int( $original_perms ) ) {
				self::apply_file_permissions( $file, $original_perms & 0777 );
			}
		}
	}

	private static function random_suffix(): string {
		try {
			return bin2hex( random_bytes( 8 ) );
		} catch ( \Throwable $exception ) {
			return wp_generate_password( 16, false, false );
		}
	}

	/**
	 * @param resource $handle
	 */
	private static function write_all( $handle, string $data ): bool {
		$written = 0;
		$length  = strlen( $data );

		while ( $written < $length ) {
			$result = fwrite( $handle, substr( $data, $written ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
			if ( false === $result || 0 === $result ) {
				return false;
			}

			$written += $result;
		}

		return true;
	}
}
