<?php
/**
 * Centralized plugin logger with environment gating and redaction.
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 * @since      0.14.253
 */

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

/**
 * Centralized logger for SiteStaffr.
 *
 * - `warning` and `error` logs are emitted.
 * - `debug` and `info` logs are suppressed in production builds.
 * - Sensitive content is redacted before writing to logs.
 */
class SiteStaffr_Logger {

	/**
	 * Log level constants.
	 */
	const LEVEL_DEBUG   = 'debug';
	const LEVEL_INFO    = 'info';
	const LEVEL_WARNING = 'warning';
	const LEVEL_ERROR   = 'error';

	/**
	 * Sensitive key fragments used for context redaction.
	 *
	 * @var string[]
	 */
	private static $sensitive_key_fragments = array(
		'token',
		'secret',
		'signature',
		'authorization',
		'password',
		'nonce',
		'api_key',
		'api_secret',
		'site_token',
		'payload',
		'request',
		'response',
		'transcript',
		'message',
		'conversation',
		'description',
		'business_context',
		'visitor_ip',
		'ip_address',
		'x_forwarded_for',
	);

	/**
	 * Backward-compatible logging adapter for legacy `error_log` call sites.
	 *
	 * Automatically derives a level from message content.
	 *
	 * @param mixed $message Message payload.
	 * @param array $context Optional structured context.
	 * @return void
	 */
	public static function legacy( $message, $context = array() ) {
		$level = self::classify_legacy_level( $message );
		self::log( $level, $message, $context );
	}

	/**
	 * Log a debug message.
	 *
	 * @param mixed $message Message payload.
	 * @param array $context Optional structured context.
	 * @return void
	 */
	public static function debug( $message, $context = array() ) {
		self::log( self::LEVEL_DEBUG, $message, $context );
	}

	/**
	 * Log an info message.
	 *
	 * @param mixed $message Message payload.
	 * @param array $context Optional structured context.
	 * @return void
	 */
	public static function info( $message, $context = array() ) {
		self::log( self::LEVEL_INFO, $message, $context );
	}

	/**
	 * Log a warning message.
	 *
	 * @param mixed $message Message payload.
	 * @param array $context Optional structured context.
	 * @return void
	 */
	public static function warning( $message, $context = array() ) {
		self::log( self::LEVEL_WARNING, $message, $context );
	}

	/**
	 * Log an error message.
	 *
	 * @param mixed $message Message payload.
	 * @param array $context Optional structured context.
	 * @return void
	 */
	public static function error( $message, $context = array() ) {
		self::log( self::LEVEL_ERROR, $message, $context );
	}

	/**
	 * Emit a log entry if enabled for the requested level.
	 *
	 * @param string $level   Log level.
	 * @param mixed  $message Message payload.
	 * @param array  $context Optional structured context.
	 * @return void
	 */
	public static function log( $level, $message, $context = array() ) {
		$normalized_level = self::normalize_level( $level );
		if ( ! self::should_log( $normalized_level ) ) {
			return;
		}

		$sanitized_message = self::redact_message( (string) $message );
		$line = sprintf(
			'SiteStaffr [%s] %s',
			strtoupper( $normalized_level ),
			$sanitized_message
		);

		if ( ! empty( $context ) ) {
			$sanitized_context = self::sanitize_context_value( $context );
			$encoded_context = wp_json_encode( $sanitized_context );
			if ( false !== $encoded_context ) {
				$line .= ' | context=' . $encoded_context;
			}
		}

		error_log( $line ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- centralized logger sink.
	}

	/**
	 * Determine whether logs should be emitted for a level.
	 *
	 * @param string $level Normalized log level.
	 * @return bool
	 */
	private static function should_log( $level ) {
		return in_array( $level, array( self::LEVEL_WARNING, self::LEVEL_ERROR ), true );
	}

	/**
	 * Normalize level to supported values.
	 *
	 * @param string $level Requested level.
	 * @return string
	 */
	private static function normalize_level( $level ) {
		$level = strtolower( (string) $level );
		if ( in_array( $level, array( self::LEVEL_DEBUG, self::LEVEL_INFO, self::LEVEL_WARNING, self::LEVEL_ERROR ), true ) ) {
			return $level;
		}
		return self::LEVEL_INFO;
	}

	/**
	 * Derive a severity for legacy string logs.
	 *
	 * @param mixed $message Legacy message payload.
	 * @return string
	 */
	private static function classify_legacy_level( $message ) {
		$text = strtolower( (string) $message );

		if ( preg_match( '/\b(fatal|panic|uncaught)\b/', $text ) ) {
			return self::LEVEL_ERROR;
		}

		if ( preg_match( '/\b(error|failed|invalid|exception|denied|missing|rejected|blocked|unavailable|timeout|exceeded)\b/', $text ) ) {
			return self::LEVEL_WARNING;
		}

		return self::LEVEL_DEBUG;
	}

	/**
	 * Redact sensitive fragments from a free-form log message.
	 *
	 * @param string $message Raw message.
	 * @return string
	 */
	private static function redact_message( $message ) {
		$message = trim( (string) $message );
		if ( '' === $message ) {
			return '';
		}

		// Replace obvious payload/body dumps.
		$message = preg_replace( '/(Response Body:\s*).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(Request was:\s*).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(Latest Message:\s*).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(Description received[^:]*:\s*).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(Verification - Description[^:]*:\s*).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(Hours received:\s*).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(Verification - Hours:\s*).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(Business phone preserved\/saved:\s*).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(Business name in saved settings:\s*).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(Business name we tried to save:\s*).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(wpdb last query:\s*).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(Middleware returned \d+ - ).*/i', '$1[redacted]', $message );
		$message = preg_replace( '/(\[\d+\]\s*(?:user|assistant|system|ai|caller|visitor)\s*:\s*).*/i', '$1[redacted]', $message );

		// Redact known key/value secret-like fragments.
		$message = preg_replace(
			'/((?:api|site|auth|bearer|x-sitestaffr)[-_ ]?(?:token|secret|signature)|nonce)\s*[:=]\s*([^\s,;]+)/i',
			'$1=[redacted]',
			$message
		);

		// Redact token-like strings (including JWT-like values).
		$message = preg_replace( '/\beyJ[A-Za-z0-9._-]{10,}\b/', '[redacted-token]', $message );
		$message = preg_replace( '/\b[A-Fa-f0-9]{32,}\b/', '[redacted-token]', $message );

		// Redact IPv4 and IPv6 addresses.
		$message = preg_replace( '/\b\d{1,3}(?:\.\d{1,3}){3}\b/', '[redacted-ip]', $message );
		$message = preg_replace( '/\b(?:[A-F0-9]{1,4}:){2,}[A-F0-9:]{2,}\b/i', '[redacted-ipv6]', $message );

		return $message;
	}

	/**
	 * Recursively sanitize context values.
	 *
	 * @param mixed  $value Context value.
	 * @param string $key   Context key.
	 * @param int    $depth Current recursion depth.
	 * @return mixed
	 */
	private static function sanitize_context_value( $value, $key = '', $depth = 0 ) {
		if ( $depth > 4 ) {
			return '[truncated]';
		}

		if ( self::is_sensitive_key( $key ) ) {
			return '[redacted]';
		}

		if ( is_array( $value ) ) {
			$sanitized = array();
			foreach ( $value as $child_key => $child_value ) {
				$sanitized[ $child_key ] = self::sanitize_context_value(
					$child_value,
					(string) $child_key,
					$depth + 1
				);
			}
			return $sanitized;
		}

		if ( is_object( $value ) ) {
			$object_vars = get_object_vars( $value );
			return self::sanitize_context_value( $object_vars, $key, $depth + 1 );
		}

		if ( is_string( $value ) ) {
			return self::redact_message( $value );
		}

		if ( is_scalar( $value ) || null === $value ) {
			return $value;
		}

		return '[unserializable]';
	}

	/**
	 * Check if a key should be redacted.
	 *
	 * @param string $key Context key.
	 * @return bool
	 */
	private static function is_sensitive_key( $key ) {
		$normalized_key = strtolower( (string) $key );
		if ( '' === $normalized_key ) {
			return false;
		}

		foreach ( self::$sensitive_key_fragments as $fragment ) {
			if ( false !== strpos( $normalized_key, $fragment ) ) {
				return true;
			}
		}

		return false;
	}
}
