<?php
/**
 * WP Multisite WaaS Logger
 *
 * Log string messages to a file with a timestamp. Useful for debugging.
 *
 * @package WP_Ultimo
 * @subpackage Logger
 * @since 2.0.0
 */

namespace WP_Ultimo;

// Exit if accessed directly
defined('ABSPATH') || exit;

use Psr\Log\AbstractLogger;
use Psr\Log\LogLevel;

/**
 * WP Multisite WaaS Logger
 *
 * @since 2.0.0
 */
class Logger extends AbstractLogger {

	use \WP_Ultimo\Traits\Singleton;

	/**
	 * Holds the log file path.
	 *
	 * @since 2.1
	 * @var string
	 */
	protected $log_file = '';

	/**
	 * Returns the logs folder
	 *
	 * @return string
	 */
	public static function get_logs_folder() {

		return wu_maybe_create_folder('wu-logs');
	}

	/**
	 * Add a log entry to chosen file.
	 *
	 * @param string           $handle Name of the log file to write to.
	 * @param string|\WP_Error $message Log message to write.
	 * @param string           $log_level Log level to write.
	 */
	public static function add($handle, $message, $log_level = LogLevel::INFO): void {

		$allowed_log_level = wu_get_setting('error_logging_level', 'default');

		if ('disabled' === $allowed_log_level) {
			return;
		}

		if ('default' === $allowed_log_level) {
			/**
			 * Get from default php reporting level
			 *
			 * Here we are converting the PHP error reporting level to the PSR-3 log level.
			 */
			$reporting_level = error_reporting();

			$psr_log_levels = [
				E_ERROR             => LogLevel::ERROR,
				E_WARNING           => LogLevel::WARNING,
				E_PARSE             => LogLevel::ERROR,
				E_NOTICE            => LogLevel::NOTICE,
				E_CORE_ERROR        => LogLevel::CRITICAL,
				E_CORE_WARNING      => LogLevel::WARNING,
				E_COMPILE_ERROR     => LogLevel::ALERT,
				E_COMPILE_WARNING   => LogLevel::WARNING,
				E_USER_ERROR        => LogLevel::ERROR,
				E_USER_WARNING      => LogLevel::WARNING,
				E_USER_NOTICE       => LogLevel::NOTICE,
				E_STRICT            => LogLevel::DEBUG,
				E_RECOVERABLE_ERROR => LogLevel::ERROR,
				E_DEPRECATED        => LogLevel::NOTICE,
				E_USER_DEPRECATED   => LogLevel::NOTICE,
			];

			$current_log_levels = [];

			foreach ($psr_log_levels as $php_level => $psr_level) {
				if ($reporting_level & $php_level) {
					$current_log_levels[] = $psr_level;
				}
			}

			if ( ! in_array($log_level, $current_log_levels, true) && ($reporting_level & ~E_ALL)) {
				return;
			}
		} elseif ('errors' === $allowed_log_level && LogLevel::ERROR !== $log_level && LogLevel::CRITICAL !== $log_level) {
			return;
		}

		$instance = self::get_instance();

		$instance->set_log_file(self::get_logs_folder() . "/$handle.log");

		if (is_wp_error($message)) {
			$message = $message->get_error_message();
		}

		$instance->log($log_level, $message);

		do_action('wu_log_add', $handle, $message);
	}

	/**
	 * Get the log contents
	 *
	 * @since  1.6.0
	 *
	 * @param  string  $handle File name to read.
	 * @param  integer $lines Number of lines to retrieve, defaults to 10.
	 * @return array
	 */
	public static function read_lines($handle, $lines = 10) {

		$file = self::get_logs_folder() . "/$handle.log";

		if ( ! file_exists($file)) {
			return [];
		}

		// read file
		$content = file_get_contents($file);

		// split into lines
		$arr_content = explode(PHP_EOL, $content);

		// remove last line if empty
		if (empty(end($arr_content))) {
			array_pop($arr_content);
		}

		// return last lines
		return array_slice($arr_content, -$lines);
	}

	/**
	 * Clear entries from chosen file.
	 *
	 * @param mixed $handle Name of the log file to clear.
	 */
	public static function clear($handle): void {

		$file = self::get_logs_folder() . "/$handle.log";

		// Delete the file if it exists.
		if (file_exists($file)) {
			@unlink($file); // phpcs:ignore
		}

		do_action('wu_log_clear', $handle);
	}

	/**
	 * Takes a callable as a parameter and logs how much time it took to execute it.
	 *
	 * @since 2.0.0
	 *
	 * @param string   $handle Name of the log file to write to.
	 * @param string   $message  Log message to write.
	 * @param callable $callback Function to track the execution time.
	 * @return array
	 */
	public static function track_time($handle, $message, $callback) {

		$start = microtime(true);

		$return = call_user_func($callback);

		$time_elapsed = microtime(true) - $start;

		// translators: the placeholder %s will be replaced by the time in seconds (float).
		$message .= ' - ' . sprintf(__('This action took %s seconds.', 'wp-ultimo'), $time_elapsed);

		self::add($handle, $message);

		return $return;
	}

	/**
	 * Set the log file path.
	 *
	 * @since 2.1
	 *
	 * @param string $log_file The log file path.
	 */
	public function set_log_file($log_file): void {

		$this->log_file = $log_file;
	}

	/**
	 * Logs with an arbitrary level.
	 *
	 * @since 2.1
	 *
	 * @param mixed   $level   The log level.
	 * @param string  $message The message to log.
	 * @param mixed[] $context The context.
	 *
	 * @return void
	 */
	public function log($level, $message, array $context = []): void {

		if ( ! $this->is_valid_log_level($level) ) {
			return;
		}

		$formatted_message = $this->format_message($level, $message, $context);

		$this->write_to_file($formatted_message);
	}

	/**
	 * Check if the log level is valid.
	 *
	 * @since 2.1
	 *
	 * @param string $level The log level to check.
	 */
	protected function is_valid_log_level($level): bool {

		$valid_log_levels = [
			LogLevel::EMERGENCY,
			LogLevel::ALERT,
			LogLevel::CRITICAL,
			LogLevel::ERROR,
			LogLevel::WARNING,
			LogLevel::NOTICE,
			LogLevel::INFO,
			LogLevel::DEBUG,
		];

		return in_array($level, $valid_log_levels, true);
	}

	/**
	 * Format the message to be logged.
	 *
	 * @since 2.1
	 *
	 * @param string $level The log level.
	 * @param string $message The message to log.
	 * @param array  $context The context of the message.
	 * @return string
	 */
	protected function format_message($level, $message, $context = []) {

		$date = new \DateTime();

		$formatted_message = sprintf(
			'[%s] [%s] %s' . PHP_EOL,
			$date->format('Y-m-d H:i:s'),
			strtoupper($level),
			$message
		);

		return $formatted_message;
	}

	/**
	 * Write the message to the log file.
	 *
	 * @since 2.1
	 *
	 * @param string $message The message to log.
	 * @return void
	 */
	protected function write_to_file($message) {

		if ( ! file_exists($this->log_file)) {
			touch($this->log_file);
		}

		if ( ! is_writable($this->log_file)) {
			return;
		}

		file_put_contents($this->log_file, $message, FILE_APPEND | LOCK_EX);
	}
}