<?php
namespace App\Core;

use Throwable;

class Security
{
    private const SENSITIVE_KEYS = [
        'password', 'password_confirmation', 'current_password', 'new_password',
        'pin', 'transaction_pin', 'otp', 'otp_code', 'token', 'token_hash',
        'client_secret', 'store_passwd', 'store_password', 'app_secret', 'merchant_key',
        'authorization', 'cookie',
    ];

    public static function csrfToken(): string
    {
        if (empty($_SESSION[CSRF_KEY])) {
            $_SESSION[CSRF_KEY] = bin2hex(random_bytes(32));
        }
        return (string) $_SESSION[CSRF_KEY];
    }

    public static function rotateCsrf(): string
    {
        $_SESSION[CSRF_KEY] = bin2hex(random_bytes(32));
        return (string) $_SESSION[CSRF_KEY];
    }

    public static function verifyCsrf(string $token): bool
    {
        return $token !== ''
            && isset($_SESSION[CSRF_KEY])
            && hash_equals((string) $_SESSION[CSRF_KEY], $token);
    }

    public static function otp(int $length = 6): string
    {
        $length = max(4, min(10, $length));
        return str_pad((string) random_int(0, (10 ** $length) - 1), $length, '0', STR_PAD_LEFT);
    }

    public static function randomToken(int $bytes = 32): string
    {
        return bin2hex(random_bytes(max(16, min(64, $bytes))));
    }

    public static function clientIp(): string
    {
        $candidate = (string) ($_SERVER['REMOTE_ADDR'] ?? '');
        if (TRUST_PROXY_HEADERS) {
            $forwarded = trim(explode(',', (string) ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''))[0] ?? '');
            if (filter_var($forwarded, FILTER_VALIDATE_IP)) {
                $candidate = $forwarded;
            }
        }
        return filter_var($candidate, FILTER_VALIDATE_IP) ? $candidate : '0.0.0.0';
    }

    public static function userAgent(): string
    {
        return mb_substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 250);
    }

    public static function logActivity(?int $userId, string $action, array $payload = []): void
    {
        if (!Database::tableExists('activity_logs')) {
            return;
        }
        $safePayload = self::redact($payload);
        $json = json_encode($safePayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($json === false) {
            $json = '{}';
        }
        if (strlen($json) > 16000) {
            $json = json_encode(['truncated' => true, 'request_id' => defined('REQUEST_ID') ? REQUEST_ID : null]);
        }
        Database::safeExecute(
            'INSERT INTO activity_logs (user_id, action, payload, ip_address, user_agent, created_at) VALUES (?,?,?,?,?,NOW())',
            [$userId, mb_substr($action, 0, 120), $json, self::clientIp(), self::userAgent()]
        );
    }

    public static function rateLimit(string $key, int $maxAttempts, int $windowSeconds): bool
    {
        $maxAttempts = max(1, $maxAttempts);
        $windowSeconds = max(1, $windowSeconds);
        $normalizedKey = hash('sha256', $key);
        $now = new \DateTimeImmutable('now');
        $expires = $now->modify('+' . $windowSeconds . ' seconds');

        if (Database::tableExists('security_rate_limits')) {
            try {
                Database::execute(
                    'INSERT INTO security_rate_limits (rate_key,hits,window_started_at,expires_at,updated_at) VALUES (?,1,?,?,?)
                     ON DUPLICATE KEY UPDATE
                       hits=IF(expires_at<=VALUES(window_started_at),1,hits+1),
                       window_started_at=IF(expires_at<=VALUES(window_started_at),VALUES(window_started_at),window_started_at),
                       expires_at=IF(expires_at<=VALUES(window_started_at),VALUES(expires_at),expires_at),
                       updated_at=VALUES(updated_at)',
                    [
                        $normalizedKey,
                        $now->format('Y-m-d H:i:s'),
                        $expires->format('Y-m-d H:i:s'),
                        $now->format('Y-m-d H:i:s'),
                    ]
                );
                $row = Database::fetchOne('SELECT hits FROM security_rate_limits WHERE rate_key=? LIMIT 1', [$normalizedKey]);
                if (random_int(1, 100) === 1) {
                    Database::safeExecute('DELETE FROM security_rate_limits WHERE expires_at < DATE_SUB(NOW(), INTERVAL 1 DAY)');
                }
                return (int) ($row['hits'] ?? 0) <= $maxAttempts;
            } catch (Throwable $exception) {
                error_log('Persistent rate-limit failure: ' . $exception->getMessage());
            }
        }

        $timestamp = time();
        $bucket = $_SESSION['_rate_limit'][$normalizedKey] ?? ['started' => $timestamp, 'count' => 0];
        if (($timestamp - (int) $bucket['started']) >= $windowSeconds) {
            $bucket = ['started' => $timestamp, 'count' => 0];
        }
        $bucket['count']++;
        $_SESSION['_rate_limit'][$normalizedKey] = $bucket;
        return (int) $bucket['count'] <= $maxAttempts;
    }

    public static function clearRateLimit(string $key): void
    {
        $normalizedKey = hash('sha256', $key);
        unset($_SESSION['_rate_limit'][$normalizedKey]);
        if (Database::tableExists('security_rate_limits')) {
            Database::safeExecute('DELETE FROM security_rate_limits WHERE rate_key=?', [$normalizedKey]);
        }
    }

    public static function passwordError(string $password, string $email = ''): ?string
    {
        if (mb_strlen($password) < PASSWORD_MIN_LENGTH) {
            return 'Password must be at least ' . PASSWORD_MIN_LENGTH . ' characters.';
        }
        if (mb_strlen($password) > 200) {
            return 'Password is too long.';
        }
        if ($email !== '' && hash_equals(strtolower(trim($email)), strtolower(trim($password)))) {
            return 'Password cannot be the same as the email address.';
        }
        $common = ['password', 'password123', '12345678', 'admin123', 'qwerty123', 'bangladesh'];
        if (in_array(strtolower($password), $common, true)) {
            return 'Choose a less common password.';
        }
        return null;
    }

    public static function safeFilename(string $name): string
    {
        $name = preg_replace('/[^a-zA-Z0-9._-]+/', '-', basename($name));
        return trim((string) $name, '.-') ?: 'file';
    }

    public static function isSafeReturnPath(string $path): bool
    {
        if ($path === '' || str_contains($path, "\0") || str_starts_with($path, '//')) {
            return false;
        }
        $parts = parse_url($path);
        return is_array($parts) && empty($parts['scheme']) && empty($parts['host']);
    }

    private static function redact(mixed $value, ?string $key = null): mixed
    {
        if ($key !== null && in_array(strtolower($key), self::SENSITIVE_KEYS, true)) {
            return '[REDACTED]';
        }
        if (!is_array($value)) {
            return is_string($value) ? mb_substr($value, 0, 2000) : $value;
        }
        $result = [];
        foreach ($value as $childKey => $childValue) {
            $result[$childKey] = self::redact($childValue, is_string($childKey) ? $childKey : null);
        }
        return $result;
    }
}
