<?php
/**
 * Monolog Stream Logger
 *
 * @package northmill-online-shared
 *
 * @phpcs:disable Squiz.Commenting.FunctionComment.MissingParamTag
 */

namespace Northmill_Online_Shared\Modules\Loggers;

use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
use Northmill_Online_Shared\Helpers\General_Helpers;
use Northmill_Online_Shared\Abstract_Shared_Base;
use Throwable;

/**
 * Class Monolog_Stream_Logger
 */
class Monolog_Stream_Logger extends Abstract_Logger {

	/**
	 * The directory where the logs are stored.
	 *
	 * @var string
	 */
	private string $log_dir;

	/**
	 * Whether the log directory exists.
	 *
	 * @var bool
	 */
	private bool $dir_exists = false;

	/**
	 * Monolog_Stream_Logger constructor.
	 *
	 * @param Abstract_Shared_Base $shared Shared instance.
	 */
	public function __construct( Abstract_Shared_Base $shared, bool $enabled = false ) {
		parent::__construct( $shared, $enabled );

		$constant = General_Helpers::get_constant( 'NM_LOG_DIR', '' );

		if ( is_dir( $constant ) ) {
			$this->update_log_dir( $constant );
		} else {
			$this->update_log_dir( '/var/log/apache2/wp_monolog/' );
		}
	}

	/**
	 * Sets the directory where the logs are stored.
	 *
	 * @param string $dir The directory where the logs are stored.
	 *
	 * @return void
	 */
	public function update_log_dir( string $dir ): void {
		$dir = General_Helpers::ensure_trailing_slash( $dir );

		try {
			if ( is_dir( $dir ) ) {
				$this->dir_exists = true;
			}
		} catch ( Throwable ) {} // phpcs:ignore

		$this->log_dir = $dir;
	}

	/**
	 * Maybe create the log directory.
	 *
	 * @return bool Whether the log directory EXISTS after this, regardless of whether it was created or not.
	 */
	private function maybe_create_log_dir(): bool {
		if ( $this->dir_exists ) {
			return true;
		}

		$created = wp_mkdir_p( $this->log_dir );

		if ( $created ) {
			$this->dir_exists = true;
		} else {
			$this->log_internal_error(
				'Failed to create log directory.',
				array( 'dir' => $this->log_dir )
			);
		}

		return $this->dir_exists;
	}


	/**
	 * {@inheritDoc}
	 */
	public function log(
		string $channel,
		string $message,
		?array $context,
		Log_Level_Enum $level,
		bool $external_log = false
	): void {
		if ( ! $this->maybe_create_log_dir() ) {
			return;
		}

		if ( ! $context ) {
			$context = array();
		}

		$logger = new Logger( $channel );

		$custom_monolog_level = General_Helpers::get_constant( 'NM_CUSTOM_MONOLOG_LEVEL' );
		$log_level            = $custom_monolog_level ? Level::fromName( $custom_monolog_level ) : Level::Warning;

		// The minimum level defaults to Warning for Monolog.
		// We only want severe levels to be sent to Datadog.
		// Note that we already check the level in Northmill_Online_Shared\Modules\Logger->log(), before getting here.
		$log_stream = new StreamHandler(
			$this->log_dir . $this->shared->package_info->get_service_name() . '.log',
			$log_level
		);

		$log_stream->setFormatter( new JsonFormatter() );

		$logger->pushHandler( $log_stream );

		$processed_context = $this->get_context( $context );
		$extra             = $this->get_extra();

		$logger->pushProcessor(
			function ( $record ) use ( $processed_context, $extra ) {
				$record['extra'] = $extra;
				return $record;
			}
		);

		$log_level = Level::fromName( $level->value );

		try {
			$logger->log( $log_level, $message, $processed_context );
		} catch ( Throwable $t ) {
			// Try writing error to wp root.
			try {
				error_log( $t->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			} catch ( Throwable ) {} // phpcs:ignore
		}
	}

	/**
	 * Returns the context array.
	 *
	 * @param array $context The context to use as base.
	 *
	 * @return array The processed context array.
	 */
	private function get_context( array $context ): array {

		$wp_current_user = wp_get_current_user();

		if ( ! empty( $wp_current_user ) ) {
			$current_user = array(
				'ID'         => $wp_current_user->ID,
				'user_login' => $wp_current_user->user_login,
				'user_email' => $wp_current_user->user_email,
			);
		} else {
			$current_user = null;
		}

		$defaults = array(
			'current_user' => $current_user,
		);

		return array_merge( $defaults, $context );
	}

	/**
	 * Returns the extra information array.
	 *
	 * @return array The processed extra information array.
	 */
	private function get_extra(): array {
		return array(
			'service_name' => $this->shared->package_info->get_service_name(),
			'package'      => $this->shared->package_info->package_data->get_name(),
			'version'      => $this->shared->package_info->package_data->get_version(),
			'env'          => $this->shared->env,
			'site_url'     => $this->shared->package_info->paths->get_site_url(),
			'nm_hosting'   => $this->shared->nm_hosting,
			'wp_version'   => get_bloginfo( 'version' ),
			'wc_version'   => General_Helpers::get_constant( 'WC_VERSION' ),
		);
	}

	/**
	 * Log internal errors, after disabling the monolog stream handler.
	 *
	 * @param string         $message The message to log.
	 * @param array|null     $context Context.
	 * @param Log_Level_Enum $level   Log level.
	 *
	 * @return void
	 */
	private function log_internal_error(
		string $message,
		null|array $context = null,
		Log_Level_Enum $level = Log_Level_Enum::ERROR
	): void {
		$this->shared->logger->disable_handler( 'monolog_stream' );

		$this->shared->logger->log(
			'monolog-stream',
			$message,
			$context,
			$level
		);
	}
}
