<?php

declare(strict_types=1);

namespace App\Framework\Waf\ValueObjects;

use App\Framework\Core\ValueObjects\Byte;

/**
 * Safe representation of malicious payload samples for logging and analysis
 * Automatically sanitizes and truncates payloads to prevent log injection
 */
final readonly class PayloadSample
{
    private const int DEFAULT_MAX_LENGTH = 200;
    private const int ABSOLUTE_MAX_LENGTH = 1000;

    public function __construct(
        public string $original,
        public string $sanitized,
        public Byte $originalSize,
        public bool $wasTruncated,
        public ?string $encoding = null,
        public ?string $contentType = null
    ) {
    }

    /**
     * Create payload sample from raw input
     */
    public static function fromInput(
        string $input,
        int $maxLength = self::DEFAULT_MAX_LENGTH,
        ?string $contentType = null
    ): self {
        $originalSize = Byte::fromBytes(strlen($input));
        $wasTruncated = strlen($input) > $maxLength;

        // Truncate if necessary
        $truncated = $wasTruncated ? substr($input, 0, $maxLength) : $input;

        // Sanitize for safe logging
        $sanitized = self::sanitizeForLogging($truncated);

        // Detect encoding
        $encoding = mb_detect_encoding($input, ['UTF-8', 'ASCII', 'ISO-8859-1'], true) ?: 'unknown';

        return new self(
            original: $truncated, // Already truncated for safety
            sanitized: $sanitized,
            originalSize: $originalSize,
            wasTruncated: $wasTruncated,
            encoding: $encoding,
            contentType: $contentType
        );
    }

    /**
     * Create sample with custom max length
     */
    public static function withMaxLength(
        string $input,
        int $maxLength,
        ?string $contentType = null
    ): self {
        // Enforce absolute maximum
        $maxLength = min($maxLength, self::ABSOLUTE_MAX_LENGTH);

        return self::fromInput($input, $maxLength, $contentType);
    }

    /**
     * Create minimal sample for high-risk payloads
     */
    public static function minimal(string $input, ?string $contentType = null): self
    {
        return self::fromInput($input, 50, $contentType);
    }

    /**
     * Create extended sample for analysis
     */
    public static function extended(string $input, ?string $contentType = null): self
    {
        return self::fromInput($input, 500, $contentType);
    }

    /**
     * Sanitize payload for safe logging (prevent log injection)
     */
    private static function sanitizeForLogging(string $input): string
    {
        // Remove null bytes
        $sanitized = str_replace("\0", '', $input);

        // Replace control characters with placeholders
        $sanitized = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '?', $sanitized);

        // Escape potential log injection patterns
        $sanitized = str_replace(["\r\n", "\r", "\n"], ['\\r\\n', '\\r', '\\n'], $sanitized);

        // Replace multiple consecutive whitespace with single space
        $sanitized = preg_replace('/\s+/', ' ', $sanitized);

        // Trim whitespace
        $sanitized = trim($sanitized);

        return $sanitized;
    }

    /**
     * Get hex representation for binary data
     */
    public function getHexDump(): string
    {
        // Only show first 100 bytes as hex
        $bytes = substr($this->original, 0, 100);

        return bin2hex($bytes);
    }

    /**
     * Check if payload appears to be binary data
     */
    public function isBinary(): bool
    {
        // Check for null bytes or high ratio of non-printable characters
        if (str_contains($this->original, "\0")) {
            return true;
        }

        $printableCount = 0;
        $totalLength = strlen($this->original);

        for ($i = 0; $i < $totalLength; $i++) {
            $char = ord($this->original[$i]);
            if ($char >= 32 && $char <= 126) {
                $printableCount++;
            }
        }

        // If less than 70% printable characters, consider it binary
        return $totalLength > 0 && ($printableCount / $totalLength) < 0.7;
    }

    /**
     * Get sample representation for logging
     */
    public function getSample(): string
    {
        return $this->getSafeDisplay();
    }

    /**
     * Get safe representation for display
     */
    public function getSafeDisplay(): string
    {
        if ($this->isBinary()) {
            return '[Binary data - ' . $this->originalSize->toHumanReadable() . ']';
        }

        $display = $this->sanitized;

        if ($this->wasTruncated) {
            $display .= ' [...truncated]';
        }

        return $display;
    }

    /**
     * Get analysis-friendly representation
     */
    public function getAnalysisView(): string
    {
        if ($this->isBinary()) {
            return $this->getHexDump();
        }

        return $this->original;
    }

    /**
     * Check if sample was truncated
     */
    public function wasTruncated(): bool
    {
        return $this->wasTruncated;
    }

    /**
     * Get original size in bytes
     */
    public function getOriginalSize(): Byte
    {
        return $this->originalSize;
    }

    /**
     * Get content type if known
     */
    public function getContentType(): ?string
    {
        return $this->contentType;
    }

    /**
     * Get detected encoding
     */
    public function getEncoding(): ?string
    {
        return $this->encoding;
    }

    /**
     * Create with content type
     */
    public function withContentType(string $contentType): self
    {
        return new self(
            original: $this->original,
            sanitized: $this->sanitized,
            originalSize: $this->originalSize,
            wasTruncated: $this->wasTruncated,
            encoding: $this->encoding,
            contentType: $contentType
        );
    }

    /**
     * Convert to array for serialization
     */
    public function toArray(): array
    {
        return [
            'sanitized' => $this->sanitized,
            'original_size_bytes' => $this->originalSize->toBytes(),
            'was_truncated' => $this->wasTruncated,
            'encoding' => $this->encoding,
            'content_type' => $this->contentType,
            'is_binary' => $this->isBinary(),
            'safe_display' => $this->getSafeDisplay(),
        ];
    }
}
