<?php

declare(strict_types=1);

namespace App\Logging;

use Monolog\LogRecord;
use Monolog\Processor\ProcessorInterface;

/**
 * Mirror of the api-side processor (SPEC §M14.4). Scrubs Bearer tokens,
 * passwords, secrets and known-sensitive-keyed context values from records
 * before they hit a handler.
 *
 * Kept as a per-container copy rather than a shared package to preserve
 * the api/ui zero-shared-code invariant in the SPEC.
 */
final class SecretScrubbingProcessor implements ProcessorInterface
{
    private const REDACTED = '***';

    private const SENSITIVE_KEY_NEEDLES = [
        'password',
        'authorization',
        'auth_token',
        'access_token',
        'refresh_token',
        'bearer',
        'secret',
        'license_key',
        'license-key',
        'license_token',
        'ipinfo_token',
        'service_token',
        'job_token',
        'cookie',
    ];

    /**
     * @var list<array{0: string, 1: string}>
     */
    private const VALUE_PATTERNS = [
        ['/(Bearer\s+irdb_(?:rep|con|adm|svc)_)[A-Z2-7]{32}/', '$1***'],
        // SEC_REVIEW F65: Bearer floor lowered from {20,} to {8,}.
        ['/(Bearer\s+)[A-Za-z0-9._\-]{8,}/', '$1***'],
        // SEC_REVIEW F65: raw JWTs anchored on `eyJ` (base64url of
        // `{"`); see api-side processor for the full rationale.
        ['/\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/', 'eyJ***'],
        ['/\birdb_(rep|con|adm|svc)_[A-Z2-7]{32}\b/', 'irdb_$1_***'],
        ['/\$argon2(?:i|id|d)\$[^\s\'"]+/', '$argon2***'],
        ['/\$2[aby]?\$\d{2}\$[A-Za-z0-9.\/]{53}/', '$2***'],
    ];

    public function __invoke(LogRecord $record): LogRecord
    {
        $context = self::scrubArray($record->context);
        $extra = self::scrubArray($record->extra);
        $message = self::scrubString($record->message);

        return $record->with(message: $message, context: $context, extra: $extra);
    }

    /**
     * @param array<array-key, mixed> $data
     * @return array<array-key, mixed>
     */
    private static function scrubArray(array $data): array
    {
        $out = [];
        foreach ($data as $key => $value) {
            if (is_string($key) && self::isSensitiveKey($key)) {
                $out[$key] = self::REDACTED;
                continue;
            }
            if (is_array($value)) {
                $out[$key] = self::scrubArray($value);
            } elseif (is_string($value)) {
                $out[$key] = self::scrubString($value);
            } else {
                $out[$key] = $value;
            }
        }

        return $out;
    }

    private static function isSensitiveKey(string $key): bool
    {
        $lower = strtolower($key);
        foreach (self::SENSITIVE_KEY_NEEDLES as $needle) {
            if (str_contains($lower, $needle)) {
                return true;
            }
        }

        return false;
    }

    private static function scrubString(string $value): string
    {
        foreach (self::VALUE_PATTERNS as [$pattern, $replacement]) {
            $value = (string) preg_replace($pattern, $replacement, $value);
        }

        return $value;
    }
}
