<?php

namespace App\Http\Middleware;

use App\Services\Security\SecurityEventLogger;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

class SecurityInputGuard
{
    private $logger;

    public function __construct(SecurityEventLogger $logger)
    {
        $this->logger = $logger;
    }

    public function handle(Request $request, Closure $next)
    {
        if ($request->is('api/*')) {
            return $next($request);
        }

        $input = $request->all();

        if ($this->honeypotFilled($input)) {
            $this->logger->log('honeypot_submission_blocked', 'critical', [
                'filled_fields' => $this->filledHoneypotFields($input),
            ], $request);

            return $this->reject($request, 'The request was rejected by the security filter.');
        }

        $scan = $this->scanArray($input);
        if ($scan !== null) {
            $this->logger->log('malicious_input_blocked', 'critical', $scan, $request);

            return $this->reject($request, 'The request contains unsafe input and was rejected.');
        }

        $fileScan = $this->scanUploadedFiles($request);
        if ($fileScan !== null) {
            $this->logger->log('unsafe_file_upload_blocked', 'critical', $fileScan, $request);

            return $this->reject($request, 'The uploaded file is not allowed. Please upload a valid document or image file.');
        }

        $request->merge($this->sanitizeArray($input));

        return $next($request);
    }

    private function honeypotFilled(array $input): bool
    {
        foreach (['_hp', 'website', 'homepage', 'company_url'] as $field) {
            if (array_key_exists($field, $input) && trim((string) $input[$field]) !== '') {
                return true;
            }
        }

        return false;
    }

    private function filledHoneypotFields(array $input): array
    {
        return array_values(array_filter(['_hp', 'website', 'homepage', 'company_url'], function ($field) use ($input) {
            return array_key_exists($field, $input) && trim((string) $input[$field]) !== '';
        }));
    }

    private function scanArray(array $input, string $prefix = ''): ?array
    {
        foreach ($input as $key => $value) {
            $path = $prefix === '' ? (string) $key : $prefix.'.'.$key;

            if ($this->isSafeKey($path)) {
                continue;
            }

            if (is_array($value)) {
                $nested = $this->scanArray($value, $path);
                if ($nested !== null) {
                    return $nested;
                }
                continue;
            }

            if (! is_string($value)) {
                continue;
            }

            if (mb_strlen($value) > $this->maxLengthForField($path)) {
                return [
                    'field' => $path,
                    'reason' => 'excessive_length',
                    'length' => mb_strlen($value),
                    'detected_value' => mb_substr($value, 0, 1000),
                ];
            }

            $reason = $this->attackReason($value);
            if ($reason !== null) {
                return [
                    'field' => $path,
                    'reason' => $reason,
                    'detected_value' => $value,
                ];
            }
        }

        return null;
    }

    private function attackReason(string $value): ?string
    {
        $value = trim($value);

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

        $patterns = [
            'script_tag' => '/<\s*script\b/i',
            'event_handler_html' => '/\bon\w+\s*=/i',
            'javascript_uri' => '/javascript\s*:/i',
            'sql_union_select' => '/\bunion\s+select\b/i',
            'sql_stacked_query' => '/;\s*(drop|alter|truncate|delete|insert|update)\s+/i',
            'sql_file_access' => '/\b(load_file|into\s+outfile|xp_cmdshell)\b/i',
            'sql_time_based' => '/\b(sleep|benchmark)\s*\(/i',
            'path_traversal' => '/(\.\.\/|\.\.\\\\)/',
            'php_wrapper' => '/\b(php|data|expect|phar):\/\//i',
            'command_injection' => '/(\|\||&&|;\s*(cat|curl|wget|bash|sh|powershell|cmd|rm|nc)\b)/i',
            'null_byte' => '/\x00/',
        ];

        foreach ($patterns as $reason => $pattern) {
            if (preg_match($pattern, $value)) {
                return $reason;
            }
        }

        return null;
    }

    private function scanUploadedFiles(Request $request): ?array
    {
        foreach ($request->allFiles() as $field => $file) {
            $files = is_array($file) ? Arr::flatten($file) : [$file];

            foreach ($files as $uploadedFile) {
                if (! $uploadedFile || ! method_exists($uploadedFile, 'getClientOriginalName')) {
                    continue;
                }

                $name = (string) $uploadedFile->getClientOriginalName();
                $extension = strtolower((string) $uploadedFile->getClientOriginalExtension());

                if (mb_strlen($name) > 180 || preg_match('/(\.\.\/|\.\.\\\\|\x00)/', $name)) {
                    return [
                        'field' => $field,
                        'reason' => 'unsafe_filename',
                        'detected_value' => $name,
                    ];
                }

                if (in_array($extension, ['php', 'phtml', 'phar', 'exe', 'bat', 'cmd', 'sh', 'js', 'html', 'htm', 'svg'], true)) {
                    return [
                        'field' => $field,
                        'reason' => 'blocked_file_extension',
                        'extension' => $extension,
                        'detected_value' => $name,
                    ];
                }

                if (method_exists($uploadedFile, 'getSize') && $uploadedFile->getSize() > (int) config('security.max_upload_bytes', 20 * 1024 * 1024)) {
                    return [
                        'field' => $field,
                        'reason' => 'file_too_large',
                        'size' => $uploadedFile->getSize(),
                        'detected_value' => $name,
                    ];
                }
            }
        }

        return null;
    }

    private function sanitizeArray(array $input): array
    {
        foreach ($input as $key => $value) {
            if ($this->isSafeKey((string) $key)) {
                continue;
            }

            if (is_array($value)) {
                $input[$key] = $this->sanitizeArray($value);
                continue;
            }

            if (is_string($value)) {
                $input[$key] = $this->sanitizeString($value);
            }
        }

        return $input;
    }

    private function sanitizeString(string $value): string
    {
        $value = str_replace("\0", '', $value);
        $value = preg_replace('/[^\P{C}\t\r\n]+/u', '', $value) ?: '';
        return trim($value);
    }

    private function isSafeKey(string $key): bool
    {
        $key = strtolower($key);

        return in_array($key, ['password', 'password_confirmation', 'password_confirmatio', 'current_password', '_token'], true)
            || Str::endsWith($key, ['.password', '.password_confirmation', '.password_confirmatio']);
    }

    private function maxLengthForField(string $field): int
    {
        $field = strtolower($field);

        if (Str::contains($field, ['notes', 'details', 'description', 'address'])) {
            return 10000;
        }

        if (Str::contains($field, ['csv_text', 'payload', 'content'])) {
            return 2000000;
        }

        return 5000;
    }

    private function reject(Request $request, string $message)
    {
        if ($request->expectsJson() || $request->ajax()) {
            return response()->json([
                'success' => false,
                'message' => $message,
            ], 422);
        }

        return back()->withInput($request->except(['password', 'password_confirmation', 'password_confirmatio']))
            ->withErrors(['security' => $message])
            ->with('error_message', $message);
    }
}
