<?php
namespace App\Core;

use PDO;

final class Security
{
    public static function applyHeaders(): void
    {
        $config = $GLOBALS['config']['security'] ?? [];
        if (!($config['enable_security_headers'] ?? true)) {
            return;
        }
        header('X-Frame-Options: SAMEORIGIN');
        header('X-Content-Type-Options: nosniff');
        header('Referrer-Policy: strict-origin-when-cross-origin');
        header('Permissions-Policy: camera=(self), microphone=(), geolocation=(self)');
        header('Cross-Origin-Opener-Policy: same-origin');
        if (($GLOBALS['config']['env'] ?? 'local') === 'production') {
            header("Content-Security-Policy: default-src 'self'; img-src 'self' data: blob: https:; script-src 'self' 'unsafe-inline' https://maps.googleapis.com https://maps.gstatic.com https://*.googleapis.com https://*.gstatic.com https://unpkg.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://*.googleapis.com https://unpkg.com; font-src 'self' data: https://fonts.gstatic.com https://*.gstatic.com; connect-src 'self' https://maps.googleapis.com https://*.googleapis.com https://maps.gstatic.com https://*.gstatic.com https://routes.googleapis.com https://router.project-osrm.org; frame-src 'self'; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests");
        }
    }

    public static function enforceHttps(): void
    {
        $config = $GLOBALS['config']['security'] ?? [];
        if (!($config['force_https'] ?? false)) {
            return;
        }
        $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
        if (!$https) {
            $host = $_SERVER['HTTP_HOST'] ?? '';
            $uri = $_SERVER['REQUEST_URI'] ?? '/';
            header('Location: https://' . $host . $uri, true, 301);
            exit;
        }
    }

    public static function clientIp(): string
    {
        $ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        if (str_contains($ip, ',')) {
            $ip = trim(explode(',', $ip)[0]);
        }
        return substr($ip, 0, 80);
    }

    public static function setting(string $key, mixed $default = null): mixed
    {
        try {
            $pdo = Database::connection();
            $stmt = $pdo->prepare('SELECT setting_value FROM app_settings WHERE setting_key = ? LIMIT 1');
            $stmt->execute([$key]);
            $value = $stmt->fetchColumn();
            if ($value === false || $value === null || $value === '') {
                return $default;
            }
            if (is_int($default)) {
                return (int)$value;
            }
            if (is_float($default)) {
                return (float)$value;
            }
            if (is_bool($default)) {
                return in_array(strtolower((string)$value), ['1', 'true', 'yes', 'on'], true);
            }
            return $value;
        } catch (\Throwable) {
            return $default;
        }
    }

    public static function setSetting(string $key, string $value, ?int $updatedBy = null): void
    {
        $pdo = Database::connection();
        $stmt = $pdo->prepare('INSERT INTO app_settings (setting_key, setting_value, updated_by, created_at, updated_at) VALUES (?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_by = VALUES(updated_by), updated_at = NOW()');
        $stmt->execute([$key, $value, $updatedBy]);
    }

    public static function settings(): array
    {
        $defaults = [
            'max_failed_attempts' => (string)($GLOBALS['config']['security']['login_max_attempts'] ?? 3),
            'lockout_minutes' => (string)($GLOBALS['config']['security']['login_lock_minutes'] ?? 60),
            'session_timeout_minutes' => (string)($GLOBALS['config']['security']['session_timeout_minutes'] ?? 180),
        ];
        try {
            $rows = Database::connection()->query('SELECT setting_key, setting_value FROM app_settings')->fetchAll(PDO::FETCH_KEY_PAIR);
            return array_merge($defaults, $rows ?: []);
        } catch (\Throwable) {
            return $defaults;
        }
    }

    public static function tooManyAttempts(string $action, string $key, int $maxAttempts, int $lockMinutes): bool
    {
        $pdo = Database::connection();
        $stmt = $pdo->prepare('SELECT * FROM rate_limits WHERE rate_key = ? AND action = ? LIMIT 1');
        $stmt->execute([$key, $action]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($row && !empty($row['blocked_until']) && strtotime($row['blocked_until']) > time()) {
            return true;
        }
        if ($row && strtotime($row['window_starts_at']) < time() - 3600) {
            $pdo->prepare('DELETE FROM rate_limits WHERE id = ?')->execute([(int)$row['id']]);
            return false;
        }
        return $row && (int)$row['attempts'] >= $maxAttempts;
    }

    public static function recordAttempt(string $action, string $key, bool $success, int $maxAttempts, int $lockMinutes): void
    {
        try {
            $pdo = Database::connection();
            if ($success) {
                $pdo->prepare('DELETE FROM rate_limits WHERE rate_key = ? AND action = ?')->execute([$key, $action]);
                return;
            }
            $stmt = $pdo->prepare('SELECT id, attempts FROM rate_limits WHERE rate_key = ? AND action = ? LIMIT 1');
            $stmt->execute([$key, $action]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if (!$row) {
                $pdo->prepare('INSERT INTO rate_limits (rate_key, action, attempts, window_starts_at, created_at, updated_at) VALUES (?, ?, 1, NOW(), NOW(), NOW())')->execute([$key, $action]);
                return;
            }
            $attempts = (int)$row['attempts'] + 1;
            $blocked = $attempts >= $maxAttempts ? date('Y-m-d H:i:s', time() + ($lockMinutes * 60)) : null;
            $pdo->prepare('UPDATE rate_limits SET attempts = ?, blocked_until = ?, updated_at = NOW() WHERE id = ?')->execute([$attempts, $blocked, (int)$row['id']]);
        } catch (\Throwable) {
            // Rate limiting must never break auth flow.
        }
    }

    public static function audit(?int $userId, string $action, string $entityType, ?int $entityId = null, mixed $newValue = null, mixed $oldValue = null): void
    {
        try {
            $pdo = Database::connection();
            $stmt = $pdo->prepare('INSERT INTO audit_logs (user_id, action, entity_type, entity_id, old_value_json, new_value_json, ip_address, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())');
            $stmt->execute([
                $userId,
                $action,
                $entityType,
                $entityId,
                $oldValue !== null ? json_encode($oldValue, JSON_UNESCAPED_UNICODE) : null,
                $newValue !== null ? json_encode($newValue, JSON_UNESCAPED_UNICODE) : null,
                self::clientIp(),
            ]);
        } catch (\Throwable) {
            // Never break user flows because audit logging failed.
        }
    }
}
