<?php

namespace Botble\Media;

use Botble\Base\Facades\AdminHelper;
use Botble\Base\Facades\BaseHelper;
use Botble\Base\Facades\Html;
use Botble\Media\Events\MediaFileRenamed;
use Botble\Media\Events\MediaFileRenaming;
use Botble\Media\Events\MediaFileUploaded;
use Botble\Media\Events\MediaFolderRenamed;
use Botble\Media\Events\MediaFolderRenaming;
use Botble\Media\Http\Resources\FileResource;
use Botble\Media\Models\MediaFile;
use Botble\Media\Models\MediaFolder;
use Botble\Media\Services\ThumbnailService;
use Botble\Media\Services\UploadsManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\File as ValidationFile;
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
use Intervention\Image\Encoders\AutoEncoder;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\ImageManager;
use League\Flysystem\UnableToWriteFile;
use Symfony\Component\Mime\MimeTypes;
use Throwable;

class RvMedia
{
    protected array $permissions = [];

    public function __construct(protected UploadsManager $uploadManager, protected ThumbnailService $thumbnailService)
    {
        $this->permissions = $this->getConfig('permissions', []);
    }

    public function renderHeader(): string
    {
        $urls = $this->getUrls();

        return view('core/media::header', compact('urls'))->render();
    }

    public function getUrls(): array
    {
        return [
            'base_url' => url(''),
            'base' => route('media.index'),
            'get_media' => route('media.list'),
            'create_folder' => route('media.folders.create'),
            'popup' => route('media.popup'),
            'download' => route('media.download'),
            'upload_file' => route('media.files.upload'),
            'get_breadcrumbs' => route('media.breadcrumbs'),
            'global_actions' => route('media.global_actions'),
            'media_upload_from_editor' => route('media.files.upload.from.editor'),
            'download_url' => route('media.download_url'),
        ];
    }

    public function renderFooter(): string
    {
        return view('core/media::footer')->render();
    }

    public function renderContent(): string
    {
        $sorts = [
            'name-asc' => [
                'label' => trans('core/media::media.file_name_asc'),
                'icon' => 'ti ti-sort-ascending-letters',
            ],
            'name-desc' => [
                'label' => trans('core/media::media.file_name_desc'),
                'icon' => 'ti ti-sort-descending-letters',
            ],
            'created_at-asc' => [
                'label' => trans('core/media::media.uploaded_date_asc'),
                'icon' => 'ti ti-sort-ascending-numbers',
            ],
            'created_at-desc' => [
                'label' => trans('core/media::media.uploaded_date_desc'),
                'icon' => 'ti ti-sort-descending-numbers',
            ],
            'size-asc' => [
                'label' => trans('core/media::media.size_asc'),
                'icon' => 'ti ti-sort-ascending-2',
            ],
            'size-desc' => [
                'label' => trans('core/media::media.size_desc'),
                'icon' => 'ti ti-sort-descending-2',
            ],
        ];

        return view('core/media::content', compact('sorts'))->render();
    }

    public function responseSuccess(array $data, ?string $message = null): JsonResponse
    {
        return response()->json([
            'error' => false,
            'data' => $data,
            'message' => $message,
        ]);
    }

    public function responseError(
        string $message,
        array $data = [],
        ?int $code = null,
        int $status = 200
    ): JsonResponse {
        return response()->json([
            'error' => true,
            'message' => $message,
            'data' => $data,
            'code' => $code,
        ], $status);
    }

    public function getAllImageSizes(?string $url): array
    {
        $images = [];
        foreach ($this->getSizes() as $size) {
            $readableSize = explode('x', $size);
            $images = $this->getImageUrl($url, $readableSize);
        }

        return $images;
    }

    public function getSizes(): array
    {
        $sizes = $this->getConfig('sizes', []);

        foreach ($sizes as $name => $size) {
            $size = explode('x', $size);

            $settingName = 'media_sizes_' . $name;

            $width = setting($settingName . '_width', $size[0]);

            $height = setting($settingName . '_height', $size[1]);

            if (! $width && ! $height) {
                unset($sizes[$name]);

                continue;
            }

            if (! $width) {
                $width = 'auto';
            }

            if (! $height) {
                $height = 'auto';
            }

            $sizes[$name] = $width . 'x' . $height;
        }

        return $sizes;
    }

    public function getImageUrl(
        ?string $url,
        $size = null,
        bool $relativePath = false,
        $default = null
    ): ?string {
        if (empty($url)) {
            return $default;
        }

        $url = trim($url);

        if (empty($url)) {
            return $default;
        }

        if (Str::startsWith($url, ['data:image/png;base64,', 'data:image/jpeg;base64,', 'http://', 'https://'])) {
            return $url;
        }

        if (empty($size) || $url == '__value__') {
            if ($relativePath) {
                return $url;
            }

            return $this->url($url);
        }

        if ($url == $this->getDefaultImage(false, $size)) {
            return url($url);
        }

        if (
            setting('media_enable_thumbnail_sizes', true) &&
            array_key_exists($size, $this->getSizes()) &&
            $this->canGenerateThumbnails($this->getMimeType($this->getRealPath($url)))
        ) {
            $fileName = File::name($url);
            $fileExtension = File::extension($url);

            $url = str_replace(
                $fileName . '.' . $fileExtension,
                $fileName . '-' . $this->getSize($size) . '.' . $fileExtension,
                $url
            );
        }

        if ($relativePath) {
            return $url;
        }

        if ($url == '__image__') {
            return $this->url($default);
        }

        return $this->url($url);
    }

    public function url(?string $path): string
    {
        $path = $path ? trim($path) : $path;

        if (Str::contains($path, ['http://', 'https://'])) {
            return $path;
        }

        if ($this->getMediaDriver() === 'do_spaces' && (int) setting('media_do_spaces_cdn_enabled')) {
            $customDomain = setting('media_do_spaces_cdn_custom_domain');

            if ($customDomain) {
                return $customDomain . '/' . ltrim($path, '/');
            }

            return str_replace('.digitaloceanspaces.com', '.cdn.digitaloceanspaces.com', Storage::url($path));
        } else {
            if ($this->getMediaDriver() === 'backblaze' && (int) setting('media_backblaze_cdn_enabled')) {
                $customDomain = setting('media_backblaze_cdn_custom_domain');
                $currentEndpoint = setting('media_backblaze_endpoint');
                if ($customDomain) {
                    return $customDomain . '/' . ltrim($path, '/');
                }

                return str_replace($currentEndpoint, $customDomain, Storage::url($path));
            }
        }

        return Storage::url($path);
    }

    public function getDefaultImage(bool $relative = false, ?string $size = null): string
    {
        $default = $this->getConfig('default_image');

        if ($placeholder = setting('media_default_placeholder_image')) {
            $filename = pathinfo($placeholder, PATHINFO_FILENAME);

            if ($size && $size = $this->getSize($size)) {
                $placeholder = str_replace($filename, $filename . '-' . $size, $placeholder);
            }

            return Storage::url($placeholder);
        }

        if ($relative) {
            return $default;
        }

        return $default ? url($default) : $default;
    }

    public function getSize(string $name): ?string
    {
        return Arr::get($this->getSizes(), $name);
    }

    public function deleteFile(MediaFile $file): bool
    {
        $this->deleteThumbnails($file);

        if (! $this->isUsingCloud() && ! Storage::exists($file->url)) {
            return false;
        }

        return Storage::delete($file->url);
    }

    public function deleteThumbnails(MediaFile $file): bool
    {
        if (! $file->canGenerateThumbnails()) {
            return false;
        }

        $filename = pathinfo($file->url, PATHINFO_FILENAME);

        $files = [];
        foreach ($this->getSizes() as $size) {
            $files[] = str_replace($filename, $filename . '-' . $size, $file->url);
        }

        return Storage::delete($files);
    }

    public function getPermissions(): array
    {
        return $this->permissions;
    }

    public function setPermissions(array $permissions): void
    {
        $this->permissions = $permissions;
    }

    public function removePermission(string $permission): void
    {
        Arr::forget($this->permissions, $permission);
    }

    public function addPermission(string $permission): void
    {
        $this->permissions[] = $permission;
    }

    public function hasPermission(string $permission): bool
    {
        return in_array($permission, $this->permissions);
    }

    public function hasAnyPermission(array $permissions): bool
    {
        $hasPermission = false;
        foreach ($permissions as $permission) {
            if (in_array($permission, $this->permissions)) {
                $hasPermission = true;

                break;
            }
        }

        return $hasPermission;
    }

    public function addSize(string $name, int|string $width, int|string $height = 'auto'): self
    {
        if (! $width) {
            $width = 'auto';
        }

        if (! $height) {
            $height = 'auto';
        }

        config(['core.media.media.sizes.' . $name => $width . 'x' . $height]);

        return $this;
    }

    public function removeSize(string $name): self
    {
        $sizes = $this->getSizes();
        Arr::forget($sizes, $name);

        config(['core.media.media.sizes' => $sizes]);

        return $this;
    }

    public function uploadFromEditor(
        Request $request,
        int|string|null $folderId = 0,
        $folderName = null,
        string $fileInput = 'upload'
    ) {
        $validator = Validator::make($request->all(), [
            'upload' => $this->imageValidationRule(),
        ], [
            'upload.required' => trans('core/media::media.validation.uploaded_file_required'),
            'upload.image' => trans('core/media::media.validation.uploaded_file_invalid_type'),
            'upload.mimes' => trans('core/media::media.validation.uploaded_file_invalid_type'),
        ], [
            'upload' => trans('core/media::media.validation.attributes.uploaded_file'),
        ]);

        if ($validator->fails()) {
            $errorMessage = $validator->getMessageBag()->first();

            return response('<script>alert("' . addslashes($errorMessage) . '")</script>')
                ->header('Content-Type', 'text/html');
        }

        $folderName = $folderName ?: $request->input('upload_type');

        $result = $this->handleUpload($request->file($fileInput), $folderId, $folderName);

        if (! $result['error']) {
            $file = $result['data'];
            if (! $request->input('CKEditorFuncNum')) {
                return response()->json([
                    'fileName' => File::name($this->url($file->url)),
                    'uploaded' => 1,
                    'url' => $this->url($file->url),
                ]);
            }

            return response(
                '<script>window.parent.CKEDITOR.tools.callFunction("' . $request->input('CKEditorFuncNum') .
                '", "' . $this->url($file->url) . '", "");</script>'
            )
                ->header('Content-Type', 'text/html');
        }

        return response('<script>alert("' . Arr::get($result, 'message') . '")</script>')
            ->header('Content-Type', 'text/html');
    }

    protected function getCustomS3Path(): string
    {
        $customPath = trim(setting('media_s3_path', $this->getConfig('custom_s3_path')), '/');
        $customPath = apply_filters('core_media_custom_s3_path', $customPath);

        return $customPath ? $customPath . '/' : '';
    }

    public function handleUpload(
        ?UploadedFile $fileUpload,
        int|string|null $folderId = 0,
        ?string $folderSlug = null,
        bool $skipValidation = false,
        string $visibility = 'public'
    ): array {
        $fileUpload = apply_filters('core_media_file_upload', $fileUpload);

        $request = request();

        if ($uploadPath = $request->input('path')) {
            $folderId = $this->handleTargetFolder($folderId, $uploadPath);
        }

        if (! $fileUpload) {
            return [
                'error' => true,
                'message' => trans('core/media::media.can_not_detect_file_type'),
            ];
        }

        $allowedMimeTypes = $this->getConfig('allowed_mime_types');

        $allowedToUploadAnyFileTypes = AdminHelper::isInAdmin(true) && $this->getConfig(
            'allowed_admin_to_upload_any_file_types',
            false
        );

        if (! $this->isChunkUploadEnabled()) {
            if (! $skipValidation) {
                $rules = ['required'];

                if (! $allowedToUploadAnyFileTypes) {
                    $rules[] = ValidationFile::types(explode(',', $allowedMimeTypes));
                }

                $validator = Validator::make(['uploaded_file' => $fileUpload], [
                    'uploaded_file' => $rules,
                ], [
                    'uploaded_file.required' => trans('core/media::media.validation.uploaded_file_required'),
                    'uploaded_file.file' => trans('core/media::media.validation.uploaded_file_invalid_type'),
                    'uploaded_file.types' => trans('core/media::media.validation.uploaded_file_invalid_type'),
                ], [
                    'uploaded_file' => trans('core/media::media.validation.attributes.uploaded_file'),
                ]);

                if ($validator->fails()) {
                    return [
                        'error' => true,
                        'message' => $validator->getMessageBag()->first(),
                    ];
                }
            }

            $maxUploadFilesizeAllowed = setting('max_upload_filesize');

            if (
                $maxUploadFilesizeAllowed
                && ($fileUpload->getSize() / 1024) / 1024 > (float) $maxUploadFilesizeAllowed
            ) {
                return [
                    'error' => true,
                    'message' => trans('core/media::media.file_too_big_readable_size', [
                        'size' => BaseHelper::humanFilesize($maxUploadFilesizeAllowed * 1024 * 1024),
                    ]),
                ];
            }

            $maxSize = $this->getServerConfigMaxUploadFileSize();

            if ($fileUpload->getSize() / 1024 > (int) $maxSize) {
                return [
                    'error' => true,
                    'message' => trans('core/media::media.file_too_big_readable_size', [
                        'size' => BaseHelper::humanFilesize($maxSize * 1024),
                    ]),
                ];
            }
        }

        $extraValidation = apply_filters('core_media_extra_validation', [], $fileUpload);

        if ($extraValidation && Arr::get($extraValidation, 'error')) {
            return [
                'error' => true,
                'message' => $extraValidation['message'],
            ];
        }

        try {
            $fileExtension = $fileUpload->getClientOriginalExtension() ?: $fileUpload->guessExtension();

            $fileExtension = strtolower($fileExtension);

            if (
                ! $skipValidation
                && ! in_array(strtolower($fileExtension), explode(',', $allowedMimeTypes))
                && ! $allowedToUploadAnyFileTypes
            ) {
                return [
                    'error' => true,
                    'message' => trans('core/media::media.validation.uploaded_file_invalid_type'),
                ];
            }

            if ($folderId == 0 && ! empty($folderSlug)) {
                if (str_contains($folderSlug, '/')) {
                    $paths = array_filter(explode('/', $folderSlug));
                    foreach ($paths as $folder) {
                        $folderId = $this->createFolder($folder, $folderId, true);
                    }
                } else {
                    $folderId = $this->createFolder($folderSlug, $folderId, true);
                }
            }

            $file = new MediaFile();

            $fileName = apply_filters(
                'core_media_upload_filename',
                File::name($fileUpload->getClientOriginalName()),
                $fileUpload
            );

            $file->name = MediaFile::createName($fileName, $folderId);

            $folderPath = MediaFolder::getFullPath($folderId);

            $fileName = MediaFile::createSlug(
                $file->name,
                $fileExtension,
                $folderPath ?: ''
            );

            $filePath = $fileName;

            if ($folderPath) {
                $filePath = $folderPath . '/' . $filePath;
            }

            if ($this->getMediaDriver() === 's3') {
                $filePath = $this->getCustomS3Path() . $filePath;
            }

            if ($this->canGenerateThumbnails($fileUpload->getMimeType())) {
                $originalFilePath = $filePath;

                try {
                    $encoder = new AutoEncoder();
                    $shouldConvertToWebp = in_array($fileExtension, ['jpg', 'jpeg', 'png'])
                        && setting('media_convert_image_to_webp', false);

                    $keepOriginalQuality = setting('media_keep_original_file_size_and_quality');

                    if ($shouldConvertToWebp) {
                        $encoder = new WebpEncoder();

                        if ($keepOriginalQuality) {
                            $encoder = new WebpEncoder(quality: 100);
                        }

                        $dirName = File::dirname($filePath);
                        $filePath = ($dirName === '.' ? '' : $dirName . '/') . File::name($filePath) . '.webp';
                    }

                    if ($keepOriginalQuality && ! $shouldConvertToWebp) {
                        $content = File::get($fileUpload->getRealPath());
                    } else {
                        $image = $this->imageManager()->read($fileUpload->getRealPath());

                        if (
                            ! $keepOriginalQuality
                            && in_array($fileExtension, ['jpg', 'jpeg', 'png', 'webp'])
                            && setting('media_reduce_large_image_size', false)
                        ) {
                            $maxWith = setting('media_image_max_width');

                            $maxHeight = setting('media_image_max_height');

                            if ($maxWith || $maxHeight) {
                                $image->scaleDown($maxWith, $maxHeight);
                            }
                        }

                        $content = (string) $image->encode($encoder);
                    }
                } catch (Throwable $exception) {
                    BaseHelper::logError($exception);

                    $content = File::get($fileUpload->getRealPath());

                    $filePath = $originalFilePath;
                }
            } else {
                $content = File::get($fileUpload->getRealPath());
            }

            $this->uploadManager->saveFile($filePath, $content, $fileUpload, $visibility);

            $data = $this->uploadManager->fileDetails($filePath);

            $file->url = $data['url'];
            $file->alt = $file->name;
            $file->size = $data['size'] ?: $fileUpload->getSize();

            $file->mime_type = $data['mime_type'];
            $file->folder_id = $folderId;
            $file->user_id = Auth::guard()->check() ? Auth::guard()->id() : 0;
            $file->options = $request->input('options', []);

            $file->visibility = $visibility;

            $file->save();

            MediaFileUploaded::dispatch($file);

            do_action('core_media_file_uploaded', $file);

            $customizedGeneratedThumbnails = apply_filters('core_media_customized_generate_thumbnails_function', null);

            if (! $customizedGeneratedThumbnails) {
                $this->generateThumbnails($file, $fileUpload);
            }

            return [
                'error' => false,
                'data' => new FileResource($file),
            ];
        } catch (UnableToWriteFile $exception) {
            $message = $exception->getMessage();

            if (! $this->isUsingCloud()) {
                $message = trans('core/media::media.unable_to_write', ['folder' => $this->getUploadPath()]);
            }

            return [
                'error' => true,
                'message' => $message,
            ];
        } catch (Throwable $exception) {
            return [
                'error' => true,
                'message' => $exception->getMessage() ?: trans('core/media::media.validation.upload_network_error'),
            ];
        }
    }

    /**
     * Returns a file size limit in bytes based on the PHP upload_max_filesize and post_max_size
     */
    public function getServerConfigMaxUploadFileSize(): float
    {
        // Start with post_max_size.
        $maxSize = $this->parseSize(@ini_get('post_max_size'));

        // If upload_max_size is less, then reduce. Except if upload_max_size is
        // zero, which indicates no limit.
        $uploadMax = $this->parseSize(@ini_get('upload_max_filesize'));
        if ($uploadMax > 0 && $uploadMax < $maxSize) {
            $maxSize = $uploadMax;
        }

        return $maxSize;
    }

    public function parseSize(int|string $size): float
    {
        $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size.
        $size = (int) preg_replace('/[^0-9\.]/', '', $size); // Remove the non-numeric characters from the size.
        if ($unit) {
            return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
        }

        return round($size);
    }

    public function generateThumbnails(MediaFile $file, ?UploadedFile $fileUpload = null): bool
    {
        if (! $file->canGenerateThumbnails()) {
            return false;
        }

        if (! $this->isUsingCloud() && ! File::exists($this->getRealPath($file->url))) {
            return false;
        }

        $folderIds = json_decode(setting('media_folders_can_add_watermark', ''), true);

        if (
            empty($folderIds) ||
            in_array($file->folder_id, $folderIds) ||
            ! empty(array_intersect($file->folder->parents->pluck('id')->all(), $folderIds))
        ) {
            $this->insertWatermark($file->url);
        }

        if (! setting('media_enable_thumbnail_sizes', true)) {
            return false;
        }

        foreach ($this->getSizes() as $size) {
            $readableSize = explode('x', $size);

            if (! $fileUpload || $this->isChunkUploadEnabled()) {
                $fileUpload = $this->getRealPath($file->url);

                if ($this->isUsingCloud()) {
                    $fileUpload = @file_get_contents($fileUpload);

                    if (! $fileUpload) {
                        continue;
                    }
                }
            }

            $thumbnailFileName = File::name($file->url) . '-' . $size . '.' . File::extension($file->url);
            $dirName = File::dirname($file->url);
            $thumbnailPath = ($dirName === '.' || ! $dirName) ? $thumbnailFileName : $dirName . '/' . $thumbnailFileName;

            if (! $this->isUsingCloud() && Storage::exists($thumbnailPath)) {
                continue;
            }

            $this->thumbnailService
                ->setImage($fileUpload)
                ->setSize($readableSize[0], $readableSize[1])
                ->setDestinationPath(File::dirname($file->url))
                ->setFileName($thumbnailFileName)
                ->save();
        }

        return true;
    }

    public function insertWatermark(string $image): bool
    {
        if (! $image || ! setting('media_watermark_enabled', $this->getConfig('watermark.enabled'))) {
            return false;
        }

        $watermarkImage = setting('media_watermark_source', $this->getConfig('watermark.source'));

        if (! $watermarkImage) {
            return false;
        }

        $watermarkPath = $this->getRealPath($watermarkImage);

        try {
            if ($this->isUsingCloud()) {
                $watermarkContent = null;
                $imageContent = null;

                try {
                    $watermarkContent = Storage::get($watermarkImage);
                    $imageContent = Storage::get($image);
                } catch (Throwable $exception) {
                    BaseHelper::logError($exception);

                    $watermarkContent = @file_get_contents($watermarkPath);
                    $imageContent = @file_get_contents($this->getRealPath($image));
                }

                if (! $watermarkContent || ! $imageContent) {
                    return false;
                }

                $watermark = $this->imageManager()->read($watermarkContent);
                $imageSource = $this->imageManager()->read($imageContent);
            } else {
                if (! File::exists($watermarkPath)) {
                    return false;
                }

                $watermark = $this->imageManager()->read($watermarkPath);

                $imageSource = $this->imageManager()->read($this->getRealPath($image));
            }
        } catch (Throwable $exception) {
            BaseHelper::logError($exception);

            return false;
        }

        // 10% less than an actual image (play with this value)
        // Watermark will be 10 less than the actual width of the image
        $watermarkSize = (int) round(
            $imageSource->width() * ((int) setting(
                'media_watermark_size',
                $this->getConfig('watermark.size')
            ) / 100),
            2
        );

        // Resize watermark width keep height auto
        $watermark->scale($watermarkSize);

        $imageSource->place(
            $watermark,
            setting('media_watermark_position', $this->getConfig('watermark.position')),
            (int) setting(
                'media_watermark_position_x',
                setting('watermark_position_x') ?: $this->getConfig('watermark.x')
            ),
            (int) setting(
                'media_watermark_position_y',
                setting('watermark_position_y') ?: $this->getConfig('watermark.y')
            ),
            (int) setting(
                'media_watermark_opacity',
                setting('watermark_opacity') ?: $this->getConfig('watermark.opacity')
            )
        );

        $destinationPath = sprintf(
            '%s/%s',
            trim(File::dirname($image), '/'),
            File::name($image) . '.' . File::extension($image)
        );

        $encodedImage = $imageSource->encode(new AutoEncoder());

        $this->uploadManager->saveFile($destinationPath, (string) $encodedImage);

        return true;
    }

    public function getRealPath(?string $url): ?string
    {
        if (empty($url)) {
            return null;
        }

        try {
            $path = $this->isUsingCloud()
                ? Storage::url($url)
                : Storage::path($url);

            return Arr::first(explode('?v=', $path));
        } catch (Throwable $exception) {
            logger()->error('Failed to get real path: ' . $exception->getMessage(), [
                'url' => $url,
                'exception' => $exception,
            ]);

            return null;
        }
    }

    public function isImage(string $mimeType): bool
    {
        return Str::startsWith($mimeType, 'image/');
    }

    public function isUsingCloud(): bool
    {
        return ! in_array($this->getMediaDriver(), ['local', 'public']);
    }

    public function uploadFromUrl(
        string $url,
        int|string $folderId = 0,
        ?string $folderSlug = null,
        ?string $defaultMimetype = null
    ): ?array {
        if (empty($url)) {
            return [
                'error' => true,
                'message' => trans('core/media::media.url_invalid'),
            ];
        }

        $info = pathinfo($url);

        try {
            $response = Http::withoutVerifying()->get($url);

            if ($response->failed() || ! $response->body()) {
                return [
                    'error' => true,
                    'message' => $response->reason() ?: trans(
                        'core/media::media.unable_download_image_from',
                        ['url' => $url]
                    ),
                ];
            }

            $contents = $response->body();
        } catch (Throwable $exception) {
            logger()->error('Failed to download file from URL: ' . $exception->getMessage(), [
                'url' => $url,
                'exception' => $exception,
            ]);

            return [
                'error' => true,
                'message' => $exception->getMessage() ?: trans('core/media::media.validation.upload_network_error'),
            ];
        }

        $path = '/tmp';
        File::ensureDirectoryExists($path);

        $path = $path . '/' . Str::limit($info['basename'], 50, '');
        file_put_contents($path, $contents);

        $fileUpload = $this->newUploadedFile($path, $defaultMimetype);

        $result = $this->handleUpload($fileUpload, $folderId, $folderSlug);

        File::delete($path);

        return $result;
    }

    public function uploadFromPath(
        string $path,
        int|string $folderId = 0,
        ?string $folderSlug = null,
        ?string $defaultMimetype = null
    ): array {
        if (empty($path)) {
            return [
                'error' => true,
                'message' => trans('core/media::media.path_invalid'),
            ];
        }

        $fileUpload = $this->newUploadedFile($path, $defaultMimetype);

        return $this->handleUpload($fileUpload, $folderId, $folderSlug);
    }

    public function uploadFromBlob(
        UploadedFile $path,
        ?string $fileName = null,
        int|string $folderId = 0,
        ?string $folderSlug = null,
    ): array {
        $fileUpload = new UploadedFile($path, $fileName ?: Str::uuid());

        return $this->handleUpload($fileUpload, $folderId, $folderSlug, true);
    }

    protected function newUploadedFile(string $path, ?string $defaultMimeType = null): UploadedFile
    {
        $mimeType = $this->getMimeType($path);

        if (empty($mimeType)) {
            $mimeType = $defaultMimeType;
        }

        $fileName = File::name($path);
        $fileExtension = File::extension($path);

        if (empty($fileExtension) && $mimeType) {
            $mimeTypeDetection = (new MimeTypes())->getExtensions($mimeType);

            $fileExtension = Arr::first($mimeTypeDetection);
        }

        return new UploadedFile($path, $fileName . '.' . $fileExtension, $mimeType, null, true);
    }

    public function getUploadPath(): string
    {
        $customFolder = $this->getConfig('default_upload_folder');

        if (setting('media_customize_upload_path')) {
            $customFolder = trim(setting('media_upload_path'), '/');
        }

        if ($customFolder) {
            return public_path($customFolder);
        }

        return is_link(public_path('storage')) ? storage_path('app/public') : public_path('storage');
    }

    public function getUploadURL(): string
    {
        $uploadUrl = $this->getConfig('default_upload_url') ?: asset('storage');

        if (setting('media_customize_upload_path')) {
            $uploadUrl = trim(asset(setting('media_upload_path')), '/');
        }

        return str_replace('/index.php', '', $uploadUrl);
    }

    public function setUploadPathAndURLToPublic(): static
    {
        add_action('init', function (): void {
            config([
                'filesystems.disks.public.root' => $this->getUploadPath(),
                'filesystems.disks.public.url' => $this->getUploadURL(),
            ]);
        }, 124);

        return $this;
    }

    public function getMimeType(?string $url): ?string
    {
        if (! $url) {
            return null;
        }

        try {
            // For remote URLs (like S3), determine MIME type from extension
            if (Str::contains($url, ['http://', 'https://'])) {
                $fileExtension = pathinfo($url, PATHINFO_EXTENSION);

                if (! $fileExtension) {
                    $realPath = $this->getRealPath($url);

                    if (empty($realPath)) {
                        return null;
                    }

                    $fileExtension = File::extension($realPath);
                }

                if (! $fileExtension) {
                    return null;
                }

                if ($fileExtension == 'jfif') {
                    return 'image/jpeg';
                }

                $mimeType = match (strtolower($fileExtension)) {
                    'ico' => 'image/x-icon',
                    'png' => 'image/png',
                    'jpg', 'jpeg' => 'image/jpeg',
                    'gif' => 'image/gif',
                    'svg' => 'image/svg+xml',
                    'webp' => 'image/webp',
                    'pdf' => 'application/pdf',
                    'doc' => 'application/msword',
                    'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                    'xls' => 'application/vnd.ms-excel',
                    'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                    'ppt' => 'application/vnd.ms-powerpoint',
                    'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
                    'mp3' => 'audio/mpeg',
                    'mp4' => 'video/mp4',
                    'zip' => 'application/zip',
                    'rar' => 'application/x-rar-compressed',
                    'txt' => 'text/plain',
                    'csv' => 'text/csv',
                    default => null,
                };

                if (! $mimeType) {
                    $mimeTypeDetection = new MimeTypes();

                    return Arr::first($mimeTypeDetection->getMimeTypes($fileExtension));
                }

                return $mimeType;
            }

            // For local files, use the existing method
            $realPath = $this->getRealPath($url);

            if (empty($realPath)) {
                return null;
            }

            $fileExtension = File::extension($realPath);

            if (! $fileExtension) {
                return null;
            }

            if ($fileExtension == 'jfif') {
                return 'image/jpeg';
            }

            $mimeTypeDetection = new MimeTypes();

            return Arr::first($mimeTypeDetection->getMimeTypes($fileExtension));
        } catch (Throwable $exception) {
            logger()->error('Failed to get MIME type: ' . $exception->getMessage(), [
                'url' => $url,
                'exception' => $exception,
            ]);

            return null;
        }
    }

    public function canGenerateThumbnails(?string $mimeType): bool
    {
        if (! $this->getConfig('generate_thumbnails_enabled')) {
            return false;
        }

        if (! $mimeType) {
            return false;
        }

        return $this->isImage($mimeType) && ! in_array($mimeType, ['image/svg+xml', 'image/x-icon']);
    }

    public function createFolder(string $folderSlug, int|string|null $parentId = 0, bool $force = false): int|string
    {
        $folder = MediaFolder::query()
            ->where([
                'slug' => $folderSlug,
                'parent_id' => $parentId,
            ])
            ->first();

        if (! $folder) {
            if ($force) {
                MediaFolder::query()
                    ->where([
                        'slug' => $folderSlug,
                        'parent_id' => $parentId,
                    ])
                    ->each(fn (MediaFolder $folder) => $folder->forceDelete());
            }

            $folder = MediaFolder::query()->create([
                'user_id' => Auth::guard()->check() ? Auth::guard()->id() : 0,
                'name' => MediaFolder::createName($folderSlug, $parentId),
                'slug' => MediaFolder::createSlug($folderSlug, $parentId),
                'parent_id' => $parentId,
            ]);
        }

        return $folder->id;
    }

    public function handleTargetFolder(int|string|null $folderId = 0, string $filePath = ''): string
    {
        if (str_contains($filePath, '/')) {
            $paths = array_filter(explode('/', $filePath));
            array_pop($paths);
            foreach ($paths as $folder) {
                $folderId = $this->createFolder($folder, $folderId, true);
            }
        }

        return $folderId;
    }

    public function isChunkUploadEnabled(): bool
    {
        return (bool) setting('media_chunk_enabled', (int) $this->getConfig('chunk.enabled') == 1);
    }

    public function getConfig(?string $key = null, bool|string|null|array $default = null)
    {
        $configs = config('core.media.media');

        if (! $key) {
            return $configs;
        }

        return Arr::get($configs, $key, $default);
    }

    public function imageValidationRule(): string
    {
        return 'required|image|mimes:jpg,jpeg,png,webp,gif,bmp';
    }

    public function turnOffAutomaticUrlTranslationIntoLatin(): bool
    {
        return (int) setting('media_turn_off_automatic_url_translation_into_latin', 0) == 1;
    }

    public function getImageProcessingLibrary(): string
    {
        return setting('media_image_processing_library') ?: 'gd';
    }

    public function getMediaDriver(): string
    {
        return setting('media_driver', 'public');
    }

    public function setS3Disk(array $config): void
    {
        if (
            ! $config['key'] ||
            ! $config['secret'] ||
            ! $config['region'] ||
            ! $config['bucket'] ||
            ! $config['url']
        ) {
            return;
        }

        config()->set([
            'filesystems.disks.s3' => [
                'driver' => 's3',
                'visibility' => 'public',
                'throw' => true,
                'key' => $config['key'],
                'secret' => $config['secret'],
                'region' => $config['region'],
                'bucket' => $config['bucket'],
                'url' => $config['url'],
                'endpoint' => $config['endpoint'],
                'use_path_style_endpoint' => (bool) $config['use_path_style_endpoint'],
            ],
        ]);
    }

    public function setR2Disk(array $config): void
    {
        if (
            ! $config['key'] ||
            ! $config['secret'] ||
            ! $config['bucket'] ||
            ! $config['endpoint']
        ) {
            return;
        }

        config()->set([
            'filesystems.disks.r2' => [
                'driver' => 's3',
                'visibility' => 'public',
                'throw' => true,
                'key' => $config['key'],
                'secret' => $config['secret'],
                'region' => 'auto',
                'bucket' => $config['bucket'],
                'url' => $config['url'],
                'endpoint' => $config['endpoint'],
                'use_path_style_endpoint' => (bool) $config['use_path_style_endpoint'],
            ],
        ]);
    }

    public function setDoSpacesDisk(array $config): void
    {
        if (
            ! $config['key'] ||
            ! $config['secret'] ||
            ! $config['region'] ||
            ! $config['bucket'] ||
            ! $config['endpoint']
        ) {
            return;
        }

        config()->set([
            'filesystems.disks.do_spaces' => [
                'driver' => 's3',
                'visibility' => 'public',
                'throw' => true,
                'key' => $config['key'],
                'secret' => $config['secret'],
                'region' => $config['region'],
                'bucket' => $config['bucket'],
                'endpoint' => $config['endpoint'],
                'use_path_style_endpoint' => (bool) $config['use_path_style_endpoint'],
            ],
        ]);
    }

    public function setWasabiDisk(array $config): void
    {
        if (
            ! $config['key'] ||
            ! $config['secret'] ||
            ! $config['region'] ||
            ! $config['bucket']
        ) {
            return;
        }

        config()->set([
            'filesystems.disks.wasabi' => [
                'driver' => 'wasabi',
                'visibility' => 'public',
                'throw' => true,
                'key' => $config['key'],
                'secret' => $config['secret'],
                'region' => $config['region'],
                'bucket' => $config['bucket'],
                'root' => $config['root'] ?: '/',
            ],
        ]);
    }

    public function setBunnyCdnDisk(array $config): void
    {
        if (
            ! $config['hostname'] ||
            ! $config['storage_zone'] ||
            ! $config['api_key']
        ) {
            return;
        }

        config()->set([
            'filesystems.disks.bunnycdn' => [
                'driver' => 'bunnycdn',
                'visibility' => 'public',
                'throw' => true,
                'hostname' => $config['hostname'],
                'storage_zone' => $config['storage_zone'],
                'api_key' => $config['api_key'],
                'region' => $config['region'],
            ],
        ]);
    }

    public function setBackblazeDisk(array $config): void
    {
        if (
            ! $config['key'] ||
            ! $config['secret'] ||
            ! $config['region'] ||
            ! $config['bucket'] ||
            ! $config['endpoint']
        ) {
            return;
        }

        config()->set([
            'filesystems.disks.backblaze' => [
                'driver' => 's3',
                'visibility' => 'public',
                'throw' => true,
                'key' => $config['key'],
                'secret' => $config['secret'],
                'region' => $config['region'],
                'bucket' => $config['bucket'],
                'url' => $config['url'],
                'endpoint' => str_starts_with(
                    $config['endpoint'],
                    'https://'
                ) ? $config['endpoint'] : 'https://' . $config['endpoint'],
                'use_path_style_endpoint' => (bool) $config['use_path_style_endpoint'],
                'options' => [
                    'StorageClass' => 'STANDARD',
                ],
                'request_checksum_calculation' => 'when_required',
                'response_checksum_validation' => 'when_required',
            ],
        ]);
    }

    public function image(
        ?string $url,
        ?string $alt = null,
        ?string $size = null,
        bool $useDefaultImage = true,
        array $attributes = [],
        ?bool $secure = null,
        ?bool $lazy = true
    ): HtmlString {
        if (! isset($attributes['loading'])) {
            $attributes['loading'] = 'lazy';
        }

        $defaultImageUrl = $this->getDefaultImage(false, $size);

        if (! $url) {
            $url = $defaultImageUrl;
        }

        $url = $this->getImageUrl($url, $size, false, $useDefaultImage ? $defaultImageUrl : null);

        if ($alt) {
            $alt = BaseHelper::clean(strip_tags($alt));
        }

        $attributes = [
            'data-bb-lazy' => $lazy ? 'true' : 'false',
            ...$attributes,
        ];

        if (Str::startsWith($url, ['data:image/png;base64,', 'data:image/jpeg;base64,', 'data:image/jpg;base64,'])) {
            return Html::tag('img', '', [...$attributes, 'src' => $url, 'alt' => $alt]);
        }

        return apply_filters(
            'core_media_image',
            Html::image($url, $alt, $attributes, $secure),
            $url,
            $alt,
            $attributes,
            $secure
        );
    }

    public function getFileSize(?string $path): ?string
    {
        try {
            if (! $path || (! $this->isUsingCloud() && ! Storage::exists($path))) {
                return null;
            }

            $size = Storage::size($path);

            if ($size == 0) {
                return '0kB';
            }
        } catch (Throwable) {
            return null;
        }

        return BaseHelper::humanFilesize($size);
    }

    public function renameFile(MediaFile $file, string $newName, bool $renameOnDisk = true): void
    {
        MediaFileRenaming::dispatch($file, $newName, $renameOnDisk);

        $file->name = MediaFile::createName($newName, $file->folder_id);

        if ($renameOnDisk) {
            $filePath = $this->getRealPath($file->url);

            if (File::exists($filePath)) {
                $newFilePath = str_replace(
                    File::name($file->url),
                    File::name($file->name),
                    $file->url
                );

                File::move($filePath, $this->getRealPath($newFilePath));

                $this->deleteFile($file);

                $file->url = str_replace(
                    File::name($file->url),
                    File::name($file->name),
                    $file->url
                );

                $this->generateThumbnails($file);
            }
        }

        $file->save();

        MediaFileRenamed::dispatch($file);
    }

    public function renameFolder(MediaFolder $folder, string $newName, bool $renameOnDisk = true): void
    {
        MediaFolderRenaming::dispatch($folder, $newName, $renameOnDisk);

        $folder->name = MediaFolder::createName($newName, $folder->parent_id);

        if ($renameOnDisk) {
            $folderPath = MediaFolder::getFullPath($folder->id);

            if (Storage::exists($folderPath)) {
                $newFolderName = MediaFolder::createSlug($newName, $folder->parent_id);

                $newFolderPath = str_replace(
                    File::name($folderPath),
                    $newFolderName,
                    $folderPath
                );

                Storage::move($folderPath, $newFolderPath);

                $folder->slug = $newFolderName;

                $folderPath = "$folderPath/";

                MediaFile::query()
                    ->where('url', 'LIKE', "$folderPath%")
                    ->update([
                        'url' => DB::raw(
                            sprintf(
                                'CONCAT(%s, SUBSTRING(url, LOCATE(%s, url) + LENGTH(%s)))',
                                DB::escape("$newFolderPath/"),
                                DB::escape($folderPath),
                                DB::escape($folderPath)
                            )
                        ),
                    ]);
            }
        }

        $folder->save();

        MediaFolderRenamed::dispatch($folder);
    }

    public function refreshCache(): void
    {
        setting()->forceSet('media_random_hash', md5((string) time()))->save();
    }

    public function getFolderColors(): array
    {
        return $this->getConfig('folder_colors', []);
    }

    public function imageManager(?string $driver = null): ImageManager
    {
        if (! $driver) {
            $driver = GdDriver::class;

            if ($this->getImageProcessingLibrary() === 'imagick' && extension_loaded('imagick')) {
                $driver = ImagickDriver::class;
            }
        }

        return new ImageManager($driver);
    }

    public function canOnlyViewOwnMedia(): bool
    {
        return setting('user_can_only_view_own_media', false)
            && AdminHelper::isInAdmin(true)
            && ! App::runningInConsole()
            && auth()->check()
            && ! auth()->user()->isSuperUser();
    }

    public function responseDownloadFile(string $filePath)
    {
        $fileName = File::basename($filePath);
        $realPath = $this->getRealPath($filePath);

        if (! $this->isUsingCloud()) {
            if (! File::exists($realPath)) {
                return RvMedia::responseError(trans('core/media::media.file_not_exists'));
            }

            return response()->download($realPath, $fileName);
        }

        try {
            $content = Storage::get($filePath);
        } catch (Throwable $exception) {
            BaseHelper::logError($exception);

            $content = Http::withoutVerifying()->get($realPath)->body();
        }

        return response()->make($content, 200, [
            'Content-Type' => $this->getMimeType($filePath),
            'Content-Disposition' => 'attachment; filename="' . $fileName . '"; filename*=UTF-8\'\'' . rawurlencode($fileName),
            'Cache-Control' => 'no-cache, must-revalidate',
        ]);
    }

    public function getAvailableDrivers(): array
    {
        return apply_filters('core_media_drivers', [
            'public' => trans('core/setting::setting.media.local_disk'),
            's3' => 'Amazon S3',
            'r2' => 'Cloudflare R2',
            'do_spaces' => 'DigitalOcean Spaces',
            'wasabi' => 'Wasabi',
            'bunnycdn' => 'BunnyCDN',
            'backblaze' => 'Backblaze B2',
        ]);
    }
}
