<?php

namespace App\Services;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\StreamedResponse;

class SecureFileService
{
    private const MAX_BYTES = 25 * 1024 * 1024;

    private const DENIED_EXTENSIONS = [
        'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar',
        'cgi', 'pl', 'py', 'sh', 'bash', 'cmd', 'bat', 'ps1',
        'exe', 'dll', 'com', 'msi', 'js', 'mjs', 'html', 'htm', 'svg',
    ];

    private const DENIED_MIME_TYPES = [
        'application/x-httpd-php',
        'application/x-php',
        'application/x-sh',
        'application/x-executable',
        'application/x-msdownload',
        'image/svg+xml',
        'text/html',
        'text/javascript',
        'application/javascript',
    ];

    public function store(UploadedFile $file, string $directory): string
    {
        $directory = $this->safePath($directory);
        $this->assertSafeUpload($file);

        $path = $file->store($directory, 'local');
        if (! is_string($path) || $path === '') {
            throw ValidationException::withMessages(['file' => 'The private file could not be stored.']);
        }

        return $path;
    }

    public function delete(?string $path): void
    {
        if ($path) {
            Storage::disk('local')->delete($this->safePath($path));
        }
    }

    public function download(string $path, ?string $downloadName = null): StreamedResponse
    {
        $path = $this->safePath($path);
        abort_unless(Storage::disk('local')->exists($path), 404);

        $response = Storage::disk('local')->download($path, $downloadName);
        $this->applyPrivateHeaders($response);

        return $response;
    }

    public function inline(string $path, ?string $displayName = null, ?string $mimeType = null): StreamedResponse
    {
        $path = $this->safePath($path);
        abort_unless(Storage::disk('local')->exists($path), 404);

        $mimeType = $mimeType ?: Storage::disk('local')->mimeType($path) ?: 'application/octet-stream';
        $response = Storage::disk('local')->response(
            $path,
            $displayName,
            ['Content-Type' => $mimeType],
            'inline',
        );
        $this->applyPrivateHeaders($response);

        return $response;
    }

    private function applyPrivateHeaders(StreamedResponse $response): void
    {
        $response->headers->set('Cache-Control', 'private, no-store, max-age=0');
        $response->headers->set('Pragma', 'no-cache');
        $response->headers->set('X-Content-Type-Options', 'nosniff');
    }

    private function assertSafeUpload(UploadedFile $file): void
    {
        if (! $file->isValid()) {
            throw ValidationException::withMessages(['file' => 'The uploaded file is invalid.']);
        }

        if (($file->getSize() ?? 0) > self::MAX_BYTES) {
            throw ValidationException::withMessages(['file' => 'Private uploads may not exceed 25 MB.']);
        }

        $extension = strtolower($file->getClientOriginalExtension());
        if (in_array($extension, self::DENIED_EXTENSIONS, true)) {
            throw ValidationException::withMessages(['file' => 'This file type is not permitted.']);
        }

        $mime = strtolower((string) $file->getMimeType());
        if (in_array($mime, self::DENIED_MIME_TYPES, true)) {
            throw ValidationException::withMessages(['file' => 'This file content type is not permitted.']);
        }
    }

    private function safePath(string $path): string
    {
        $path = trim(str_replace('\\', '/', $path), '/');
        if ($path === '' || str_contains($path, "\0") || str_contains($path, '../') || str_starts_with($path, '..')) {
            throw ValidationException::withMessages(['file' => 'Invalid private file path.']);
        }

        return $path;
    }
}
