<?php

namespace App\Domain\Documents\Services;

use App\Domain\Documents\Models\Document;
use App\Domain\Documents\Models\DocumentType;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

/**
 * Physical storage of document files (§17.3, §68A).
 *
 * Files live on a private disk under randomized names, are validated by real
 * MIME type rather than the client's claim, and are only ever handed out
 * through short-lived signed links.
 */
class DocumentStorage
{
    public function disk(): Filesystem
    {
        return Storage::disk(config('mams.documents.disk'));
    }

    /**
     * Validates an upload against its document type and stores it.
     *
     * @return array{path: string, checksum: string, mime: string, size: int, original_name: string}
     *
     * @throws ValidationException
     */
    public function store(UploadedFile $file, DocumentType $type, string $ownerKey): array
    {
        $this->assertIsSafe($file, $type);

        // The stored name reveals nothing and cannot be guessed.
        $extension = $this->safeExtension($file);
        $name = Str::uuid()->toString().($extension ? ".{$extension}" : '');
        $directory = trim(config('mams.documents.root'), '/')."/{$ownerKey}/{$type->code}";

        $path = $this->disk()->putFileAs($directory, $file, $name, ['visibility' => 'private']);

        if ($path === false) {
            throw ValidationException::withMessages([
                'file' => ['The file could not be stored. Try again.'],
            ]);
        }

        return [
            'path' => $path,
            'checksum' => hash_file('sha256', $file->getRealPath()),
            'mime' => $file->getMimeType(),
            'size' => $file->getSize(),
            'original_name' => $this->safeOriginalName($file),
        ];
    }

    /** Short-lived link; never a permanent public URL (§17B.15). */
    public function temporaryUrl(Document $document): ?string
    {
        $disk = $this->disk();

        if (! method_exists($disk, 'temporaryUrl')) {
            return null;
        }

        try {
            return $disk->temporaryUrl(
                $document->file_path,
                now()->addMinutes((int) config('mams.documents.link_ttl_minutes')),
            );
        } catch (\RuntimeException) {
            // The local driver cannot sign URLs; the app streams instead.
            return null;
        }
    }

    public function exists(Document $document): bool
    {
        return $this->disk()->exists($document->file_path);
    }

    public function delete(Document $document): void
    {
        $this->disk()->delete($document->file_path);
    }

    /** @throws ValidationException */
    private function assertIsSafe(UploadedFile $file, DocumentType $type): void
    {
        if (! $file->isValid()) {
            throw ValidationException::withMessages(['file' => ['The upload did not complete.']]);
        }

        $extension = Str::lower($file->getClientOriginalExtension());
        $blocked = config('mams.documents.blocked_extensions');

        // An executable or script must never reach storage, whatever it claims.
        if (in_array($extension, $blocked, true)) {
            throw ValidationException::withMessages([
                'file' => ['That file type is not allowed.'],
            ]);
        }

        // Trust the sniffed type, not the Content-Type header the client sent.
        $mime = $file->getMimeType();

        if (! in_array($mime, $type->acceptedMimeTypes(), true)) {
            throw ValidationException::withMessages([
                'file' => ["A {$type->name} must be one of: ".implode(', ', $type->acceptedMimeTypes()).'.'],
            ]);
        }

        if ($file->getSize() > $type->maxFileSizeBytes()) {
            $limit = round($type->max_file_size_kb / 1024, 1);

            throw ValidationException::withMessages([
                'file' => ["The file must be smaller than {$limit} MB."],
            ]);
        }
    }

    private function safeExtension(UploadedFile $file): string
    {
        $extension = Str::lower($file->getClientOriginalExtension());

        return preg_match('/^[a-z0-9]{1,8}$/', $extension) ? $extension : 'bin';
    }

    /** Keeps a readable download name without letting path characters through. */
    private function safeOriginalName(UploadedFile $file): string
    {
        $name = $file->getClientOriginalName();

        return Str::limit(preg_replace('/[^\w\-. ]/u', '_', $name) ?: 'document', 200, '');
    }
}
