<?php

namespace WPDRMS\ASP\Utils;

use Exception;
use InvalidArgumentException;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
use WPDRMS\ASP\Patterns\SingletonTrait;

/**
 * File manager/wrapper
 */
class FileManager {
	use SingletonTrait;

	/**
	 * @var string[]
	 */
	private array $allowed_directories;

	private function __construct() {
		$this->allowed_directories = array( wd_asp()->upload_path, wd_asp()->cache_path );
	}

	public function initialized( bool $init = false, string $check_method = '' ): bool {
		global $wp_filesystem;
		if ( $init && empty($wp_filesystem) ) {
			require_once ABSPATH . '/wp-admin/includes/file.php';
		}
		if ( function_exists('WP_Filesystem') && WP_Filesystem() === true && is_object($wp_filesystem) ) {
			if ( $check_method !== '' ) {
				return method_exists($wp_filesystem, $check_method);
			}
			return true;
		}
		// Did not init
		return false;
	}

	/**
	 * @param callable-string $func
	 * @return mixed
	 */
	public function wrapper( string $func ) {
		global $wp_filesystem;
		$args = func_get_args();
		array_shift($args);
		return $this->initialized(false, $func) ?
			call_user_func_array(array( $wp_filesystem, $func ), $args) : // @phpstan-ignore-line
			call_user_func_array($func, $args);
	}

	/**
	 * @param string $file
	 * @return false|int
	 */
	public function mtime( string $file ) {
		global $wp_filesystem;
		// Did it fail?
		if ( $this->initialized(false, 'mtime') ) {
			return $wp_filesystem->mtime($file);
		}
		return filemtime( $file );
	}

	public function isFile( string $path ): bool {
		return $this->wrapper('is_file', $path);
	}

	public function isDir( string $path ): bool {
		return $this->wrapper('is_dir', $path);
	}

	/**
	 * @param string $filename
	 * @return false|string
	 */
	public function read( string $filename ) {
		global $wp_filesystem;
		// Replace double
		$filename = str_replace(array( '\\\\', '//' ), array( '\\', '/' ), $filename);

		if ( !file_exists($filename) ) {
			return '';
		}

		if ( $this->initialized(false, 'get_contents') ) {
			// All went well, return
			return $wp_filesystem->get_contents( $filename );
		}

		return @file_get_contents($filename); //@phpcs:ignore
	}

	public function write( string $filename, string $contents ): bool {
		global $wp_filesystem;
		// Replace double
		$filename = str_replace(array( '\\\\', '//' ), array( '\\', '/' ), $filename);

		if ( $this->inAllowedDirectory($filename) ) {
			// Make sure that the directory exists
			$this->createRequiredDirectories();

			// Did it fail?
			if ( !$this->initialized(false, 'put_contents') ) {
				/* any problems and we exit */
				return !( @file_put_contents($filename, $contents) === false ); //@phpcs:ignore
			}

			// It worked, use it!
			if ( defined('FS_CHMOD_FILE') ) {
				if ( !$wp_filesystem->put_contents($filename, $contents, FS_CHMOD_FILE) ) {
					return !( @file_put_contents($filename, $contents) === false ); //@phpcs:ignore
				}
			} elseif ( !$wp_filesystem->put_contents($filename, $contents) ) {
				return !( @file_put_contents($filename, $contents) === false ); //@phpcs:ignore
			}
		}

		return true;
	}

	/**
	 * @param string $filename
	 * @return bool
	 */
	public function delFile( string $filename ): bool {
		global $wp_filesystem;
		if ( $this->inAllowedDirectory($filename) ) {
			// Did it fail?
			if ( !$this->initialized(false, 'delete') ) {
				/* any problems and we exit */
				return @unlink($filename); //@phpcs:ignore
			}
			return $wp_filesystem->delete($filename);
		} else {
			return false;
		}
	}

	/**
	 * Delete files in directory according to a pattern
	 *
	 * @param string $dir
	 * @param string $file_arg
	 * @return int files and directories deleted
	 */
	public function deleteByPattern( string $dir, string $file_arg = '*.*' ): int {
		if ( $dir !== '' && $dir !== '/' ) {
			$count = 0;
			$files = @glob($dir . $file_arg, GLOB_MARK); // @phpcs:ignore
			// Glob can return FALSE on error
			if ( is_array($files) ) {
				foreach ( $files as $file ) {
					$this->delFile($file);
					++$count;
				}
			}
			return $count;
		}
		return 0;
	}

	public function rmdir( string $dir, bool $recursive = false, bool $force = false ): bool {
		global $wp_filesystem;
		if ( $this->inAllowedDirectory($dir) ) {
			if ( $force ) {
				$this->recursiveRmdir($dir);
				return true;
			}

			// Did it fail?
			if ( !$this->initialized(false, 'rmdir') ) {
				// $recursive is not supported in the default php rmdir function
				return rmdir($dir); // @phpcs:ignore
			}

			$wp_filesystem->rmdir($dir, $recursive);
		} else {
			return false;
		}

		return false;
	}

	/**
	 * @param string $path
	 * @return void
	 * @throws InvalidArgumentException
	 */
	public function recursiveRmdir( string $path ): void {
		if ( !is_dir($path) ) {
			return;
		}
		if ( !str_ends_with($path, '/') ) {
			$path .= '/';
		}
		$files = glob($path . '*', GLOB_MARK);
		if ( $files === false ) {
			return;
		}
		foreach ( $files as $file ) {
			if ( is_dir($file) ) {
				$this->recursiveRmdir($file);
			} else {
				unlink($file); // @phpcs:ignore
			}
		}
		rmdir($path); // @phpcs:ignore
	}

	/**
	 * Lists directories, excluding WP system, plugin and theme directories
	 *
	 * @param string $path
	 * @param int    $limit
	 * @return string[]
	 * @throws Exception
	 */
	public function safeListDirectories( string $path, int $limit = 5000 ): array {
		$unsafe_directories = apply_filters(
			'asp/utils/filemanager/unsafe_directories',
			array(
				'node_modules',
				'vendor',
				'cache',
				'.git',
				'_phpstorm',
				'themes',
				'wp-includes',
				'wp-admin',
				'plugins',
				'mu-plugins',
				'et-cache',
				'wpml',
				'wflogs',
				'w3tc-config',
				'config',
				'elementor',
				'wpallimport',
				'buddypress',
				'asp_upload',
				'AAPL',
				'sucuri',
			)
		);
		$directories        = $this->listDirectoriesRecursively( $path, $unsafe_directories );

		sort($directories);

		return array_slice(
			array_values(
				array_map(
					function ( $directory ) {
						return str_replace(ABSPATH, '', rtrim($directory, DIRECTORY_SEPARATOR));
					},
					$directories
				)
			),
			0,
			$limit
		);
	}


	/**
	 * A very fast recursive directory listing method
	 *
	 * Much faster then using DirectoryIterators
	 *
	 * @param string   $path
	 * @param string[] $exclude_dirs
	 * @return string[]
	 */
	function listDirectoriesRecursively( string $path = '', $exclude_dirs = array() ): array {
		$directories = array();
		$paths       = glob($path . '*', GLOB_MARK | GLOB_ONLYDIR | GLOB_NOSORT);
		foreach ( $paths as $path ) {
			// Skip excluded directories
			$dir_name = basename(rtrim($path, DIRECTORY_SEPARATOR));
			if ( in_array($dir_name, $exclude_dirs, true) ) {
				continue;
			}
			$directories[] = $path;
			$directories   = array_merge($directories, $this->listDirectoriesRecursively($path, $exclude_dirs));
		}
		return $directories;
	}

	/**
	 * Gets all unique subdirectories from the path
	 *
	 * @param string $path
	 * @return string[]
	 */
	public function listDirectoriesRecursively2( string $path ): array {
		$path        = trailingslashit($path);
		$directories = array();
		$iterator    = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
			RecursiveIteratorIterator::SELF_FIRST,
		);

		foreach ( $iterator as $file ) {
			if ( $file->isDir() ) {
				$dir_path = $file->getRealpath();
				if ( !in_array($dir_path, $directories, true ) ) {
					$directories[] = $dir_path;
				}
			}
		}

		// $directories = array_unique($directories);

		return $directories;
	}

	/**
	 * @return bool
	 */
	public function createRequiredDirectories(): bool {
		foreach ( $this->allowed_directories as $directory ) {
			if ( !is_dir($directory) ) {
				if ( !wp_mkdir_p($directory) ) {
					@mkdir($directory, 0755, true); // @phpcs:ignore
				}
			}
		}

		return true;
	}

	/**
	 * @return void
	 */
	public function removeRequiredDirectories(): void {
		foreach ( $this->allowed_directories as $directory ) {
			if ( $this->pathSafetyCheck($directory) && $this->isDir($directory) ) {
				$this->rmdir( $directory  );
				if ( $this->isDir( $directory ) ) { // @phpstan-ignore-line
					$this->rmdir( $directory, true);
					if ( $this->isDir( $directory ) ) { // @phpstan-ignore-line
						// Last attempt, with force
						$this->rmdir( $directory, true, true);
					}
				}
			}
		}
	}

	private function pathSafetyCheck( string $path ): bool {
		if (
			$path !== '' &&
			$path !== '/' &&
			$path !== './' &&
			str_replace('/', '', get_home_path()) !== str_replace('/', '', $path) &&
			strpos($path, 'wp-content') > 5 &&
			strpos($path, 'plugins') === false &&
			strpos($path, 'wp-includes') === false &&
			strpos($path, 'wp-admin') === false &&
			is_dir( $path )
		) {
			return true;
		}

		return false;
	}

	/**
	 * @param string $path
	 * @return bool
	 */
	private function inAllowedDirectory( string $path ): bool {
		foreach ( $this->allowed_directories as $directory ) {
			if ( strpos($path, $directory) !== false ) {
				return true;
			}
		}
		return false;
	}
}
