<?php
/**
 * Logging service using WooCommerce Logger.
 *
 * Supports 3-level logging (off/basic/detailed), recent blocks tracking,
 * block reason codes, and proxy-aware IP resolution.
 *
 * @package Carticy\CheckoutShield\Services
 */

declare(strict_types=1);

namespace Carticy\CheckoutShield\Services;

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

/**
 * Handles all logging for the plugin using WooCommerce Logger.
 *
 * Log levels:
 * - off:      No WC log writes. Still records blocks to transient for dashboard.
 * - basic:    Blocked requests only (one-line summary). Default.
 * - detailed: All requests with full context (IP, UA, email, payment method, signals).
 */
final class LoggingService {

	/**
	 * Log source identifier.
	 */
	private const SOURCE = 'carticy-checkout-shield';

	/**
	 * Transient key for recent blocked requests.
	 */
	private const RECENT_BLOCKS_KEY = 'ccs_recent_blocks';

	/**
	 * Max entries kept in recent blocks.
	 */
	private const RECENT_BLOCKS_MAX = 50;

	/**
	 * Transient TTL: 48 hours.
	 */
	private const RECENT_BLOCKS_TTL = 172800;

	/**
	 * Constructor.
	 */
	public function __construct() {
	}

	/**
	 * Log a detection event, respecting the configured log level.
	 *
	 * @param string $origin           Checkout origin (store_api, classic_ajax).
	 * @param bool   $validation_passed Whether the request passed validation.
	 * @param array  $context          Detection context signals.
	 * @param array  $checkout_data    Optional checkout-specific data (email, payment_method).
	 */
	public function log_detection( string $origin, bool $validation_passed, array $context = array(), array $checkout_data = array() ): void {
		$level  = $this->get_log_level();
		$status = $validation_passed ? 'passed' : 'blocked';

		// Off = no logging at all, but still record blocks for dashboard visibility.
		if ( 'off' === $level ) {
			if ( ! $validation_passed ) {
				$this->record_recent_block( $origin, $context, $checkout_data );
			}
			return;
		}

		// Basic = only log blocked requests, one-line summary.
		if ( 'basic' === $level ) {
			if ( ! $validation_passed ) {
				$this->log_basic_block( $origin, $context, $checkout_data );
				$this->record_recent_block( $origin, $context, $checkout_data );
			}
			return;
		}

		// Detailed = log everything with full context.
		$this->log_detailed( $origin, $status, $context, $checkout_data );

		if ( ! $validation_passed ) {
			$this->record_recent_block( $origin, $context, $checkout_data );
		}
	}

	/**
	 * Log a one-line summary for a blocked request (basic level).
	 *
	 * @param string $origin        Checkout origin.
	 * @param array  $context       Detection context.
	 * @param array  $checkout_data Checkout data.
	 */
	private function log_basic_block( string $origin, array $context, array $checkout_data ): void {
		$logger = $this->get_logger();
		if ( ! $logger ) {
			return;
		}

		$parts = array(
			'BLOCKED',
			$origin,
			'IP:' . $this->get_client_ip( false ),
		);

		if ( ! empty( $checkout_data['email'] ) ) {
			$parts[] = $checkout_data['email'];
		}
		if ( ! empty( $checkout_data['payment_method'] ) ) {
			$parts[] = 'pay:' . $checkout_data['payment_method'];
		}

		$reason = $this->get_block_reason( $context );
		if ( $reason ) {
			$parts[] = 'reason:' . $reason;
		}

		$logger->warning( implode( ' | ', $parts ), array( 'source' => self::SOURCE ) );
	}

	/**
	 * Log a detailed entry with full context (detailed level).
	 *
	 * @param string $origin        Checkout origin.
	 * @param string $status        Detection status.
	 * @param array  $context       Detection context.
	 * @param array  $checkout_data Checkout data.
	 */
	private function log_detailed( string $origin, string $status, array $context, array $checkout_data ): void {
		$logger = $this->get_logger();
		if ( ! $logger ) {
			return;
		}

		$message = sprintf(
			'%s | %s | IP:%s | UA:%s',
			strtoupper( $status ),
			$origin,
			$this->get_client_ip( false ),
			$this->get_user_agent()
		);

		if ( ! empty( $checkout_data['email'] ) ) {
			$message .= ' | email:' . $checkout_data['email'];
		}
		if ( ! empty( $checkout_data['payment_method'] ) ) {
			$message .= ' | pay:' . $checkout_data['payment_method'];
		}

		$log_context = array_merge(
			array( 'source' => self::SOURCE ),
			$context,
			$checkout_data
		);

		if ( 'passed' === $status ) {
			$logger->info( $message, $log_context );
		} else {
			$logger->warning( $message, $log_context );
		}
	}

	/**
	 * Record a blocked request to the recent-blocks transient for dashboard display.
	 *
	 * @param string $origin        Checkout origin.
	 * @param array  $context       Detection context.
	 * @param array  $checkout_data Checkout data.
	 */
	private function record_recent_block( string $origin, array $context, array $checkout_data ): void {
		$blocks = get_transient( self::RECENT_BLOCKS_KEY );
		if ( ! is_array( $blocks ) ) {
			$blocks = array();
		}

		$entry = array(
			'time'           => time(),
			'origin'         => $origin,
			'ip'             => $this->get_client_ip( true ),
			'email'          => $checkout_data['email'] ?? '',
			'payment_method' => $checkout_data['payment_method'] ?? '',
			'reason'         => $this->get_block_reason( $context ),
			'user_agent'     => $this->get_user_agent(),
			'has_cookie'     => $context['has_cookie'] ?? false,
			'has_submitted'  => $context['has_submitted'] ?? false,
			'has_session'    => $context['has_session'] ?? false,
			'referrer'       => $context['referrer'] ?? false,
		);

		// Prepend (newest first) and trim to max.
		array_unshift( $blocks, $entry );
		$blocks = array_slice( $blocks, 0, self::RECENT_BLOCKS_MAX );

		set_transient( self::RECENT_BLOCKS_KEY, $blocks, self::RECENT_BLOCKS_TTL );
	}

	/**
	 * Get recent blocked requests for dashboard display.
	 *
	 * @param int $limit Max entries to return.
	 * @return array
	 */
	public function get_recent_blocks( int $limit = 10 ): array {
		$blocks = get_transient( self::RECENT_BLOCKS_KEY );
		if ( ! is_array( $blocks ) ) {
			return array();
		}

		return array_slice( $blocks, 0, $limit );
	}

	/**
	 * Determine a human-readable block reason from detection context.
	 *
	 * @param array $context Detection context.
	 * @return string Block reason code.
	 */
	private function get_block_reason( array $context ): string {
		if ( ! empty( $context['strict_session_required'] ) ) {
			return 'no_session(strict)';
		}
		if ( empty( $context['has_cookie'] ) && empty( $context['has_submitted'] ) ) {
			return 'no_token';
		}
		if ( empty( $context['has_cookie'] ) ) {
			return 'no_cookie';
		}
		if ( empty( $context['has_submitted'] ) ) {
			return 'no_header';
		}
		if ( empty( $context['double_submit'] ) ) {
			return 'token_mismatch';
		}
		return 'unknown';
	}

	/**
	 * Log an info message.
	 *
	 * @param string $message The message to log.
	 * @param array  $context Additional context.
	 */
	public function info( string $message, array $context = array() ): void {
		if ( 'off' === $this->get_log_level() ) {
			return;
		}
		$logger = $this->get_logger();
		if ( $logger ) {
			$logger->info( $message, array_merge( array( 'source' => self::SOURCE ), $context ) );
		}
	}

	/**
	 * Log a warning message.
	 *
	 * @param string $message The message to log.
	 * @param array  $context Additional context.
	 */
	public function warning( string $message, array $context = array() ): void {
		if ( 'off' === $this->get_log_level() ) {
			return;
		}
		$logger = $this->get_logger();
		if ( $logger ) {
			$logger->warning( $message, array_merge( array( 'source' => self::SOURCE ), $context ) );
		}
	}

	/**
	 * Log an error message. Always writes regardless of log level.
	 *
	 * @param string $message The message to log.
	 * @param array  $context Additional context.
	 */
	public function error( string $message, array $context = array() ): void {
		$logger = $this->get_logger();
		if ( $logger ) {
			$logger->error( $message, array_merge( array( 'source' => self::SOURCE ), $context ) );
		}
	}

	/**
	 * Get the configured log level.
	 *
	 * @return string One of: off, basic, detailed.
	 */
	private function get_log_level(): string {
		$level = get_option( 'carticy_checkout_shield_log_level', 'basic' );
		return in_array( $level, array( 'off', 'basic', 'detailed' ), true ) ? $level : 'basic';
	}

	/**
	 * Get WooCommerce logger instance.
	 *
	 * @return \WC_Logger_Interface|null
	 */
	private function get_logger(): ?\WC_Logger_Interface {
		if ( function_exists( 'wc_get_logger' ) ) {
			return wc_get_logger();
		}
		return null;
	}

	/**
	 * Get client IP for logging.
	 *
	 * @param bool $mask Whether to mask the last octet for privacy.
	 * @return string
	 */
	public function get_client_ip( bool $mask = true ): string {
		$ip = $this->resolve_client_ip();

		if ( ! $mask ) {
			return $ip;
		}

		if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
			$parts    = explode( '.', $ip );
			$parts[3] = 'xxx';
			return implode( '.', $parts );
		}

		if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
			$parts = explode( ':', $ip );
			return implode( ':', array_slice( $parts, 0, 4 ) ) . '::xxxx';
		}

		return $ip;
	}

	/**
	 * Resolve the real client IP, considering proxy/CDN headers.
	 *
	 * Auto mode: trusts CDN-specific headers (Cloudflare, Sucuri, Akamai)
	 * that the CDN overwrites on every request and cannot be spoofed.
	 * Always On (yes): also trusts generic proxy headers (X-Forwarded-For, X-Real-IP).
	 * Off (no): only reads REMOTE_ADDR.
	 *
	 * @return string The resolved client IP address.
	 */
	private function resolve_client_ip(): string {
		$proxy_mode = get_option( 'carticy_checkout_shield_proxy_support', 'no' );

		// Off = direct connection only.
		if ( 'no' === $proxy_mode ) {
			return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) );
		}

		// CDN-specific headers — safe to trust because the CDN overwrites them.
		// Checked in both 'auto' and 'yes' modes.
		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
		$cdn_headers = array(
			'HTTP_CF_CONNECTING_IP',   // Cloudflare.
			'HTTP_X_SUCURI_CLIENTIP',  // Sucuri.
			'HTTP_TRUE_CLIENT_IP',     // Akamai, Cloudflare Enterprise.
		);

		foreach ( $cdn_headers as $header ) {
			if ( ! empty( $_SERVER[ $header ] ) ) {
				$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
				if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
					return $ip;
				}
			}
		}

		// Generic proxy headers — only trust when explicitly enabled ('yes' mode).
		if ( 'yes' === $proxy_mode ) {
			$generic_headers = array(
				'HTTP_X_REAL_IP',        // Nginx reverse proxy.
				'HTTP_X_FORWARDED_FOR',  // AWS ALB/ELB, Azure, generic load balancers.
				'HTTP_X_FORWARDED',      // General proxy.
				'HTTP_FORWARDED_FOR',    // Older proxy implementations.
			);

			foreach ( $generic_headers as $header ) {
				if ( ! empty( $_SERVER[ $header ] ) ) {
					$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
					// X-Forwarded-For may contain a chain: client, proxy1, proxy2.
					if ( str_contains( $ip, ',' ) ) {
						$parts = array_map( 'trim', explode( ',', $ip ) );
						$ip    = $parts[0];
					}
					if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
						return $ip;
					}
				}
			}
		}
		// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotValidated

		return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) );
	}

	/**
	 * Get user agent for logging.
	 *
	 * @return string
	 */
	private function get_user_agent(): string {
		$ua = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );
		return mb_substr( $ua, 0, 150 );
	}
}
