<?php
/**
 * TutorHelper Bookings logger.
 *
 * @package TutorHelper_Bookings
 */

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

/**
 * Writes privacy-conscious TutorHelper Bookings diagnostic entries.
 */
final class TutorHelper_Logger {

	/**
	 * Maximum active log size before rotation.
	 *
	 * One mebibyte.
	 *
	 * @var int
	 */
	const MAX_FILE_SIZE = 1048576;

	/**
	 * Number of recent lines shown on the diagnostics page.
	 *
	 * @var int
	 */
	const DEFAULT_RECENT_LINES = 100;

	/**
	 * Write an informational entry.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional technical context.
	 *
	 * @return bool
	 */
	public static function info( $message, $context = array() ) {
		return self::log( 'info', $message, $context );
	}

	/**
	 * Write a success entry.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional technical context.
	 *
	 * @return bool
	 */
	public static function success( $message, $context = array() ) {
		return self::log( 'success', $message, $context );
	}

	/**
	 * Write a warning entry.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional technical context.
	 *
	 * @return bool
	 */
	public static function warning( $message, $context = array() ) {
		return self::log( 'warning', $message, $context );
	}

	/**
	 * Write an error entry.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional technical context.
	 *
	 * @return bool
	 */
	public static function error( $message, $context = array() ) {
		return self::log( 'error', $message, $context );
	}

	/**
	 * Write an entry to the TutorHelper log.
	 *
	 * @param string $level   Log level.
	 * @param string $message Log message.
	 * @param array  $context Optional technical context.
	 *
	 * @return bool
	 */
	public static function log( $level, $message, $context = array() ) {
		$allowed_levels = array(
			'info',
			'success',
			'warning',
			'error',
		);

		$level = sanitize_key( $level );

		if ( ! in_array( $level, $allowed_levels, true ) ) {
			$level = 'info';
		}

		$message = sanitize_text_field( $message );
		$context = self::sanitize_context( $context );

		if ( empty( $message ) ) {
			return false;
		}

		if ( ! self::prepare_log_directory() ) {
			return false;
		}

		self::rotate_if_required();

		$entry = array(
			'timestamp' => gmdate( 'Y-m-d H:i:s' ),
			'level'     => $level,
			'message'   => $message,
			'context'   => $context,
		);

		$encoded_entry = wp_json_encode(
			$entry,
			JSON_UNESCAPED_SLASHES
		);

		if ( false === $encoded_entry ) {
			return false;
		}

		$log_file = self::get_log_file_path();

		if ( empty( $log_file ) ) {
			return false;
		}

		$result = file_put_contents(
			$log_file,
			$encoded_entry . PHP_EOL,
			FILE_APPEND | LOCK_EX
		);

		return false !== $result;
	}

	/**
	 * Return the active log file path.
	 *
	 * @return string
	 */
	public static function get_log_file_path() {
		$directory = self::get_log_directory();

		if ( empty( $directory ) ) {
			return '';
		}

		$hash = substr(
			hash(
				'sha256',
				wp_salt( 'auth' )
				. '|tutorhelper-bookings-log'
			),
			0,
			32
		);

		return trailingslashit( $directory )
			. 'tutorhelper-bookings-'
			. $hash
			. '.log';
	}

	/**
	 * Return the rotated log path.
	 *
	 * @return string
	 */
	public static function get_rotated_log_file_path() {
		$log_file = self::get_log_file_path();

		return empty( $log_file )
			? ''
			: $log_file . '.1';
	}

	/**
	 * Return the protected log directory.
	 *
	 * Plugin-generated files are stored under the WordPress uploads directory
	 * rather than directly in wp-content or the plugin directory.
	 *
	 * @return string
	 */
	public static function get_log_directory() {
		$uploads = wp_upload_dir();

		if (
			! empty( $uploads['error'] )
			|| empty( $uploads['basedir'] )
		) {
			return '';
		}

		return trailingslashit( $uploads['basedir'] )
			. 'tutorhelper-bookings/logs';
	}

	/**
	 * Check whether the log is currently writable.
	 *
	 * @return bool
	 */
	public static function is_writable() {
		if ( ! self::prepare_log_directory() ) {
			return false;
		}

		return wp_is_writable(
			self::get_log_directory()
		);
	}

	/**
	 * Return the active log size in bytes.
	 *
	 * @return int
	 */
	public static function get_log_size() {
		$file = self::get_log_file_path();

		if ( empty( $file ) || ! file_exists( $file ) ) {
			return 0;
		}

		$size = filesize( $file );

		return false === $size ? 0 : absint( $size );
	}

	/**
	 * Return the complete active log contents.
	 *
	 * @return string
	 */
	public static function get_log_contents() {
		$file = self::get_log_file_path();

		if ( empty( $file ) || ! file_exists( $file ) ) {
			return '';
		}

		$filesystem = self::get_filesystem();

		if ( ! $filesystem ) {
			return '';
		}

		$contents = $filesystem->get_contents(
			$file
		);

		return false === $contents
			? ''
			: $contents;
	}

	/**
	 * Return recent log entries.
	 *
	 * @param int $limit Maximum number of entries.
	 *
	 * @return array
	 */
	public static function get_recent_entries(
		$limit = self::DEFAULT_RECENT_LINES
	) {
		$file  = self::get_log_file_path();
		$limit = max( 1, min( 500, absint( $limit ) ) );

		if ( empty( $file ) || ! file_exists( $file ) ) {
			return array();
		}

		$lines = file(
			$file,
			FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES
		);

		if ( ! is_array( $lines ) ) {
			return array();
		}

		$lines   = array_slice( $lines, -$limit );
		$entries = array();

		foreach ( $lines as $line ) {
			$entry = json_decode( $line, true );

			if ( is_array( $entry ) ) {
				$entries[] = $entry;
			}
		}

		return array_reverse( $entries );
	}

	/**
	 * Clear active and rotated logs.
	 *
	 * @return bool
	 */
	public static function clear() {
		$success = true;

		$files = array(
			self::get_log_file_path(),
			self::get_rotated_log_file_path(),
		);

		foreach ( $files as $file ) {
			if (
				! empty( $file )
				&& file_exists( $file )
				&& ! wp_delete_file( $file )
			) {
				$success = false;
			}
		}

		return $success;
	}

	/**
	 * Initialise and return the WordPress filesystem.
	 *
	 * @return WP_Filesystem_Base|null
	 */
	private static function get_filesystem() {
		global $wp_filesystem;

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

		if (
			! WP_Filesystem()
			|| ! $wp_filesystem
		) {
			return null;
		}

		return $wp_filesystem;
	}

	/**
	 * Create the log directory and protective files.
	 *
	 * @return bool
	 */
	private static function prepare_log_directory() {
		$directory = self::get_log_directory();

		if ( empty( $directory ) ) {
			return false;
		}

		if (
			! is_dir( $directory )
			&& ! wp_mkdir_p( $directory )
		) {
			return false;
		}

		self::create_protection_file(
			trailingslashit( $directory ) . 'index.php',
			"<?php\n// Silence is golden.\n"
		);

		self::create_protection_file(
			trailingslashit( $directory ) . '.htaccess',
			"Require all denied\nDeny from all\n"
		);

		self::create_protection_file(
			trailingslashit( $directory ) . 'web.config',
			"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
			. "<configuration>\n"
			. "\t<system.webServer>\n"
			. "\t\t<authorization>\n"
			. "\t\t\t<deny users=\"*\" />\n"
			. "\t\t</authorization>\n"
			. "\t</system.webServer>\n"
			. "</configuration>\n"
		);

		return wp_is_writable( $directory );
	}

	/**
	 * Create a protective file if it does not already exist.
	 *
	 * @param string $path    File path.
	 * @param string $content File content.
	 *
	 * @return void
	 */
	private static function create_protection_file( $path, $content ) {
		if ( file_exists( $path ) ) {
			return;
		}

		file_put_contents(
			$path,
			$content,
			LOCK_EX
		);
	}

	/**
	 * Rotate the active log when it becomes too large.
	 *
	 * @return void
	 */
	private static function rotate_if_required() {
		$file = self::get_log_file_path();

		if (
			empty( $file )
			|| ! file_exists( $file )
			|| self::MAX_FILE_SIZE > self::get_log_size()
		) {
			return;
		}

		$rotated_file = self::get_rotated_log_file_path();

		if ( empty( $rotated_file ) ) {
			return;
		}

		if (
			file_exists( $rotated_file )
			&& ! wp_delete_file( $rotated_file )
		) {
			return;
		}

		$filesystem = self::get_filesystem();

		if ( ! $filesystem ) {
			return;
		}

		$filesystem->move(
			$file,
			$rotated_file,
			true
		);
	}

	/**
	 * Remove sensitive or unsuitable values from log context.
	 *
	 * @param mixed $context Context supplied by plugin code.
	 *
	 * @return array
	 */
	private static function sanitize_context( $context ) {
		if ( ! is_array( $context ) ) {
			return array();
		}

		$blocked_keys = array(
			'password',
			'pass',
			'nonce',
			'token',
			'secret',
			'api_key',
			'stripe_secret',
			'student_email',
			'email',
			'student_name',
			'name',
			'address',
			'phone',
		);

		$blocked_fragments = array(
			'password',
			'passphrase',
			'nonce',
			'token',
			'secret',
			'api_key',
			'email',
			'name',
			'address',
			'phone',
		);

		$clean_context = array();

		foreach ( $context as $key => $value ) {
			$clean_key = sanitize_key( $key );

			if (
				empty( $clean_key )
				|| in_array(
					$clean_key,
					$blocked_keys,
					true
				)
			) {
				continue;
			}

			$contains_blocked_fragment = false;

			foreach (
				$blocked_fragments
				as $blocked_fragment
			) {
				if (
					false !== strpos(
						$clean_key,
						$blocked_fragment
					)
				) {
					$contains_blocked_fragment = true;
					break;
				}
			}

			if ( $contains_blocked_fragment ) {
				continue;
			}

			if ( is_bool( $value ) ) {
				$clean_context[ $clean_key ] = $value;
				continue;
			}

			if ( is_int( $value ) || is_float( $value ) ) {
				$clean_context[ $clean_key ] = $value;
				continue;
			}

			if ( is_string( $value ) ) {
				$clean_context[ $clean_key ] = sanitize_text_field(
					$value
				);
			}
		}

		return $clean_context;
	}
}
