<?php
/**
 * Module Storage Class
 *
 * @version     1.0.$Revision:$
 * @version     SVN: $Id:$
 * @package     bplan-base/globals
 * @subpackage  Support
 * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
 * @copyright   Copyright (C) 2025, 2026 bplan-solutions GmbH & Co. KG <https://www.bplan-solutions.de/>
 * /Δ\
 */

namespace BplanBase\Globals\Support;


use BplanBase\Globals\Helpers\StringHelper;
use BplanBase\Globals\Models\Tenant;
use BplanBase\Globals\Repositories\Core\TenantRepository;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Http\File;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;


/**
 * Module Storage Class
 *
 * Ersetzt die Notwendigkeit in einem Projekt eigene Storages zu definieren, indem innerhalb
 * der Standard-Storages "local" und "public" eine einheitliche Grundstruktur zur Speicherung
 * von Dateien geschaffen wird. Dabei werden Dateien in einem allgemeinen globalen Kontext
 * oder im Module-/Tenant-Kontext gespeichert.
 *
 * - storage/private/.global
 * - storage/private/.global/{Tenant}
 * - storage/private/{ModuleName}/.global
 * - storage/private/{ModuleName}/{Tenant}
 *
 * - storage/public/.global
 * - storage/public/.global/{Tenant}
 * - storage/public/{ModuleName}/.global
 * - storage/public/{ModuleName}/{Tenant}
 *
 * @version     4.2.0 / 2026-05-06
 * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
 */
class ModuleStorage
{


/* +++ TRAITS +++ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */


/* +++ CLASS CONSTANTS +++ ++++++++++++++++++++++++++++++++++++++++++++++++++ */


    /**
     * @var     string
     */
    const GLOBAL_DIR = '.global';


    /**
     * @var     string
     */
    const DEFAULT_TENANT_FOLDER_NAME_BASE = 'uuid';


/* +++ OBJECT MEMBERS +++ +++++++++++++++++++++++++++++++++++++++++++++++++++ */


    /**
     * @var     string
     */
    protected string $_basePath;


    /**
     * @var     string
     */
    protected string $_disk;


    /**
     * @var     string
     */
    protected string $_module;


    /**
     * @var     bool
     */
    protected string $_public;


    /**
     * @var     boolean|integer|string|null
     */
    protected bool|int|string|null $_tenant;


/* +++ CLASS MEMBERS +++ ++++++++++++++++++++++++++++++++++++++++++++++++++++ */


/* +++ OBJECT METHODS +++ +++++++++++++++++++++++++++++++++++++++++++++++++++ */


    /**
     *
     * @todo        $module-Parameter umdefinieren:
     *              FALSE   kein Modulkontext
     *              NULL    Module aus der Registry verwenden ("env.currentModule")
     *              string  Modulname
     *
     * @param       boolean $public
     *
     * @param       string $module
     *              Der Name des Moduls (~ Verzeichnisname).
     *
     * @param       boolean|integer|string|null $tenant
     *              Es kann die ID, die UUID oder der Identifier eines Tenants übergeben
     *              werden. Bei Übergabe von NULL (Default) wird der Tenant aus der Registry
     *              verwendet. Wenn FALSE übergeben wird, dann wird im globalen/tenant-freien
     *              Kontext gearbeitet.
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function __construct(bool $public, string $module, bool|int|string|null $tenant = null)
    {
        $this->_disk = $public ? 'public' : 'local';
        $this->_module = $module;
        $this->_public = $public;
        $this->_tenant = $tenant;

        $this->_basePath = $this->_basePath();

    } // __construct()


    /**
     * Pfad relativ zum Disk ermitteln
     *
     * @return      string
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _basePath(): string
    {
        $tenantFolder = $this->_resolveTenantFolder();

        return $this->_moduleBaseDir().'/'.$tenantFolder;

    } // _basePath()


    /**
     * Löscht ein lokales Verzeichnis außerhalb der Storage rekursiv vom Dateisystem
     *
     * @param       string $path
     *              Absoluter Pfad zum zu löschenden Verzeichnis.
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-04-11
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _deleteLocalDirectory(string $path): void
    {
        if (!is_dir($path)) {
            return;
        }
        $Iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($Iterator as $Item) {
            if ($Item->isDir() === true) {
                rmdir($Item->getRealPath());
            } else {
                unlink($Item->getRealPath());
            }
        }
        rmdir($path);

    } // _deleteLocalDirectory()


    /**
     * Gibt den Modul-Basis-Pfad (ohne Tenant-Verzeichnis) relativ zum Disk-Root zurück
     *
     * @return      string
     *
     * @version     1.0.0 / 2025-08-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _moduleBaseDir(): string
    {
        return $this->_module;

    } // _moduleBaseDir()


    /**
     *
     * @param       string $path
     *
     * @return      string
     *
     * @version     1.1.0 / 2025-08-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _normalize(string $path = ''): string
    {
        $path = trim($path);

        if ($path === '') {
            return '';
        }
        return ltrim(StringHelper::normalizePath($path), '/');

    } // _normalize()


    /**
     * Bereitet den Namen eines Tenant-Verzeichnisses in Abhängigkeit von Konfigurationseinstellungen auf
     *
     * @param       Tenant $Tenant
     *
     * @return      string
     *
     * @version     1.0.0 / 2026-03-15
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _prepareTenantFolderName(Tenant $Tenant): string
    {
        $baseProperty = config('globals.module-storage.tenant-folder-name-base', self::DEFAULT_TENANT_FOLDER_NAME_BASE);
        $baseValue = $Tenant->{$baseProperty};

        if (config('globals.module-storage.plain-tenant-folder-names', false) === true) {
            $prefix = config('globals.module-storage.tenant-folder-name-prefix', '');

            return $prefix.$baseValue;
        }
        return md5($baseValue);

    } // _prepareTenantFolderName()


    /**
     * Interner Helper zum Speichern einer Datei im Modul-Storage
     *
     * @param       string $destination
     *              Zielpfad relativ zum Modul-/Tenant-Root.
     *
     * @param       string|resource $contents
     *              Dateiinhalt oder Stream.
     *
     * @return      bool
     *
     * @version     1.1.0 / 2025-08-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _putWithDirectory(string $destination, $contents): bool
    {
        $toPath = $this->_normalize($destination);
        /*
        **  Zielverzeichnis anlegen, falls erforderlich. */
        $targetDir = dirname($toPath);

        if (!Storage::disk($this->_disk)->exists($targetDir)) {
            Storage::disk($this->_disk)->makeDirectory($targetDir);
        }
        return Storage::disk($this->_disk)->put($toPath, $contents);

    } // _putWithDirectory()


    /**
     * Tenant-Ordner ermitteln
     *
     * @return      string
     *              Liefert den Verzeichnisnamen des Tenant-Verzeichnisses oder ".global", wenn
     *              kein Tenant involviert ist.
     *              Der Name des Tenant-Verzeichnisses entspricht standardmäßig der MD5-Summe der
     *              UUID des Tenants. So lassen sich über den Verzeichnisnamen keine Rückschlüsse auf
     *              den Tenant ziehen. Durch die Änderung von ein paar Konfigurationseinstellungen
     *              können aber auch die ID oder der Identifier des Mandanten verwendet und das Hashen
     *              des Namens deaktiviert werden.
     *
     * @version     1.2.0 / 2026-03-15
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _resolveTenantFolder(): string
    {
        if ($this->_tenant === false) {
            return self::GLOBAL_DIR;
        }
        if ($this->_tenant === null) {
            $Tenant = Tenant::current();

            if ($Tenant === null) {
                throw new \RuntimeException('No tenant found in context. Specify tenant explicitly or FALSE for global.');
            }
            return $this->_prepareTenantFolderName($Tenant);
        }
        $Tenant = TenantRepository::getByVarious($this->_tenant, ignoreRestriction: true);

        return $this->_prepareTenantFolderName($Tenant);

    } // _resolveTenantFolder()


    /**
     * Listet alle Unterverzeichnisse rekursiv im aktuellen Modul-/Tenant-Ordner
     *
     * @param       string $subdir
     *
     * @return      array
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function allDirectories(string $subdir = ''): array
    {
        $path = $this->_basePath.'/'.$this->_normalize($subdir);

        return Storage::disk($this->_disk)->allDirectories($path);

    } // allDirectories()


    /**
     * Listet alle Dateien rekursiv im aktuellen Modul-/Tenant-Ordner
     *
     * @param       string $subdir
     *
     * @param       bool $fullPath
     *              TRUE um vollständige Dateipfade in der Standard-Storage zu erhalten
     *              FALSE für relative Pfade innerhalb der der ModuleStorage
     *
     * @return      array
     *
     * @version     2.0.0 / 2026-04-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function allFiles(string $subdir = '', bool $fullPath = false): array
    {
        $path = $this->_basePath.'/'.$this->_normalize($subdir);

        if ($fullPath === true) {
            $files = Storage::disk($this->_disk)->allFiles($path);

        } else {
            $tmp = Storage::disk($this->_disk)->allFiles($path);

            $files = [];

            foreach ($tmp as $file) {
                $files[] = str_replace($this->_basePath.'/', '', $file);
            }
        }
        return $files;

    } // allFiles()


    /**
     * Kopiert eine Datei innerhalb desselben Modul-Storages
     *
     * @param       string $from
     *              Relativer Pfad zur Quelldatei (innerhalb des Modul-/Tenant-Bereichs)
     *
     * @param       string $to
     *              Relativer Pfad zur Zieldatei (innerhalb des Modul-/Tenant-Bereichs)
     *
     * @return      bool
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function copy(string $from, string $to): bool
    {
        $contents = Storage::disk($this->_disk)->get($from);

        return $this->_putWithDirectory($to, $contents);

    } // copy()


    /**
     * Kopiert eine Datei aus einem anderen Storage ins aktuelle Modul-Storage
     *
     * @param       \Illuminate\Contracts\Filesystem\Filesystem $SourceDisk
     *              Quelle-Disk
     *
     * @param       string $sourcePath
     *              Pfad der Quelldatei im Quell-Storage
     *
     * @param       string $destination
     *              Relativer Pfad (innerhalb des Modul-/Tenant-Bereichs)
     *
     * @return      bool
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function copyFrom(Filesystem $SourceDisk, string $sourcePath, string $destination): bool
    {
        $contents = $SourceDisk->get($sourcePath);

        return $this->_putWithDirectory($destination, $contents);

    } // copyFrom()


    /**
     * Kopiert eine Datei vom aktuellen Modul-Storage in ein anderes Storage
     *
     * @param       string $source
     *              Relativer Pfad zur Quelldatei (innerhalb des Modul-/Tenant-Bereichs)
     *
     * @param       \Illuminate\Contracts\Filesystem\Filesystem $TargetDisk
     *              Das Ziel-Storage
     *
     * @param       string $targetPath
     *              Pfad der Zieldatei im Ziel-Storage
     *
     * @return      bool
     *
     * @version     1.0.0 / 2025-08-15
     */
    public function copyTo(string $source, Filesystem $TargetDisk, string $targetPath): bool
    {
        $sourcePath = $this->_basePath.'/'.$this->_normalize($source);

        if (!Storage::disk($this->_disk)->exists($sourcePath)) {
            return false;
        }
        $contents = Storage::disk($this->_disk)->get($sourcePath);
        /*
        **  Zielverzeichnis anlegen, falls erforderlich. */
        $targetDir = dirname($targetPath);

        if ($targetDir !== '.' && !$TargetDisk->exists($targetDir)) {
            $TargetDisk->makeDirectory($targetDir);
        }
        return $TargetDisk->put($targetPath, $contents);

    } // copyTo()


    /**
     * Löscht eine oder mehrere Dateien im aktuellen Modul-/Tenant-Verzeichnis
     *
     * @param       string|array $files
     *              Relativer Dateiname oder Array von Dateinamen.
     *
     * @return      bool
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function delete(string|array $files): bool
    {
        $files = (array) $files;

        $fullPaths = array_map(function ($file) {
            return $this->_basePath.'/'.$this->_normalize($file);

        }, $files);

        return Storage::disk($this->_disk)->delete($fullPaths);

    } // delete()


    /**
     * Löscht ein Verzeichnis im aktuellen Modul-/Tenant-Verzeichnis
     *
     * Wird $subdir nicht angegeben, wird das gesamte Tenant-Verzeichnis des Moduls gelöscht.
     *
     * @param       string $subdir
     *              Relativer Verzeichnispfad innerhalb des Tenant-Ordners.
     *
     * @return      bool
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function deleteDirectory(string $subdir = ''): bool
    {
        $path = $this->_basePath;

        if ($subdir !== '') {
            $path .= '/'.$this->_normalize($subdir);
        }
        return Storage::disk($this->_disk)->deleteDirectory($path);

    } // deleteDirectory()


    /**
     * Listet Unterverzeichnisse im aktuellen Modul-/Tenant-Ordner (nicht rekursiv).
     *
     * @param       string $subdir
     *
     * @return      array
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function directories(string $subdir = ''): array
    {
        $path = $this->_basePath.'/'.$this->_normalize($subdir);

        return Storage::disk($this->_disk)->directories($path);

    } // directories()


    /**
     * Datei als Download ausgeben.
     *
     * @param       string $file
     *
     * @param       string|null $name
     *
     * @param       array $headers
     *
     * @return      StreamedResponse
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function download(string $file, string|null $name = null, array $headers = []): StreamedResponse
    {
        $fullPath = $this->_basePath.'/'.$this->_normalize($file);

        if (!Storage::disk($this->_disk)->exists($fullPath)) {
            abort(404, 'File ['.$file.'] not found.');
        }
        return Storage::disk($this->_disk)->download($fullPath, $name ?? basename($file), $headers);

    } // download()


    /**
     * Überprüft die Existenz der übergebenen Datei
     *
     * @param       string $file
     *
     * @return      boolean
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function exists(string $file): bool
    {
        $fullPath = $this->_basePath.'/'.$this->_normalize($file);

        return Storage::disk($this->_disk)->exists($fullPath);

    } // exists()


    /**
     * Listet Dateien im aktuellen Modul-/Tenant-Ordner (nicht rekursiv)
     *
     * @param       string $subdir
     *
     * @return      array
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function files(string $subdir = ''): array
    {
        $path = $this->_basePath.'/'.$this->_normalize($subdir);

        return Storage::disk($this->_disk)->files($path);

    } // files()


    /**
     * Datei lesen.
     *
     * @param       string $file
     *
     * @return      string|null
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function get(string $file): string|null
    {
        $fullPath = $this->_basePath.'/'.$this->_normalize($file);

        return Storage::disk($this->_disk)->exists($fullPath)
            ? Storage::disk($this->_disk)->get($fullPath)
            : null;

    } // get()


    /**
     *
     * @param       string $subdir
     *
     * @return     string
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function getBasePath(string $subdir = ''): string
    {
        return $this->_basePath.'/'.$this->_normalize($subdir);

    } // getBasePath()


    /**
     *
     * @return      string
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function getDisk(): string
    {
        return $this->_disk;

    } // getDisk()


    /**
     * Liefert den absoluten Pfad vom Wurzelverzeichnis des Laufwerks zum Wurzelverzeichnis der aktuellen Storage
     *
     * @param       string $subPath
     *
     * @return      string
     *
     * @version     1.1.0 / 2025-08-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function getFullPath(string $subPath = ''): string
    {
        return $this->getRootPath($this->getSubPath($subPath));

    } // getFullPath()


    /**
     * Liefert den absoluten Pfad vom Wurzelverzeichnis des Laufwerks zum Wurzelverzeichnis des Moduls
     *
     * @todo        Private machen. Der Zugriff von außerhalb auf dieses Verzeichnis, macht keinen Sinn und ist nicht gestattet.
     *
     * @param       string $subPath
     *
     * @return      string
     *
     * @version     1.0.0 / 2025-08-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function getModulePath(string $subPath = ''): string
    {
        return $this->getRootPath(
            $this->_moduleBaseDir()
                .'/'.$this->_normalize($subPath)
        );
    } // getModulePath()


    /**
     * Liefert den absoluten Pfad vom Wurzelverzeichnis des Laufwerks zum Wurzelverzeichnis der Storage (inklusive private/public)
     *
     * @todo        Private machen. Der Zugriff von außerhalb auf dieses Verzeichnis, macht keinen Sinn und ist nicht gestattet.
     *
     * @param       string $subPath
     *
     * @return      string
     *
     * @version     1.0.0 / 2025-08-14
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function getRootPath(string $subPath = ''): string
    {
        /*
        **  Hier wird bewusst nicht mit _normalize() gearbeitet, weil führende
        **  Forwardslashes (/) nicht entfernt werden dürfen. */
        return StringHelper::normalizePath(
            Storage::disk($this->_disk)->path($subPath)
        );
    } // getRootPath()


    /**
     * Liefert den relativen Pfad vom Wurzelverzeichnis der Storage zum Wurzelverzeichnis des Tenants
     *
     * @param       string $subPath
     *
     * @return      string
     *
     * @version     1.0.0 / 2025-08-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function getSubPath(string $subPath = ''): string
    {
        return $this->_moduleBaseDir()
            .'/'.$this->_normalize(
                $this->_resolveTenantFolder().'/'.$this->_normalize($subPath)
            );
    } // getSubPath()


    /**
     *
     * @return      Filesystem
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function getStorage(): Filesystem
    {
        return Storage::disk($this->_disk);

    } // getStorage()


    /**
     * Verschiebt (oder benennt um) eine Datei innerhalb desselben Modul-Storages.
     *
     * @param       string $from
     *              Relativer Pfad zur Quelldatei (innerhalb des Modul-/Tenant-Bereichs)
     *
     * @param       string $to
     *              Relativer Pfad zur Zieldatei (innerhalb des Modul-/Tenant-Bereichs)
     *
     * @return      bool
     *
     * @version     1.0.0 / 2025-08-12
     */
    public function move(string $from, string $to): bool
    {
        $fromPath = $this->_basePath.'/'.$this->_normalize($from);
        $toPath = $this->_basePath.'/'.$this->_normalize($to);
        /*
        **  Zielverzeichnis anlegen, falls erforderlich. */
        $targetDir = dirname($toPath);

        if (!Storage::disk($this->_disk)->exists($targetDir)) {
            Storage::disk($this->_disk)->makeDirectory($targetDir);
        }
        /*
        **  Atomare Verschiebung mit rename durchführen. */
        $Storage = Storage::disk($this->_disk);

        $fromFullPath = $Storage->path($fromPath);
        $toFullPath = $Storage->path($toPath);

        return rename($fromFullPath, $toFullPath);

    } // move()


    /**
     * Verschiebt ein externes Verzeichnis in den aktuellen Modul-/Tenant-Bereich der Storage.
     *
     * Es wird zunächst versucht, das Verzeichnis atomar per rename() zu verschieben. Schlägt
     * das fehl (z. B. bei unterschiedlichen Dateisystemen), wird als Fallback der Inhalt
     * rekursiv kopiert und das Quellverzeichnis anschließend gelöscht.
     *
     * @param       string $sourcePath
     *              Absoluter Pfad zum Quellverzeichnis außerhalb der Storage.
     *
     * @param       string $destination
     *              Zielpfad relativ zum Modul-/Tenant-Root. Wird leer gelassen, wird das
     *              Verzeichnis direkt im Modul-/Tenant-Root abgelegt.
     *
     * @throws      \InvalidArgumentException
     *
     * @return      bool
     *
     * @version     1.0.0 / 2026-04-11
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function moveDirectory(string $sourcePath, string $destination = ''): bool
    {
        if (!is_dir($sourcePath)) {
            throw new \InvalidArgumentException('Source path ['.$sourcePath.'] is not a directory.');
        }
        $normalizedDest = $this->_normalize($destination);
        $relativeDestPath = $this->_basePath.($normalizedDest !== '' ? '/'.$normalizedDest : '');
        $absoluteDestPath = Storage::disk($this->_disk)->path($relativeDestPath);
        /*
        **  Elternverzeichnis des Ziels anlegen, falls erforderlich. */
        $parentDir = dirname($relativeDestPath);

        if (!Storage::disk($this->_disk)->exists($parentDir)) {
            Storage::disk($this->_disk)->makeDirectory($parentDir);
        }
        /*
        **  Atomare Verschiebung per rename() versuchen. */
        if (@rename($sourcePath, $absoluteDestPath)) {
            return true;
        }
        /*
        **  Fallback: rekursiv kopieren und Quelle löschen. */
        $success = $this->putDirectory($sourcePath, $destination, true);

        if ($success === true) {
            $this->_deleteLocalDirectory($sourcePath);
        }
        return $success;

    } // moveDirectory()


    /**
     * Verschiebt eine Datei aus einem anderen Storage ins aktuelle Modul-Storage.
     *
     * @param       \Illuminate\Contracts\Filesystem\Filesystem $sourceDisk
     *              Die Quell-Disk.
     *
     * @param       string $sourcePath
     *              Pfad der Quelldatei im Quell-Storage.
     *
     * @param       string $destination
     *              Relativer Pfad (innerhalb des Modul-/Tenant-Bereichs).
     *
     * @return      bool
     *
     * @version     1.0.0 / 2025-08-12
     */
    public function moveFrom(Filesystem $SourceDisk, string $sourcePath, string $destination): bool
    {
        $toPath = $this->_basePath.'/'.$this->_normalize($destination);
        /*
        **  Zielverzeichnis anlegen, falls erforderlich. */
        $targetDir = dirname($toPath);

        if (!Storage::disk($this->_disk)->exists($targetDir)) {
            Storage::disk($this->_disk)->makeDirectory($targetDir);
        }
        /*
        **  Prüfen ob beide Disks auf demselben Filesystem liegen. */
        $sourceFullPath = $SourceDisk->path($sourcePath);
        $targetFullPath = Storage::disk($this->_disk)->path($toPath);
        /*
        **  Atomare Verschiebung mit rename versuchen. */
        if (@rename($sourceFullPath, $targetFullPath)) {
            return true;
        }
        /*
        **  Falls rename fehlschlägt (verschiedene Filesysteme), Fallback zu copy+delete. */
        $success = $this->copyFrom($SourceDisk, $sourcePath, $destination);

        if ($success === true) {
            $SourceDisk->delete($sourcePath);
        }
        return $success;

    } // moveFrom()


    /**
     * Verschiebt eine Datei vom aktuellen Modul-Storage in ein anderes Storage.
     *
     * @param       string $source
     *              Relativer Pfad zur Quelldatei (innerhalb des Modul-/Tenant-Bereichs)
     *
     * @param       \Illuminate\Contracts\Filesystem\Filesystem $TargetDisk
     *              Das Ziel-Storage
     *
     * @param       string $targetPath
     *              Pfad der Zieldatei im Ziel-Storage
     *
     * @return      bool
     *
     * @version     1.0.0 / 2025-08-15
     */
    public function moveTo(string $source, Filesystem $TargetDisk, string $targetPath): bool
    {
        $sourcePath = $this->_basePath.'/'.$this->_normalize($source);

        if (!Storage::disk($this->_disk)->exists($sourcePath)) {
            return false;
        }
        /*
        **  Zielverzeichnis anlegen, falls erforderlich. */
        $targetDir = dirname($targetPath);

        if ($targetDir !== '.' && !$TargetDisk->exists($targetDir)) {
            $TargetDisk->makeDirectory($targetDir);
        }
        /*
        **  Prüfen ob beide Disks auf demselben Filesystem liegen. */
        $sourceFullPath = Storage::disk($this->_disk)->path($sourcePath);
        $targetFullPath = $TargetDisk->path($targetPath);
        /*
        **  Atomare Verschiebung mit rename versuchen. */
        if (@rename($sourceFullPath, $targetFullPath)) {
            return true;
        }
        /*
        **  Falls rename fehlschlägt (verschiedene Filesysteme), Fallback zu copy+delete. */
        $success = $this->copyTo($source, $TargetDisk, $targetPath);

        if ($success === true) {
            Storage::disk($this->_disk)->delete($sourcePath);
        }
        return $success;

    } // moveTo()


    /**
     * Liefert den absoluten Pfad vom Wurzelverzeichnis des Laufwerks zum Wurzelverzeichnis der aktuellen Storage
     *
     * Diese Methode ist ein Alias für die Methode getFullPath() und dient dazu, einen
     * Methodennamen verfügbar zu machen, den es auch bei der normalen Storage gibt.
     *
     * @param       string $subdir
     *
     * @return      string
     *
     * @version     1.0.1 / 2026-05-06
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function path(string $subdir = ''): string
    {
        return $this->getFullPath($subdir);

    } // path()


    /**
     * Datei speichern (Verzeichnis wird komplett angelegt).
     *
     * @param       string $file
     *
     * @param       string $contents
     *
     * @return      string
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function put(string $file, string $contents): string
    {
        $dirPath = dirname($this->_basePath.'/'.$this->_normalize($file));
        $fullPath = $this->_basePath.'/'.$file;

        Storage::disk($this->_disk)->makeDirectory($dirPath);
        Storage::disk($this->_disk)->put($fullPath, $contents);

        return $fullPath;

    } // put()


    /**
     * Überträgt den Inhalt eines externen Verzeichnisses in den aktuellen Modul-/Tenant-Bereich der Storage.
     *
     * @param       string $sourcePath
     *              Absoluter Pfad zum Quellverzeichnis außerhalb der Storage.
     *
     * @param       string $destination
     *              Zielpfad relativ zum Modul-/Tenant-Root. Wird leer gelassen, wird der
     *              Inhalt direkt im Modul-/Tenant-Root abgelegt.
     *
     * @param       bool $recursive
     *              TRUE (Standard) = Unterverzeichnisse werden rekursiv übernommen.
     *              FALSE = Nur Dateien im obersten Verzeichnis werden übertragen.
     *
     * @throws      \InvalidArgumentException
     *
     * @return      bool
     *
     * @version     1.0.0 / 2026-04-11
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function putDirectory(string $sourcePath, string $destination = '', bool $recursive = true): bool
    {
        if (!is_dir($sourcePath)) {
            throw new \InvalidArgumentException('Source path ['.$sourcePath.'] is not a directory.');
        }
        $normalizedDest = $this->_normalize($destination);
        $realSourcePath = realpath($sourcePath);

        $Iterator = $recursive
            ? new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($realSourcePath, \FilesystemIterator::SKIP_DOTS)
            )
            : new \IteratorIterator(
                new \DirectoryIterator($realSourcePath)
            );

        $success = true;

        foreach ($Iterator as $File) {
            if ($File->isDir() === true) {
                continue;
            }
            /*
            **  Relativen Pfad zur Quelle ermitteln und Trennzeichen normalisieren. */
            $relativePath = str_replace(
                DIRECTORY_SEPARATOR,
                '/',
                ltrim(substr($File->getRealPath(), strlen($realSourcePath)), DIRECTORY_SEPARATOR)
            );

            $destPath = ($normalizedDest !== '' ? $normalizedDest.'/' : '').$relativePath;
            $fullDestPath = $this->_basePath.'/'.$destPath;
            /*
            **  Zielverzeichnis anlegen, falls erforderlich. */
            $directory = dirname($fullDestPath);

            if (!Storage::disk($this->_disk)->exists($directory)) {
                Storage::disk($this->_disk)->makeDirectory($directory);
            }

            if (Storage::disk($this->_disk)->put($fullDestPath, file_get_contents($File->getRealPath())) === false) {
                $success = false;
            }
        }
        return $success;

    } // putDirectory()


    /**
     * Speichert eine Datei unter einem definierten oder generierten Namen im Modul-Storage.
     *
     * @param       UploadedFile|File|string $file
     *              Dateiobjekt oder Pfad zu einer existierenden Datei.
     *
     * @param       string $path
     *              Zielpfad relativ zum Modul-/Tenant-Root (ohne Dateinamen).
     *
     * @param       string|false|null $name
     *              FALSE = Originalname verwenden,
     *              NULL = eindeutigen Namen mit Zeitstempel generieren,
     *              string = expliziter Name (Endung wird ggf. korrigiert).
     *
     * @throws      InvalidArgumentException
     *
     * @return      string
     *              Relativer Speicherpfad zur gespeicherten Datei, innerhalb der aktuellen Module/Tenant-Storage.
     *
     * @version     1.0.2 / 2026-04-07
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function putFileAs(UploadedFile|File|string $file, string $path, string|false|null $name = null): string
    {
        /*
        **  Extension ermitteln. */
        if ($file instanceof UploadedFile) {
            $extension     = strtolower($file->getClientOriginalExtension());
            $originalName  = $file->getClientOriginalName();

        } elseif ($file instanceof File) {
            $extension     = strtolower($file->extension());
            $originalName  = $file->getFilename();

        } elseif (is_string($file)) {
            $extension     = strtolower(pathinfo($file, PATHINFO_EXTENSION));
            $originalName  = pathinfo($file, PATHINFO_BASENAME);

        } else {
            throw new \InvalidArgumentException('Invalid file type');
        }
        /*
        **  Ziel-Dateiname bestimmen. */
        if ($name === false) {
            /*
            **  Originalname verwenden. */
            $finalName = $originalName;

        } elseif ($name === null) {
            /*
            **  Eindeutiger Name mit Zeitstempel. */
            $finalName = now()->format('Ymd_His').'.'.uniqid().($extension ? '.'.$extension : '');

        } else {
            /*
            **  Entwickler gibt einen Namen vor – Endung ggf. korrigieren. */
            $givenExtension = strtolower(pathinfo($name, PATHINFO_EXTENSION));

            if (empty($givenExtension) || $givenExtension !== $extension) {
                $name .= $extension ? '.'.$extension : '';
            }
            $finalName = $name;
        }
        /*
        **  Vollständigen Zielpfad im Modul-Storage ermitteln. */
        $normalizedFilePath = $this->_normalize($path).'/'.$finalName;
        $fullPath = $this->_basePath.'/'.$normalizedFilePath;

        $directory = dirname($fullPath);

        if (!Storage::disk($this->_disk)->exists($directory)) {
            Storage::disk($this->_disk)->makeDirectory($directory);
        }
        /*
        **  Datei speichern. */
        if ($file instanceof UploadedFile || $file instanceof File) {
            Storage::disk($this->_disk)->putFileAs($directory, $file, $finalName);

        } elseif (is_string($file)) {
            Storage::disk($this->_disk)->put($fullPath, file_get_contents($file));
        }
        return $normalizedFilePath;

    } // putFileAs()


    /**
     * Gibt eine Datei als Download oder Inline-Response aus.
     *
     * @param       string $file
     *              Relativer Dateiname/Pfad innerhalb des Modul-/Tenant-Verzeichnisses
     *
     * @param       bool   $download
     *              TRUE = Download erzwingen, FALSE = Inline-Anzeige (z. B. Bild im Browser)
     *
     * @param       string|null $name
     *              Optionaler Dateiname für den Download
     *
     * @param       array  $headers
     *              Zusätzliche HTTP-Header
     *
     * @throws      \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
     *
     * @return      StreamedResponse
     *
     * @version     1.0.1 / 2025-08-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function serve(string $file, bool $download = true, ?string $name = null, array $headers = [])
    {
        $path = $this->_basePath.'/'.$this->_normalize($file);

        if (!Storage::disk($this->_disk)->exists($path)) {
            abort(404, 'File ['.$file.'] not found.');
        }
        if ($download === true) {
            /*
            **  Für Downloads. */
            return Storage::disk($this->_disk)->download($path, $name, $headers);
        }
        /*
        **  Für Inline-Anzige. */
        $mimeType = Storage::disk($this->_disk)->mimeType($path);

        if (!empty($mimeType)) {
            $headers['Content-Type'] = $mimeType;
        }
        return Storage::disk($this->_disk)->response($path, $name, $headers);

    } // serve()


    /**
     * Gibt eine Datei aus dem Tenant- oder dem .global-Verzeichnis zurück.
     *
     * Falls die gesuchte Datei im Tenant-Kontext nicht gefunden wird, wird aus ".global" geladen.
     *
     * @param       string $file
     *              Relativer Dateiname/Pfad innerhalb des Modul-Verzeichnisses.
     *
     * @param       bool $download
     *              TRUE = Download erzwingen, FALSE = Inline-Anzeige.
     *
     * @param       string|null $name
     *              Optionaler Dateiname für den Download.
     *
     * @param       array $headers
     *              Zusätzliche HTTP-Header.
     *
     * @return      StreamedResponse
     *
     * @throws      \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
     *
     * @version     1.1.0 / 2025-08-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function serveGlobalFallback(string $file, bool $download = true, ?string $name = null, array $headers = [])
    {
        $pathTenant = $this->_basePath.'/'.$this->_normalize($file);

        if (Storage::disk($this->_disk)->exists($pathTenant)) {
            return $this->serve($file, $download, $name, $headers);
        }
        /*
        **  Prüfen, ob die Datei als Global-Version existiert. */
        $pathGlobal = $this->_moduleBaseDir().'/'.self::GLOBAL_DIR.'/'.$file;

        if (Storage::disk($this->_disk)->exists($pathGlobal)) {
            /*
            **  Temporäres Storage-Objekt für das globale Verzeichnis. */
            $globalStorage = new self($this->_public, $this->_module, false);

            return $globalStorage->serve(self::GLOBAL_DIR.'/'.$file, $download, $name, $headers);
        }
        abort(404, 'File ['.$file.'] not found in either the tenant or global directory.');

    } // serveGlobalFallback()


    /**
     *
     * @return      Filesystem
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function storage(): Filesystem
    {
        return Storage::disk($this->_disk);

    } // storage()


    /**
     * Datei als Stream öffnen.
     *
     * @param       string $file
     *
     * @version     1.0.0 / 2025-08-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function stream(string $file)
    {
        $fullPath = $this->_basePath.'/'.$this->_normalize($file);

        if (!Storage::disk($this->_disk)->exists($fullPath)) {
            throw new \RuntimeException('File ['.$file.'] not found.');
        }
        return Storage::disk($this->_disk)->readStream($fullPath);

    } // stream()


    /**
     *
     * @param       string $file
     *
     * @return      string
     *
     * @version     1.0.0 / 2026-03-15
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function url(string $file): string
    {
        return Storage::disk($this->_disk)
            ->url(($this->_public === true ? 'storage' : '').$this->_basePath.'/'.$file);

    } // url()


/* +++ CLASS METHODS +++ ++++++++++++++++++++++++++++++++++++++++++++++++++++ */


} // class ModuleStorage {}
