<?php
/**
 * RewardsWP logger implementation.
 *
 * @package Awesomemotive\Rewardswp\Services
 * @since 1.1.0
 */

namespace Awesomemotive\Rewardswp\Services;

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

use Awesomemotive\Rewardswp\Interfaces\LogWriterInterface;
use Awesomemotive\Rewardswp\Interfaces\LoggerInterface;
use Awesomemotive\Rewardswp\Logging\Writers\ErrorLogWriter;
use Awesomemotive\Rewardswp\Logging\Writers\FileLogWriter;

/**
 * Logger service responsible for dispatching log messages to configured writers.
 *
 * @since 1.1.0
 */
class LoggerService implements LoggerInterface {
	public const LEVEL_EMERGENCY = 'emergency';
	public const LEVEL_ALERT     = 'alert';
	public const LEVEL_CRITICAL  = 'critical';
	public const LEVEL_ERROR     = 'error';
	public const LEVEL_WARNING   = 'warning';
	public const LEVEL_NOTICE    = 'notice';
	public const LEVEL_INFO      = 'info';
	public const LEVEL_DEBUG     = 'debug';

	/**
	 * Default minimum level.
	 *
	 * @since 1.1.0
	 */
	private const DEFAULT_MINIMUM_LEVEL = self::LEVEL_WARNING;

	/**
	 * Get the minimum log level based on WordPress debug constants.
	 *
	 * @since 1.1.0
	 *
	 * @return string Minimum log level.
	 */
	private static function get_minimum_level_from_wp_debug(): string {
		// If WP_DEBUG is enabled, allow all log levels including debug.
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			return self::LEVEL_DEBUG;
		}

		// If WP_DEBUG_LOG is enabled but WP_DEBUG is not, allow info level.
		if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
			return self::LEVEL_INFO;
		}

		// Production: only log warnings and above.
		return self::DEFAULT_MINIMUM_LEVEL;
	}

	/**
	 * Severity map in descending order.
	 *
	 * @since 1.1.0
	 * @var array<string,int>
	 */
	/**
	 * Logging severity map ordered by priority.
	 *
	 * @since 1.1.0
	 *
	 * @var array<string,int>
	 */
	private array $severity_map = [
		self::LEVEL_EMERGENCY => 0,
		self::LEVEL_ALERT     => 1,
		self::LEVEL_CRITICAL  => 2,
		self::LEVEL_ERROR     => 3,
		self::LEVEL_WARNING   => 4,
		self::LEVEL_NOTICE    => 5,
		self::LEVEL_INFO      => 6,
		self::LEVEL_DEBUG     => 7,
	];

	/**
	 * Registered log writers.
	 *
	 * @since 1.1.0
	 *
	 * @var LogWriterInterface[]
	 */
	private array $writers = [];

	/**
	 * Minimum log level that should be persisted.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	private string $minimum_level;

	/**
	 * LoggerService constructor.
	 *
	 * @since 1.1.0
	 *
	 * @param LogWriterInterface[]|null $writers       Optional array of log writers.
	 * @param string|null               $minimum_level Minimum log level. If null, automatically determined from WP_DEBUG constants.
	 */
	public function __construct( ?array $writers = null, ?string $minimum_level = null ) {
		if ( empty( $writers ) ) {
			$writers = [
				new ErrorLogWriter(),
			];

			// Only add FileLogWriter if WP_DEBUG_LOG is enabled.
			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
				$writers[] = new FileLogWriter();
			}
		}

		$this->writers = array_values( array_filter( $writers, [ $this, 'filter_writers' ] ) );

		// If no minimum level provided, determine from WordPress debug constants.
		if ( null === $minimum_level ) {
			$minimum_level = self::get_minimum_level_from_wp_debug();
		}

		$this->minimum_level = $this->is_valid_level( $minimum_level ) ? $minimum_level : self::DEFAULT_MINIMUM_LEVEL;
	}

	/**
	 * Log a message at a given level.
	 *
	 * @since 1.1.0
	 *
	 * @param string $level   Log level.
	 * @param string $message Log message.
	 * @param array  $context Contextual data.
	 */
	public function log( string $level, string $message, array $context = [] ): void {
		if ( ! $this->is_valid_level( $level ) ) {
			return;
		}

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

		$normalized_context = $this->normalize_context( $context );
		$interpolated       = $this->interpolate_message( $message, $normalized_context );

		foreach ( $this->writers as $writer ) {
			try {
				$writer->write( $level, $interpolated, $normalized_context );
			} catch ( \Throwable $throwable ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
				// Swallow exceptions from writers to avoid breaking runtime execution.
			}
		}
	}

	/**
	 * Log an emergency level message.
	 *
	 * @since 1.1.0
	 *
	 * @param string $message Log message.
	 * @param array  $context Contextual data.
	 */
	public function emergency( string $message, array $context = [] ): void {
		$this->log( self::LEVEL_EMERGENCY, $message, $context );
	}

	/**
	 * Log an alert level message.
	 *
	 * @since 1.1.0
	 *
	 * @param string $message Log message.
	 * @param array  $context Contextual data.
	 */
	public function alert( string $message, array $context = [] ): void {
		$this->log( self::LEVEL_ALERT, $message, $context );
	}

	/**
	 * Log a critical level message.
	 *
	 * @since 1.1.0
	 *
	 * @param string $message Log message.
	 * @param array  $context Contextual data.
	 */
	public function critical( string $message, array $context = [] ): void {
		$this->log( self::LEVEL_CRITICAL, $message, $context );
	}

	/**
	 * Log an error level message.
	 *
	 * @since 1.1.0
	 *
	 * @param string $message Log message.
	 * @param array  $context Contextual data.
	 */
	public function error( string $message, array $context = [] ): void {
		$this->log( self::LEVEL_ERROR, $message, $context );
	}

	/**
	 * Log a warning level message.
	 *
	 * @since 1.1.0
	 *
	 * @param string $message Log message.
	 * @param array  $context Contextual data.
	 */
	public function warning( string $message, array $context = [] ): void {
		$this->log( self::LEVEL_WARNING, $message, $context );
	}

	/**
	 * Log a notice level message.
	 *
	 * @since 1.1.0
	 *
	 * @param string $message Log message.
	 * @param array  $context Contextual data.
	 */
	public function notice( string $message, array $context = [] ): void {
		$this->log( self::LEVEL_NOTICE, $message, $context );
	}

	/**
	 * Log an informational level message.
	 *
	 * @since 1.1.0
	 *
	 * @param string $message Log message.
	 * @param array  $context Contextual data.
	 */
	public function info( string $message, array $context = [] ): void {
		$this->log( self::LEVEL_INFO, $message, $context );
	}

	/**
	 * Log a debug level message.
	 *
	 * @since 1.1.0
	 *
	 * @param string $message Log message.
	 * @param array  $context Contextual data.
	 */
	public function debug( string $message, array $context = [] ): void {
		$this->log( self::LEVEL_DEBUG, $message, $context );
	}

	/**
	 * Validate writer instances.
	 *
	 * @since 1.1.0
	 *
	 * @param mixed $writer Writer candidate.
	 *
	 * @return bool
	 */
	private function filter_writers( $writer ): bool {
		return $writer instanceof LogWriterInterface;
	}

	/**
	 * Determine if the level is valid.
	 *
	 * @since 1.1.0
	 *
	 * @param string $level Log level to validate.
	 *
	 * @return bool
	 */
	private function is_valid_level( string $level ): bool {
		return isset( $this->severity_map[ $level ] );
	}

	/**
	 * Compare level against minimum threshold.
	 *
	 * @since 1.1.0
	 *
	 * @param string $level Log level.
	 *
	 * @return bool
	 */
	private function should_log( string $level ): bool {
		return $this->severity_map[ $level ] <= $this->severity_map[ $this->minimum_level ];
	}

	/**
	 * Interpolate context values into the message placeholders.
	 *
	 * @since 1.1.0
	 *
	 * @param string $message Log message.
	 * @param array  $context Contextual data.
	 *
	 * @return string
	 */
	private function interpolate_message( string $message, array $context ): string {
		if ( empty( $context ) ) {
			return $message;
		}

		$replace = [];

		foreach ( $context as $key => $value ) {
			$replace[ '{' . $key . '}' ] = $value;
		}

		return strtr( $message, $replace );
	}

	/**
	 * Normalize context values for safe logging.
	 *
	 * @since 1.1.0
	 *
	 * @param array $context Raw context data.
	 *
	 * @return array
	 */
	private function normalize_context( array $context ): array {
		$normalized = [];

		foreach ( $context as $key => $value ) {
			$normalized[ $key ] = $this->normalize_value( $key, $value );
		}

		return $normalized;
	}

	/**
	 * Normalize an individual context value.
	 *
	 * @since 1.1.0
	 *
	 * @param string $key   Context key.
	 * @param mixed  $value Context value.
	 *
	 * @return string
	 */
	private function normalize_value( string $key, $value ): string {
		if ( $this->is_sensitive_key( $key ) ) {
			return '[redacted]';
		}

		if ( is_scalar( $value ) || ( is_object( $value ) && method_exists( $value, '__toString' ) ) ) {
			return (string) $value;
		}

		return wp_json_encode( $value, JSON_PARTIAL_OUTPUT_ON_ERROR );
	}

	/**
	 * Determine whether the context key likely contains sensitive data.
	 *
	 * @since 1.1.0
	 *
	 * @param string $key Context key.
	 *
	 * @return bool
	 */
	private function is_sensitive_key( string $key ): bool {
		$sensitive_fragments = [
			'password',
			'secret',
			'token',
			'key',
			'nonce',
		];

		$key = strtolower( $key );

		foreach ( $sensitive_fragments as $fragment ) {
			if ( false !== strpos( $key, $fragment ) ) {
				return true;
			}
		}

		return false;
	}
}
