<?php

namespace App\Domains\Attachable\Services;

use App\Domains\Attachable\Enums\AttachmentType;
use App\Domains\Attachable\Exceptions\AttachmentStorageException;
use App\Domains\Attachable\Models\Attachment;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\Mime\MimeTypes;
use Throwable;

class AttachmentService
{
    public function store(
        UploadedFile|string $file,
        string $path,
    ): Attachment {
        $normalizedPath = $this->normalizeDirectory($path);

        if ($normalizedPath === '') {
            throw new AttachmentStorageException('The attachment path cannot be empty.');
        }

        $key = (string) Str::uuid();
        $fileData = $this->resolveFileData($file, $key);
        $storedPath = $this->storeFile(
            $fileData,
            $normalizedPath,
            $this->buildStoredFilename($key, $fileData['extension']),
        );

        if (! is_string($storedPath) || $storedPath === '') {
            throw new AttachmentStorageException('No se pudo subir el archivo al disco s3.');
        }

        try {
            /** @var Attachment $attachment */
            $attachment = Attachment::query()->create([
                'key' => $key,
                'path' => $storedPath,
                'filename' => $this->resolveFilename($file, $key),
                'type' => $this->resolveAttachmentType($fileData['mime_type']),
                'mime_type' => $fileData['mime_type'],
                'extension' => $fileData['extension'],
                'size' => $fileData['size'],
            ]);

            return $attachment;
        } catch (Throwable $throwable) {
            Storage::disk('s3')->delete($storedPath);

            throw $throwable;
        }
    }

    public function delete(Attachment $attachment): void
    {
        $deleted = Storage::disk('s3')->delete($attachment->path);

        if (! $deleted) {
            throw new AttachmentStorageException('No se pudo eliminar el archivo del disco s3.');
        }

        $attachment->delete();
    }

    protected function normalizeDirectory(string $path): string
    {
        return trim($path, '/');
    }

    protected function resolveFilename(UploadedFile|string $file, string $key): string
    {
        if ($file instanceof UploadedFile) {
            $originalName = trim($file->getClientOriginalName());

            if ($originalName !== '') {
                return $originalName;
            }
        }

        return $key;
    }

    /**
     * @return array{contents: string|null, extension: string, file: UploadedFile|null, mime_type: string, size: int}
     */
    protected function resolveFileData(UploadedFile|string $file, string $filename): array
    {
        if ($file instanceof UploadedFile) {
            return $this->fileDataFromUploadedFile($file);
        }

        return $this->fileDataFromBase64($file, $filename);
    }

    /**
     * @return array{contents: string|null, extension: string, file: UploadedFile, mime_type: string, size: int}
     */
    protected function fileDataFromUploadedFile(UploadedFile $file): array
    {
        $mimeType = strtolower($file->getClientMimeType() ?? $file->getMimeType() ?? 'application/octet-stream');

        return [
            'contents' => null,
            'extension' => strtolower($file->getClientOriginalExtension() ?: $file->extension() ?: ''),
            'file' => $file,
            'mime_type' => $mimeType,
            'size' => $file->getSize() ?? 0,
        ];
    }

    /**
     * @return array{contents: string, extension: string, file: null, mime_type: string, size: int}
     */
    protected function fileDataFromBase64(string $file, string $filename): array
    {
        ['data' => $contents, 'mime_type' => $declaredMimeType] = $this->decodeBase64File($file);
        $mimeType = $this->detectMimeType($contents, $declaredMimeType);

        return [
            'contents' => $contents,
            'extension' => $this->resolveExtension($filename, $mimeType),
            'file' => null,
            'mime_type' => $mimeType,
            'size' => strlen($contents),
        ];
    }

    /**
     * @return array{data: string, mime_type: string|null}
     */
    protected function decodeBase64File(string $file): array
    {
        $payload = trim($file);

        if ($payload === '') {
            throw new AttachmentStorageException('The base64 attachment content cannot be empty.');
        }

        $declaredMimeType = null;

        if (preg_match('/^data:(?<mime>[-\w.+\/]+);base64,(?<data>.+)$/s', $payload, $matches) === 1) {
            $declaredMimeType = strtolower($matches['mime']);
            $payload = $matches['data'];
        }

        $decoded = base64_decode(preg_replace('/\s+/', '', $payload), true);

        if ($decoded === false || $decoded === '') {
            throw new AttachmentStorageException('The attachment base64 payload is invalid.');
        }

        return [
            'data' => $decoded,
            'mime_type' => $declaredMimeType,
        ];
    }

    protected function detectMimeType(string $contents, ?string $fallback = null): string
    {
        if (is_string($fallback) && $fallback !== '') {
            return strtolower($fallback);
        }

        $detectedMimeType = (new \finfo(FILEINFO_MIME_TYPE))->buffer($contents);

        if (is_string($detectedMimeType) && $detectedMimeType !== '') {
            return strtolower($detectedMimeType);
        }

        return strtolower($fallback ?? 'application/octet-stream');
    }

    protected function resolveExtension(string $filename, string $mimeType): string
    {
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

        if ($extension !== '') {
            return $extension;
        }

        return strtolower(MimeTypes::getDefault()->getExtensions($mimeType)[0] ?? '');
    }

    /**
     * @param  array{contents: string|null, extension: string, file: UploadedFile|null, mime_type: string, size: int}  $fileData
     */
    protected function storeFile(array $fileData, string $directory, string $storedFilename): string
    {
        if ($fileData['file'] instanceof UploadedFile) {
            return Storage::disk('s3')->putFileAs($directory, $fileData['file'], $storedFilename);
        }

        $storedPath = $directory !== ''
            ? $directory.'/'.$storedFilename
            : $storedFilename;

        $stored = Storage::disk('s3')->put($storedPath, $fileData['contents'] ?? '');

        if (! $stored) {
            return '';
        }

        return $storedPath;
    }

    protected function resolveAttachmentType(string $mimeType): AttachmentType
    {
        $mimeType = strtolower($mimeType);

        if (str_starts_with($mimeType, 'image/')) {
            return AttachmentType::Image;
        }

        if (str_starts_with($mimeType, 'video/')) {
            return AttachmentType::Video;
        }

        if ($mimeType === 'application/pdf') {
            return AttachmentType::Pdf;
        }

        if (str_starts_with($mimeType, 'audio/')) {
            return AttachmentType::Audio;
        }

        if (
            str_starts_with($mimeType, 'text/')
            || str_contains($mimeType, 'document')
            || str_contains($mimeType, 'word')
            || str_contains($mimeType, 'excel')
            || str_contains($mimeType, 'spreadsheet')
            || str_contains($mimeType, 'presentation')
            || str_contains($mimeType, 'officedocument')
        ) {
            return AttachmentType::Document;
        }

        return AttachmentType::Other;
    }

    protected function buildStoredFilename(string $key, string $extension): string
    {
        if ($extension === '') {
            return $key;
        }

        return $key.'.'.$extension;
    }
}
