<?php

namespace App\Services\Files;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Log;
use RuntimeException;

class FileValidationService
{
    /**
     * Validate a file's MIME type using magic bytes.
     *
     * @throws RuntimeException If the file type is not allowed or doesn't match its extension
     */
    public function validate(UploadedFile $file, ?array $allowedMimeTypes = null): array
    {
        $allowedMimeTypes = $allowedMimeTypes ?? config('file_upload.allowed_mime_types', []);

        // Detect actual MIME type from file content
        $detectedMimeType = $this->detectMimeType($file);

        // Get declared MIME type
        $declaredMimeType = $file->getMimeType();

        // Check if MIME type is allowed
        if (! empty($allowedMimeTypes) && ! in_array($detectedMimeType, $allowedMimeTypes)) {
            throw new RuntimeException(
                "File type '{$detectedMimeType}' is not allowed. Allowed types: ".implode(', ', $allowedMimeTypes)
            );
        }

        // Verify extension matches MIME type
        $extension = strtolower($file->getClientOriginalExtension());
        $expectedMimeTypes = $this->getMimeTypesForExtension($extension);

        if (! empty($expectedMimeTypes) && ! in_array($detectedMimeType, $expectedMimeTypes)) {
            Log::warning('File extension mismatch', [
                'extension' => $extension,
                'detected_mime' => $detectedMimeType,
                'declared_mime' => $declaredMimeType,
                'expected_mimes' => $expectedMimeTypes,
            ]);

            throw new RuntimeException(
                "File extension '.{$extension}' does not match the detected file type '{$detectedMimeType}'."
            );
        }

        // Validate file size
        $this->validateFileSize($file);

        return [
            'mime_type' => $detectedMimeType,
            'extension' => $extension,
            'size' => $file->getSize(),
            'is_valid' => true,
        ];
    }

    /**
     * Detect the actual MIME type of a file using magic bytes.
     */
    public function detectMimeType(UploadedFile $file): string
    {
        $filePath = $file->getRealPath();

        // First, try to detect using magic bytes
        $magicByteMime = $this->detectByMagicBytes($filePath);

        if ($magicByteMime) {
            return $magicByteMime;
        }

        // Fallback to finfo
        $finfo = new \finfo(FILEINFO_MIME_TYPE);
        $mimeType = $finfo->file($filePath);

        return $mimeType ?: 'application/octet-stream';
    }

    /**
     * Detect MIME type by reading magic bytes.
     */
    protected function detectByMagicBytes(string $filePath): ?string
    {
        $handle = fopen($filePath, 'rb');
        if ($handle === false) {
            return null;
        }

        $bytes = fread($handle, 32);
        fclose($handle);

        if ($bytes === false || strlen($bytes) === 0) {
            return null;
        }

        $hexBytes = strtoupper(bin2hex($bytes));
        $magicSignatures = config('file_upload.magic_bytes', []);

        foreach ($magicSignatures as $mimeType => $signatures) {
            foreach ($signatures as $signature) {
                // Handle wildcards (?) in signatures
                $pattern = '/^'.str_replace('?', '.', $signature).'/i';

                if (preg_match($pattern, $hexBytes)) {
                    return $mimeType;
                }
            }
        }

        return null;
    }

    /**
     * Get the magic bytes of a file (for debugging/analysis).
     */
    public function getMagicBytes(string $filePath, int $length = 12): string
    {
        $handle = fopen($filePath, 'rb');
        if ($handle === false) {
            throw new RuntimeException("Cannot open file: {$filePath}");
        }

        $bytes = fread($handle, $length);
        fclose($handle);

        if ($bytes === false) {
            throw new RuntimeException("Cannot read file: {$filePath}");
        }

        return strtoupper(bin2hex($bytes));
    }

    /**
     * Validate file size against maximum allowed.
     */
    public function validateFileSize(UploadedFile $file, ?int $maxSize = null): void
    {
        $maxSize = $maxSize ?? config('file_upload.max_file_size', 104857600);
        $fileSize = $file->getSize();

        if ($fileSize > $maxSize) {
            $maxSizeMB = round($maxSize / 1048576, 2);
            $fileSizeMB = round($fileSize / 1048576, 2);

            throw new RuntimeException(
                "File size ({$fileSizeMB}MB) exceeds the maximum allowed size ({$maxSizeMB}MB)."
            );
        }
    }

    /**
     * Validate file size for a chunk.
     */
    public function validateChunkSize(int $chunkSize, ?int $maxChunkSize = null): void
    {
        $maxChunkSize = $maxChunkSize ?? config('file_upload.default_chunk_size', 5242880) * 2;

        if ($chunkSize > $maxChunkSize) {
            throw new RuntimeException(
                "Chunk size exceeds the maximum allowed size."
            );
        }
    }

    /**
     * Get expected MIME types for a file extension.
     */
    public function getMimeTypesForExtension(string $extension): array
    {
        $map = config('file_upload.extension_mime_map', []);

        $extension = strtolower($extension);

        if (isset($map[$extension])) {
            return [$map[$extension]];
        }

        // Some extensions map to multiple MIME types
        $multiMap = [
            'jpg' => ['image/jpeg'],
            'jpeg' => ['image/jpeg'],
            'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip'],
            'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/zip'],
            'pptx' => ['application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/zip'],
        ];

        return $multiMap[$extension] ?? [];
    }

    /**
     * Calculate the checksum of a file.
     */
    public function calculateChecksum(string $filePath): string
    {
        return hash_file('sha256', $filePath);
    }

    /**
     * Calculate the checksum of content.
     */
    public function calculateChecksumFromContent(string $content): string
    {
        return hash('sha256', $content);
    }

    /**
     * Verify a file's checksum.
     */
    public function verifyChecksum(string $filePath, string $expectedChecksum): bool
    {
        return hash_equals($expectedChecksum, $this->calculateChecksum($filePath));
    }

    /**
     * Check if a MIME type is allowed.
     */
    public function isMimeTypeAllowed(string $mimeType, ?array $allowedMimeTypes = null): bool
    {
        $allowedMimeTypes = $allowedMimeTypes ?? config('file_upload.allowed_mime_types', []);

        if (empty($allowedMimeTypes)) {
            return true;
        }

        return in_array($mimeType, $allowedMimeTypes);
    }

    /**
     * Validate content for the first chunk (detect MIME type early).
     */
    public function validateFirstChunk(string $content, string $filename, ?array $allowedMimeTypes = null): array
    {
        // Save to temp file for analysis
        $tempFile = tempnam(sys_get_temp_dir(), 'upload_');
        file_put_contents($tempFile, $content);

        try {
            $magicBytes = $this->getMagicBytes($tempFile);
            $detectedMime = $this->detectByMagicBytes($tempFile);

            if ($detectedMime && ! $this->isMimeTypeAllowed($detectedMime, $allowedMimeTypes)) {
                throw new RuntimeException(
                    "File type '{$detectedMime}' is not allowed."
                );
            }

            return [
                'magic_bytes' => $magicBytes,
                'mime_type' => $detectedMime,
                'checksum' => $this->calculateChecksumFromContent($content),
                'size' => strlen($content),
            ];
        } finally {
            unlink($tempFile);
        }
    }
}
