<?php

namespace App\Logging;

/**
 * Merkezi log redaction yardımcısı.
 *
 * Tüm log çağrılarında hassas bilgileri (kart no, CVV, parola, token,
 * cPanel/whm şifresi, reset kodları, kimlik no, KVKK kapsamı PII)
 * filtrelemek için kullanılır. İki kullanım modu vardır:
 *
 *   1) `redact($context)` — geçilen context array'i hassas alanları
 *      `'***REDACTED***'` ile değiştirerek döndürür (recursive).
 *   2) `mask*` yardımcıları — KVKK kapsamı PII (email/ip/phone/isim)
 *      için tamamen anonimize değil ama tanınmaz hale getiren maskeler.
 *
 * Anti-pattern: log çağrısından önce manuel `array_diff_key` yapma —
 * key listesi tek noktada güncellenmeli ki yeni hassas alan eklendiğinde
 * tüm modüller faydalansın.
 *
 * Defense-in-depth: `SensitiveContextProcessor` Monolog handler'a da
 * register edilir; call-site redaction unutulsa bile son satır filtrelenir.
 */
final class SensitiveDataRedactor
{
    /** Sentinel değeri — log satırında "redacted" olduğunu açık şekilde işaretler. */
    public const PLACEHOLDER = '***REDACTED***';

    /**
     * Hassas anahtar pattern'leri (lowercase substring match).
     *
     * Bu listedeki herhangi bir substring key'in lowercase halinde
     * bulunursa değer redact edilir. `pan` özel — PCI kart numarası
     * için tam eşleşme; substring olarak diğer kelimelerle çakışmasın
     * diye `pan` regex pattern'i ayrı.
     */
    private const SENSITIVE_KEY_SUBSTRINGS = [
        // Payment / PCI DSS
        'card_number', 'cardnumber', 'card_no', 'cardno',
        'cvv', 'cvc', 'cv2', 'card_cvv', 'cardcvv',
        'card_holder', 'cardholder', 'cardholder_name',
        'card_expiry', 'cardexpiry', 'card_exp',
        'billing_cvc', 'billing_cvv',

        // Authentication / secrets
        'password', 'passwd', 'pwd',
        'secret', 'api_secret', 'apisecret', 'client_secret',
        'token', 'access_token', 'refresh_token', 'bearer',
        'api_key', 'apikey', 'license_key', 'licensekey',
        'private_key', 'privatekey', 'auth_token',

        // Application-specific credential names
        'cpanel_password', 'cpanelpassword',
        'whm_password', 'whmpassword',
        'plesk_password', 'pleskpassword',
        'server_password', 'serverpassword',
        'root_password', 'rootpassword',
        'admin_password', 'adminpassword',
        'mysql_password', 'mysqlpassword',
        'db_password', 'dbpassword',
        'ftp_password', 'ftppassword',
        'mail_password', 'mailpassword',

        // Reset / verification codes
        'resetcode', 'reset_code', 'reset_token',
        'verificationcode', 'verification_code', 'verify_code',
        'otp', 'one_time_password',
        'two_factor_secret', 'totp_secret', '2fa_secret',

        // Identifiers protected by KVKK / PCI
        'tckn', 'tc_kimlik', 'identification_number',
        'iban', 'account_number', 'accountnumber',
        'ssn', 'social_security',

        // Fraud module raw payloads
        'mc_data', 'minfraud_data',
    ];

    /**
     * Tam eşleşmeli (case-insensitive) hassas key'ler.
     *
     * Substring match çok geniş olur (örn. `panel` kelimesinde `pan` geçer).
     * Bu set yalnızca tam eşleşme arar.
     */
    private const SENSITIVE_KEY_EXACT = [
        'pan',
        'key',         // generic — API contract dışı 'key' her yerde hassas sayılır
        'auth',
        'pin',
        'pwd',
    ];

    /**
     * Bir context array'ini recursive olarak filtrele.
     *
     * @param  array<int|string, mixed>  $context
     * @return array<int|string, mixed>
     */
    public static function redact(array $context): array
    {
        return self::walkArray($context);
    }

    /**
     * Tek bir string değeri kısmen maskele (debug açısından prefix korunur).
     *
     * Boş veya çok kısa string'ler tamamen ***. Aksi halde
     * ilk 2 karakter + `***`.
     */
    public static function maskString(?string $value, int $visible = 2): string
    {
        $value = (string) ($value ?? '');

        if ($value === '') {
            return '';
        }

        if (mb_strlen($value) <= $visible) {
            return str_repeat('*', max(3, mb_strlen($value)));
        }

        return mb_substr($value, 0, $visible) . '***';
    }

    /**
     * Email adresini local-part'tan ilk karakter + domain göster.
     *  "john.doe@example.com" → "j***@example.com"
     */
    public static function maskEmail(?string $email): string
    {
        $email = (string) ($email ?? '');

        if ($email === '' || !str_contains($email, '@')) {
            return $email === '' ? '' : '***';
        }

        [$local, $domain] = explode('@', $email, 2);
        $localMasked = mb_substr($local, 0, 1) . '***';

        return $localMasked . '@' . $domain;
    }

    /**
     * IPv4 → "192.168.x.x", IPv6 → ilk 2 hextet + "::***"
     */
    public static function maskIp(?string $ip): string
    {
        $ip = (string) ($ip ?? '');

        if ($ip === '') {
            return '';
        }

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $parts = explode('.', $ip);
            return $parts[0] . '.' . $parts[1] . '.x.x';
        }

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            $parts = explode(':', $ip);
            return ($parts[0] ?? '') . ':' . ($parts[1] ?? '') . '::***';
        }

        return '***';
    }

    /**
     * Telefon numarasının son 4 hanesini koru: "+90 532 *** **67"
     */
    public static function maskPhone(?string $phone): string
    {
        $phone = (string) ($phone ?? '');
        $digits = preg_replace('/\D+/', '', $phone);

        if ($digits === '' || strlen($digits) < 4) {
            return $digits === '' ? '' : '***';
        }

        $last4 = substr($digits, -4);
        return '***' . $last4;
    }

    /**
     * Kart numarasından sadece BIN (ilk 6) + son 4 hane: "411111******1111"
     * PCI-DSS'in izin verdiği truncation formatı.
     */
    public static function maskPan(?string $pan): string
    {
        $pan = (string) ($pan ?? '');
        $digits = preg_replace('/\D+/', '', $pan);
        $len = strlen($digits);

        if ($len < 10) {
            return self::PLACEHOLDER;
        }

        return substr($digits, 0, 6) . str_repeat('*', $len - 10) . substr($digits, -4);
    }

    /**
     * İsmi maskele: "Ahmet Yılmaz" → "Ahmet Y***"
     */
    public static function maskName(?string $name): string
    {
        $name = trim((string) ($name ?? ''));

        if ($name === '') {
            return '';
        }

        $parts = preg_split('/\s+/', $name) ?: [];

        if (count($parts) === 1) {
            return mb_substr($parts[0], 0, 1) . '***';
        }

        $first = array_shift($parts);
        $rest = array_map(static fn (string $p) => mb_substr($p, 0, 1) . '***', $parts);

        return trim($first . ' ' . implode(' ', $rest));
    }

    /**
     * Verilen key (string|int) hassas olarak değerlendirilmeli mi?
     */
    public static function isSensitiveKey(int|string $key): bool
    {
        if (is_int($key)) {
            return false;
        }

        $needle = strtolower($key);

        if (in_array($needle, self::SENSITIVE_KEY_EXACT, true)) {
            return true;
        }

        foreach (self::SENSITIVE_KEY_SUBSTRINGS as $pattern) {
            if (str_contains($needle, $pattern)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param  array<int|string, mixed>  $arr
     * @return array<int|string, mixed>
     */
    private static function walkArray(array $arr): array
    {
        $out = [];

        foreach ($arr as $key => $value) {
            if (self::isSensitiveKey($key)) {
                $out[$key] = self::PLACEHOLDER;
                continue;
            }

            if (is_array($value)) {
                $out[$key] = self::walkArray($value);
                continue;
            }

            $out[$key] = $value;
        }

        return $out;
    }
}
