<?php

namespace App\Libs\Backoffice\Managers;

use App\Models\Backoffice\Auth\BackendUser;
use App\Models\Backoffice\MediaFolder;
use App\Models\Backoffice\MediaItem;
use Exception;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Drivers\Imagick\Driver;
use Intervention\Image\ImageManager;
use Symfony\Component\HttpKernel\Exception\HttpException;

class MediaManager
{
    protected $baseFolderConfig;
    protected $disk;

    public function __construct($baseFolder = 'default')
    {
        if (!array_key_exists($baseFolder, config('media.base_folders', []))) {
            throw new Exception("Base folder '$baseFolder' not found in config/media.php");
        }

        $this->baseFolderConfig = config("media.base_folders.$baseFolder");
        if (!$this->baseFolderConfig) {
            throw new Exception("Base folder '$baseFolder' not found in config/media.php");
        }

        $this->disk = $this->baseFolderConfig['disk'];
        if (!config("filesystems.disks.{$this->disk}")) {
            throw new Exception("Disk '{$this->disk}' not found in config/filesystems.php");
        }
        $this->_ensureRootFolderExists();
    }

    private function _ensureRootFolderExists(): void
    {
        MediaFolder::firstOrCreate([
            'base_folder' => $this->disk,
            'path' => '',
            'name' => 'root',
            'parent_id' => null,
        ]);
    }

    public function getFiles($path = '')
    {
        return Storage::disk($this->disk)->files($path);
    }

    public function uploadFile(BackendUser $author, UploadedFile $file, ?string $relatedType = null, ?int $mediaFolderId = null): MediaItem
    {
        if ($mediaFolderId) {
            $folder = MediaFolder::where('base_folder', $this->disk)
                ->where('id', $mediaFolderId)
                ->firstOrFail();
        } else {
            $folder = MediaFolder::where('base_folder', $this->disk)
                ->where('path', '')
                ->firstOrFail();
        }

        $destinationPath = $folder->path;
        $originalFilename = $file->getClientOriginalName();
        $extension = strtolower($file->getClientOriginalExtension());
        $withoutExt = preg_replace('/\\.[^.\\s]{3,4}$/', '', $originalFilename);
        $size = $file->getSize();
        $width = null;
        $height = null;
        $hasAnonymizeFilenames = $this->baseFolderConfig['anonymize_filenames'] ?? false;

        $mediaType = $this->_getMediaType($extension);
        if (!$mediaType) {
            throw new HttpException(403, 'Invalid extension file');
        }

        if ($mediaType === MediaItem::MEDIA_TYPE_IMAGE) {
            $manager = new ImageManager(Driver::class);
            $img = $manager->read($file->getRealPath());
            $width = $img->width();
            $height = $img->height();
        }

        if ($hasAnonymizeFilenames) {
            $newFilename = substr(str_replace('-', '', Str::uuid()), 0, 12);
        } else {
            $newFilename = Str::slug($withoutExt);
        }
        $finalName = $newFilename . '.' . $extension;
        $i = 1;
        while (Storage::disk($this->disk)->exists($destinationPath ? $destinationPath . '/' . $finalName : $finalName)) {
            $finalName = $newFilename . '-' . $i . '.' . $extension;
            $i++;
            if ($i > 200) {
                throw new HttpException(400, 'The uploaded file already exists');
            }
        }

        $filePath = Storage::disk($this->disk)->putFileAs($destinationPath, $file, $finalName);
        if (!$filePath) {
            throw new HttpException(400, 'Error saving the uploaded file');
        }

        return MediaItem::create([
            'base_folder' => $this->disk,
            'path' => $filePath,
            'filename' => $finalName,
            'original_filename' => $originalFilename,
            'title' => $hasAnonymizeFilenames ? $originalFilename :  $finalName,
            'extension' => $extension,
            'related_type' => $relatedType,
            'media_type' => $mediaType,
            'backend_user_id' => $author->id,
            'size' => $size,
            'width' => $width,
            'height' => $height,
            'media_folder_id' => $folder->id,
        ]);
    }

    /**
     * gets the media type (video|document|image|svg)
     *
     * @param  string  $ext
     *
     * @return bool|string
     */
    private function _getMediaType($ext)
    {
        $allowedExts = config('backoffice.media.allowedExts');
        foreach ($allowedExts as $type => $allowedExt) {
            if (in_array(strtolower($ext), $allowedExt)) {
                return $type;
            }
        }
        return false;
    }

    /**
     * Creates a Media Folder
     * @param string $name
     * @param int $parentId
     * @throws \Exception
     * @return MediaFolder
     */
    public function createFolder(string $name, int $parentId = null): MediaFolder
    {
        if ($parentId) {
            $parent = MediaFolder::where('base_folder', $this->disk)
                ->where('id', $parentId)
                ->firstOrFail();
        } else {
            $parent = null;
        }

        $fullPath = $parent && $parent->path !== '' ? rtrim($parent->path, '/') . '/' . $name : $name;

        if (!Storage::disk($this->disk)->exists($fullPath) && !Storage::disk($this->disk)->makeDirectory($fullPath)) {
            throw new Exception("Failed to create folder '$fullPath'");
        }

        return MediaFolder::create([
            'base_folder' => $this->disk,
            'path' => $fullPath,
            'name' => $name,
            'parent_id' => $parentId,
        ]);
    }

    /**
     * Deletes a Media Folder
     * @param int $folderId
     * @throws \Exception
     * @return void
     */
    public function deleteFolder(int $folderId): void
    {
        $folder = MediaFolder::where('base_folder', $this->disk)
            ->where('id', $folderId)
            ->firstOrFail();

        // Prevent deletion of root folder
        if ($folder->path === '') {
            throw new Exception('Cannot delete the base folder');
        }

        $folderHasMediaItems = MediaItem::where('base_folder', $this->disk)
            ->where('media_folder_id', $folderId)
            ->exists();
        if ($folderHasMediaItems) {
            throw new Exception("Cannot delete folder '{$folder->path}' because it contains media items");
        }

        $folderHasSubFolders = MediaFolder::where('base_folder', $this->disk)
            ->where('parent_id', $folderId)
            ->exists();
        if ($folderHasSubFolders) {
            throw new Exception("Cannot delete folder '{$folder->path}' because it contains subfolders");
        }

        if (!Storage::disk($this->disk)->deleteDirectory($folder->path)) {
            throw new Exception("Failed to delete folder '{$folder->path}'");
        }

        $folder->delete();
    }

    /**
     * Renames a Media Folder
     * @param int $folderId
     * @param string $newName
     * @throws \Exception
     * @return MediaFolder
     */
    public function renameFolder(int $folderId, string $newName): MediaFolder
    {
        $folder = MediaFolder::where('base_folder', $this->disk)
            ->where('id', $folderId)
            ->firstOrFail();

        if ($folder->path === '') {
            throw new Exception('Cannot rename the base folder');
        }

        $oldPath = $folder->path;
        if (!is_null($folder->parent_id)) {
            $parentPath = MediaFolder::findOrFail($folder->parent_id)->path;
            $newPath = $parentPath !== '' ? rtrim($parentPath, '/') . '/' . $newName : $newName;
        } else {
            $parentPath = '';
            $newPath = $newName;
        }

        if (Storage::disk($this->disk)->exists($newPath)) {
            throw new Exception("Folder '$newPath' already exists");
        }

        if (!Storage::disk($this->disk)->move($oldPath, $newPath)) {
            throw new Exception("Failed to rename folder '$oldPath' to '$newPath'");
        }

        DB::beginTransaction();
        try {
            $folder->path = $newPath;
            $folder->name = $newName;
            $folder->save();

            /**
             * I'm using path for this because it's faster and avoids more complexity to
             * recursively get the descendants and update their paths
             */
            $descendants = MediaFolder::where('base_folder', $this->disk)
                ->where('path', 'like', $oldPath . '/%')
                ->get();

            foreach ($descendants as $descendant) {
                $descendant->path = preg_replace('/' . preg_quote($oldPath, '/') . '/', $newPath, $descendant->path, 1);
                $descendant->save();
            }

            $items = MediaItem::where('base_folder', $this->disk)
                ->where('path', 'like', $oldPath . '/%')
                ->get();

            foreach ($items as $item) {
                $item->path = preg_replace('/' . preg_quote($oldPath, '/') . '/', $newPath, $item->path, 1);
                $item->save();
            }

            DB::commit();
            return $folder;
        } catch (Exception $e) {
            DB::rollBack();
            throw new Exception("Failed to rename folder '$oldPath' to '$newPath': " . $e->getMessage());
        }
    }

    public function getFolder(int $folderId = null): MediaFolder
    {
        $this->_ensureRootFolderExists();

        $query = MediaFolder::where('base_folder', $this->disk)
            ->withCount('children');

        if ($folderId) {
            $query->where('id', $folderId);
        } else {
            $query->whereNull('parent_id');
        }

        return $query->firstOrFail();
    }

    public function getFolders(array $data): Paginator
    {
        $this->_ensureRootFolderExists();

        $query = MediaFolder::withCount('children')
            ->where('base_folder', $this->disk);

        if (!empty($data['searchString'])) {
            $query->where('name', 'LIKE', '%' . $data['searchString'] . '%')
                ->whereNotNull('parent_id');
        } elseif (!empty($data['folderId'])) {
            $query->where('parent_id', $data['folderId']);
        } else {
            $query->whereHas('parent', function ($q) {
                $q->whereNull('parent_id');
            });
        }

        return $query->orderBy('name', 'asc')->paginate(
            $data['perPage'] ?? 20,
            ['*'],
            'page',
            $data['page'] ?? 1
        );
    }

    /**
     * Move media items to a different folder.
     * Moves both physical files on disk and updates DB records in a transaction.
     *
     * @param array $mediaItemIds
     * @param int $targetFolderId
     * @return int Number of items moved
     * @throws Exception
     */
    public function moveItems(array $mediaItemIds, int $targetFolderId): int
    {
        $targetFolder = MediaFolder::where('base_folder', $this->disk)
            ->where('id', $targetFolderId)
            ->firstOrFail();

        $items = MediaItem::where('base_folder', $this->disk)
            ->whereIn('id', $mediaItemIds)
            ->get();

        if ($items->isEmpty()) {
            throw new Exception('No valid media items found to move');
        }

        $movedCount = 0;
        $destinationPath = ltrim($targetFolder->path, '/');

        // Ensure the target directory exists on disk
        if ($destinationPath && !Storage::disk($this->disk)->exists($destinationPath)) {
            if (!Storage::disk($this->disk)->makeDirectory($destinationPath)) {
                throw new Exception("Failed to create target directory '{$destinationPath}' on disk");
            }
        }

        DB::beginTransaction();
        try {
            foreach ($items as $item) {
                // Skip items already in the target folder
                if ($item->media_folder_id == $targetFolderId) {
                    continue;
                }

                $oldPath = $item->path;
                $filename = $item->filename;

                // Handle filename collisions in the target folder
                $finalName = $filename;
                $withoutExt = preg_replace('/\\.[^.\\s]{3,4}$/', '', $filename);
                $extension = $item->extension;
                $i = 1;
                $newPath = $destinationPath ? $destinationPath . '/' . $finalName : $finalName;

                while (Storage::disk($this->disk)->exists($newPath) && $newPath !== $oldPath) {
                    $finalName = $withoutExt . '-' . $i . '.' . $extension;
                    $newPath = $destinationPath ? $destinationPath . '/' . $finalName : $finalName;
                    $i++;
                    if ($i > 200) {
                        throw new HttpException(400, "Cannot move file '{$filename}': too many name collisions in target folder");
                    }
                }

                // Move physical file
                if (!Storage::disk($this->disk)->exists($oldPath)) {
                    throw new Exception("Source file '{$oldPath}' does not exist on disk");
                }

                if ($oldPath !== $newPath) {
                    if (!Storage::disk($this->disk)->move($oldPath, $newPath)) {
                        throw new Exception("Failed to move file from '{$oldPath}' to '{$newPath}'");
                    }
                }

                // Delete old thumbnails (they will regenerate on-demand)
                if ($item->isImage()) {
                    $this->_deleteItemThumbs($item);
                }

                // Update DB record
                $item->path = $newPath;
                $item->filename = $finalName;
                $item->media_folder_id = $targetFolderId;
                $item->save();

                $movedCount++;
            }

            DB::commit();
            return $movedCount;
        } catch (Exception $e) {
            DB::rollBack();
            throw new Exception('Failed to move media items: ' . $e->getMessage());
        }
    }

    /**
     * Delete all generated thumbnails for a media item
     *
     * @param MediaItem $item
     * @return void
     */
    private function _deleteItemThumbs(MediaItem $item): void
    {
        $allowedSizes = config('imageResize');
        if (!$allowedSizes) {
            return;
        }

        $thumbsPath = config('path.internal.thumbs');
        $filename = $item->filename;
        $withoutExt = preg_replace('/\\.[^.\\s]{3,4}$/', '', $filename);
        $ext = $item->extension;
        $path = pathinfo($item->path, PATHINFO_DIRNAME);

        foreach ($allowedSizes as $size => $sizeConfig) {
            $thumbFile = "{$thumbsPath}/{$item->base_folder}/{$path}/{$withoutExt}_{$size}.{$ext}";
            if (Storage::disk('public')->exists($thumbFile)) {
                Storage::disk('public')->delete($thumbFile);
            }
        }
    }
}
