<?php

namespace App\Services;

use App\Models\Attachment;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\StreamedResponse;

class AttachmentService
{
    private const STORAGE_DIR = 'attachments';

    /**
     * @var list<string>
     */
    private const ALLOWED_EXTENSIONS = [
        'pdf', 'jpg', 'jpeg', 'png', 'gif', 'webp',
        'doc', 'docx', 'xls', 'xlsx', 'txt', 'csv',
    ];

    /**
     * @var list<string>
     */
    private const ALLOWED_MIMES = [
        'application/pdf',
        'image/jpeg',
        'image/png',
        'image/gif',
        'image/webp',
        'application/msword',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'application/vnd.ms-excel',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'text/plain',
        'text/csv',
        'application/csv',
        'text/comma-separated-values',
    ];

    /**
     * @var list<string>
     */
    private const BLOCKED_EXTENSIONS = [
        'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar',
        'exe', 'bat', 'cmd', 'com', 'sh', 'bash', 'js', 'jsx', 'ts', 'tsx',
        'html', 'htm', 'svg', 'htaccess', 'env',
    ];

    public function __construct(
        private readonly SettingsService $settings,
        private readonly AuditService $audit,
    ) {}

    public function store(UploadedFile $file, Model $attachable, ?User $user = null, ?string $description = null): Attachment
    {
        $this->validateFile($file);

        $extension = strtolower($file->getClientOriginalExtension());
        $storedFilename = Str::uuid()->toString().'.'.$extension;
        $diskPath = self::STORAGE_DIR.'/'.$storedFilename;

        $file->storeAs(self::STORAGE_DIR, $storedFilename, 'local');

        $attachment = Attachment::query()->create([
            'attachable_type' => $attachable->getMorphClass(),
            'attachable_id' => $attachable->getKey(),
            'original_filename' => $this->sanitizeFilename($file->getClientOriginalName()),
            'stored_filename' => $storedFilename,
            'mime_type' => (string) $file->getMimeType(),
            'extension' => $extension,
            'file_size' => $file->getSize(),
            'disk_path' => $diskPath,
            'uploaded_by' => $user?->id ?? Auth::id(),
            'description' => $description,
        ]);

        $this->audit->log('attachment.uploaded', $attachment, null, [
            'original_filename' => $attachment->original_filename,
            'attachable_type' => $attachment->attachable_type,
            'attachable_id' => $attachment->attachable_id,
        ]);

        return $attachment;
    }

    public function download(Attachment $attachment): StreamedResponse
    {
        $path = $this->resolveStoragePath($attachment);

        if (! Storage::disk('local')->exists($path)) {
            abort(404, 'Attachment file not found.');
        }

        return Storage::disk('local')->download(
            $path,
            $attachment->original_filename,
            ['Content-Type' => $attachment->mime_type],
        );
    }

    public function delete(Attachment $attachment): void
    {
        $path = $this->resolveStoragePath($attachment);

        if (Storage::disk('local')->exists($path)) {
            Storage::disk('local')->delete($path);
        }

        $this->audit->log('attachment.deleted', $attachment, $attachment->toArray(), null);

        $attachment->delete();
    }

    private function validateFile(UploadedFile $file): void
    {
        if (! $file->isValid()) {
            abort(422, 'The uploaded file is invalid.');
        }

        $extension = strtolower($file->getClientOriginalExtension());
        $mime = strtolower((string) $file->getMimeType());

        if ($extension === '' || in_array($extension, self::BLOCKED_EXTENSIONS, true)) {
            abort(422, 'This file type is not allowed.');
        }

        if (! in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
            abort(422, 'This file extension is not allowed.');
        }

        if (! in_array($mime, self::ALLOWED_MIMES, true)) {
            abort(422, 'This MIME type is not allowed.');
        }

        $maxBytes = $this->maxFileSizeBytes();

        if ($file->getSize() > $maxBytes) {
            abort(422, 'File exceeds the maximum allowed size of '.$this->maxFileSizeMb().' MB.');
        }

        $originalName = $file->getClientOriginalName();

        if (str_contains($originalName, '..') || str_contains($originalName, '/') || str_contains($originalName, '\\')) {
            abort(422, 'Invalid filename.');
        }
    }

    private function resolveStoragePath(Attachment $attachment): string
    {
        $path = str_replace('\\', '/', $attachment->disk_path);

        if (str_contains($path, '..') || ! str_starts_with($path, self::STORAGE_DIR.'/')) {
            abort(403, 'Invalid attachment path.');
        }

        return $path;
    }

    private function sanitizeFilename(string $filename): string
    {
        $filename = basename(str_replace('\\', '/', $filename));

        return preg_replace('/[^\w\s\.\-()]/u', '_', $filename) ?: 'attachment';
    }

    private function maxFileSizeMb(): int
    {
        return max(1, (int) $this->settings->get('attachment_max_size_mb', 10));
    }

    private function maxFileSizeBytes(): int
    {
        return $this->maxFileSizeMb() * 1024 * 1024;
    }
}
