<?php

namespace AIPT\Api\Chatbot;

class ChatbotLogger {

	private const PREFIX = '[AIPT][chatbot] ';
	private const RING_TRANSIENT_KEY = 'aipt_chatbot_log_ring';
	private const RING_LIMIT = 20;

	private const LEVELS = [
		'info'  => 10,
		'warn'  => 20,
		'error' => 30,
	];

	public static function info( string $event, array $context = [] ): void {
		self::log( 'info', $event, $context );
	}

	public static function warn( string $event, array $context = [] ): void {
		self::log( 'warn', $event, $context );
	}

	public static function error( string $event, array $context = [] ): void {
		self::log( 'error', $event, $context );
	}

	public static function getRecentEntries(): array {
		$entries = get_transient( self::RING_TRANSIENT_KEY );

		return is_array( $entries ) ? $entries : [];
	}

	public static function getLastError(): ?array {
		$entries = array_reverse( self::getRecentEntries() );

		foreach ( $entries as $entry ) {
			if ( is_array( $entry ) && ( $entry['level'] ?? '' ) === 'error' ) {
				return $entry;
			}
		}

		return null;
	}

	private static function log( string $level, string $event, array $context ): void {
		$level = self::normalizeLevel( $level );

		if ( ! self::shouldLog( $level ) ) {
			return;
		}

		$entry = self::buildEntry( $level, $event, $context );
		$json  = wp_json_encode( $entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );

		if ( false === $json || '' === $json ) {
			return;
		}

		$line          = self::PREFIX . $json;
		$entry['line'] = $line;

		self::appendToRingBuffer( $entry );

	}

	private static function shouldLog( string $level ): bool {
		$min_level = apply_filters( 'aipt_chatbot_log_min_level', 'warn' );
		$min_level = is_string( $min_level ) ? self::normalizeLevel( $min_level ) : 'warn';

		return self::LEVELS[ $level ] >= self::LEVELS[ $min_level ];
	}

	private static function buildEntry( string $level, string $event, array $context ): array {
		$context = self::normalizeContext( $context );

		return [
			'timestamp'  => current_time( 'mysql', true ),
			'session_id' => isset( $context['session_id'] ) ? self::normalizeString( $context['session_id'] ) : '',
			'provider'   => isset( $context['provider'] ) ? sanitize_key( self::normalizeString( $context['provider'] ) ) : '',
			'request_id' => isset( $context['request_id'] ) ? self::normalizeString( $context['request_id'] ) : '',
			'level'      => $level,
			'event'      => sanitize_key( $event ) ?: 'unknown_event',
			'context'    => $context,
		];
	}

	private static function appendToRingBuffer( array $entry ): void {
		$entries = self::getRecentEntries();
		$entries[] = $entry;

		if ( count( $entries ) > self::RING_LIMIT ) {
			$entries = array_slice( $entries, -self::RING_LIMIT );
		}

		set_transient( self::RING_TRANSIENT_KEY, $entries, self::getRingTtl() );
	}

	private static function normalizeLevel( string $level ): string {
		$level = strtolower( $level );

		return isset( self::LEVELS[ $level ] ) ? $level : 'warn';
	}

	private static function normalizeContext( array $context ): array {
		$normalized = [];

		foreach ( $context as $key => $value ) {
			$key = is_string( $key ) ? sanitize_key( $key ) : (string) $key;
			if ( '' === $key ) {
				continue;
			}

			$normalized[ $key ] = self::normalizeValue( $value );
		}

		return $normalized;
	}

	private static function normalizeValue( $value ) {
		if ( null === $value || is_bool( $value ) || is_int( $value ) || is_float( $value ) ) {
			return $value;
		}

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

		if ( is_array( $value ) ) {
			return self::normalizeContext( $value );
		}

		if ( is_object( $value ) && method_exists( $value, '__toString' ) ) {
			return self::normalizeString( (string) $value );
		}

		return gettype( $value );
	}

	private static function normalizeString( $value ): string {
		$value = wp_strip_all_tags( (string) $value );
		$value = preg_replace( '/\s+/', ' ', $value );
		$value = is_string( $value ) ? trim( $value ) : '';

		$value = self::redactSensitive( $value );

		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $value, 0, 2000, 'UTF-8' );
		}

		return substr( $value, 0, 2000 );
	}

	/**
	 * Mask common secret shapes (API keys, emails, absolute paths, UUIDs,
	 * SQL fragments) before they reach the ring buffer. Runs after tag
	 * stripping + whitespace collapse but BEFORE multibyte truncation so a
	 * cap at 2000 chars never bisects a redacted token.
	 */
	private static function redactSensitive( string $value ): string {
		$patterns = [
			'/\b(?:sk|pk|rk|ak|api_key|sk_live|sk_test|pk_live|pk_test)[_\-][A-Za-z0-9]{12,}/',
			'/\bBearer\s+[A-Za-z0-9._\-]+/i',
			'/\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/',
			'/(?:^|\s)(?:\/[A-Za-z0-9_\-.]+){2,}/',
			'/\b[A-Za-z]:\\\\[A-Za-z0-9_\-.\\\\ ]+/',
			'/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i',
			'/\b(?:SELECT|INSERT|UPDATE|DELETE)\b\s+(?:[A-Za-z_*][\w*,.()\s]*?\s+)?(?:FROM|INTO|SET|VALUES)\b[^;]*/i',
		];

		foreach ( $patterns as $pattern ) {
			$replaced = preg_replace( $pattern, '[REDACTED]', $value );
			if ( is_string( $replaced ) ) {
				$value = $replaced;
			}
		}

		return $value;
	}

	private static function getRingTtl(): int {
		return defined( 'HOUR_IN_SECONDS' ) ? HOUR_IN_SECONDS : 3600;
	}
}
