<?php
/**
 * Core TenantResolver Class
 *
 * @version     1.0.$Revision:$
 * @version     SVN: $Id:$
 * @package     bplan-base/globals
 * @subpackage  MultiTenancy
 * @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\MultiTenancy;


use BplanBase\Globals\Enums\UserAccessLevel;
use BplanBase\Globals\Models\Tenant;
use BplanBase\Globals\Models\User;
use BplanBase\Globals\Repositories\Core\TenantLocaleRepository;
use BplanBase\Globals\Repositories\Core\TenantRepository;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use LogicException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;


/**
 * Core TenantResolver Class
 *
 * @version     1.5.0 / 2026-06-08
 * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
 */
class CoreWebTenantInitializer
{


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


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


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


    /**
     * @var     bool
     */
    private bool $_authCheck;


    /**
     * @var     Collection
     */
    private Collection $_AvailableTenants;


    /**
     * @var     string
     */
    private string|null $_configTenant;


    /**
     * @var     Request
     */
    private Request $_Request;


    /**
     * @var     Store
     */
    private $_Session;


    /**
     * @var     bool
     */
    private bool $_singleTenantMode;


    /**
     * @var     User|null
     */
    private ?User $_User = null;


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


    /**
     * @var     string
     */
    private static string $_tenantQueryParameter = \BplanBase\Globals\PackageServiceProvider::TENANT_QUERY_PARAMETER;


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


    /**
     *
     * @param       Request $Request
     *
     * @param       User $User
     *
     * @version     1.0.0 / 2026-02-17
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function __construct(Request $Request, User $User)
    {
        $this->_init($Request, $User);

    } // __construct()


    /**
     * Ermittelt den Mandanten für den initialen Request
     *
     * @throws      InvalidArgumentException
     *
     * @throws      ModelNotFoundException
     *
     * @throws      AccessDeniedHttpException
     *
     * @return      Tenant
     *
     * @version     1.0.0 / 2026-02-17
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _determineInitialRequestTenant(): Tenant
    {
        $this->_initAvailableTenants();
        /*
        **  Standardverfahren beim initialen Aufruf ohne übergebenen Mandanten.
        **
        **  Im Single-Tenant-Mode wird der konfigurierte Tenant verwendet, ansonsten
        **  der Mandant dem der User direkt zugeordnet ist. */
        /*
            @todo   Gilt nur für angemeldete User:
                    Wenn zukünftig der zuletzt verwendete Mandant beim User gespeichert wird,
                    dann könnte er hier initialisiert werden.
        */
        if ($this->_singleTenantMode === true) {
            $Tenant = TenantRepository::getByVarious($this->_configTenant, ignoreRestriction: true);

            if ($Tenant === null) {
                $this->_reset();

                throw new InvalidArgumentException('Configured tenant ['.$this->_configTenant.'] not found.');
            }
        } else {
            $Tenant = TenantRepository::getById($this->_User->tenant_id, ignoreRestriction: true);

            if ($Tenant === null) {
                $this->_reset();

                throw new ModelNotFoundException('Tenant ['.$this->_User->tenant_id.'] not found.');
            }
        }
        if ($this->_authCheck === true) {
            /*
            **  Angemeldete User verarbeiten.
            **
            **  Bei angemeldeten Benutzern muss sichergestellt werden, dass die übergebene
            **  Tenant-ID auch in der Liste der Tenants des Users enthalten ist. */
            if ($this->_AvailableTenants->has($Tenant->id) === false) {
                $this->_reset();

                throw new AccessDeniedHttpException(__('error.auto-logout.invalid-tenant'));
            }
            /*
            **  Sollte der ermittelte Mandant inaktiv sein, dann darf hier, beim ersten Request,
            **  ein alternativer Mandant ermittelt werden. */
            if ($Tenant->active === false) {
                $ActiveTenants = $this->_AvailableTenants->where('active', '=', 1);

                if ($ActiveTenants->isEmpty()) {
                    $this->_reset();
                    // @todo    Dieser Fall muss zu einem Logout führen
                    throw new AccessDeniedHttpException(__('error.auto-logout.no-active-tenant'));
                }
            }
        } else {
            /*
            **  Guest-User verarbeiten. */
            if ($Tenant->active === false) {
                $this->_reset();
                /*
                **  Dieser Fall sollte bei einem regulären Request, bei dem entweder der System-
                **  Mandant oder der globale Mandant eingesetzt wird, nicht auftreten (sofern beide
                **  korrekt konfiguriert sind). */
                throw new AccessDeniedHttpException(__('error.auto-logout.no-active-tenant'));
            }
        }
        return $Tenant;

    } // _determineInitialRequestTenant()


    /**
     * Liefert einen Wert aus dem Cache
     *
     * @return      mixed
     *
     * @version     1.0.0 / 2026-02-17
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _getCacheValue($key): mixed
    {
        if ($this->_Session->has($key) === false) {
            return null;
        }
        return $this->_Session->get($key);

    } // _getAuthenticatedUserTenant()


    /**
     * Prüft die Gültigkeit des übergebenen Mandanten
     *
     * @param       Tenant $Tenant
     *
     * @throws      AccessDeniedHttpException
     *
     * @return      Tenant Gibt im Erfolgsfall das übergebene Tenant-Objekt zurück.
     *
     * @version     1.0.0 / 2026-02-17
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _handleTenantChange(Tenant $Tenant): Tenant
    {
        $this->_initAvailableTenants();
        /*
        **  Beim aktiven Wechsel des Mandanten muss sichergestellt werden, dass der
        **  übergebene Mandant auch in der Liste der Tenants des Users enthalten ist. */
        if ($this->_AvailableTenants->has($Tenant->id) === false) {
            $this->_reset();

            throw new AccessDeniedHttpException(__('error.auto-logout.invalid-tenant'));
        }
        if ($Tenant->active === false) {
            $this->_reset();

            throw new AccessDeniedHttpException(__('error.auto-logout.no-active-tenant'));
        }
        return $Tenant;

    } // _handleTenantChange()


    /**
     * Überprüft das Vorhandensein von Cache-Daten
     *
     * @param       null|string $key
     *
     * @return      boolean
     *
     * @version     1.0.0 / 2026-02-17
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _hasCacheData(null|string $key = null): bool
    {
        if ($key === null) {
            $key = 'core.tenantId';
        }
        return $this->_Session->has($key);

    } // _hasCacheData()


    /**
     * Initialisiert Objektvariablen
     *
     * @param       Request $Request
     *
     * @param       User $User
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-02-17
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _init(Request $Request, User $User): void
    {
        $this->_authCheck = auth()->check();
        $this->_configTenant = config('globals.global-tenant');
        $this->_Request = $Request;
        $this->_Session = session();
        $this->_singleTenantMode = !empty($this->_configTenant);
        $this->_User = $User;

    } // _init()


    /**
     * Ermittelt die Liste der verfügbaren Mandanten zum User
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-02-17
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _initAvailableTenants(): void
    {
        $tenants = TenantRepository::determineUserTenants($this->_User, ignoreRestriction: true);

        if (empty($tenants)) {
            $this->_reset();
            /*
            **  Dieser Fall dürfte nie eintreten.
            **  Ein User hat immer den Mandanten dem er direkt zugeordnet ist (auch wenn er selbst
            **  als "restricted" gekennzeichnet ist). Wenn er "restricted" ist und Zuordnungen zu
            **  anderen Mandanten existieren, dann besteht die Möglichkeit, dass er den Zugriff auf
            **  seinen direkt zugeordneten Mandanten verloren hat, dafür hat er dann aber Zugriff
            **  auf den/die extern zugeordneten Mandanten. */
            throw new LogicException(__('error.auto-logout.no-tenant'));
        }
        $this->_AvailableTenants = collect($tenants);

    } // _initAvailableTenants()


    /**
     * Legt Daten zur späteren Verwendung in der Session ab
     *
     * @param       Tenant $Tenant
     *
     * @return      void
     *
     * @version     1.2.0 / 2026-05-03
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _initCacheData(Tenant $Tenant): void
    {
        $tenantLocales = TenantLocaleRepository::determineTenantLocales($Tenant->id);

        $currentAccessLevel = $this->_User->access_level ?? UserAccessLevel::Guest->name;
        /*
            @todo   Hier muss noch der AccessLevel des Users zum aktuellen Tenant-Kontext ermittelt werden.
                    Dieser soll dann den im Objekt gespeicherten Wert überschreiben, damit im Code einfach
                    folgendes gemacht werden kann:
                        \Registry::get('core.user')->hasMinAccessLevel(UserAccessLevel::Maintainer)

        $this->_User->access_level = $currentAccessLevel;

        */
        $this->_Session->put('core.authCheck',       $this->_authCheck);
        $this->_Session->put('core.masterTenantId',  null);
        $this->_Session->put('core.tenantId',        (int) $Tenant->id);
        $this->_Session->put('core.tenantIds',       $this->_AvailableTenants->keys()->toArray());
        $this->_Session->put('core.tenantLocales',   $tenantLocales);
        $this->_Session->put('core.tenants',         $this->_AvailableTenants);
        //  Es macht keinen Sinn den User in der Session zu halten, weil der bei jedem Request neu initialisiert wird.
        // $this->_Session->put('core.user',            $this->_User);
        $this->_Session->put('core.userAccessLevel', $currentAccessLevel);
        $this->_Session->put('core.userId',          $this->_User->id);

    } // _initCacheData()


    /**
     * Überträgt Session-Daten in die Registry
     *
     * In dieser Methode wird auch der aktuelle Mandant via Spatie verfügbar gemacht.
     *
     * @return      void
     *
     * @version     1.3.0 / 2026-06-08
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _initRegistry(): void
    {
        /*
        **  Release any previously set readonly values before (re-)initialization.
        **  This makes the method idempotent, which is necessary because Livewire
        **  re-runs persistent middleware (InitWebEnv) within the same request cycle. */
        \Registry::forget('core', true);

        $Tenant = Tenant::findOrFail($this->_Session->get('core.tenantId'));

        $Tenant->makeCurrent();

        setPermissionsTeamId($Tenant->id);

        $this->_User->unsetRelation('roles')->unsetRelation('permissions');

        \Registry::put('core.authCheck',       $this->_Session->get('core.authCheck'), true);
        \Registry::put('core.masterTenantId',  $this->_Session->get('core.masterTenantId'), true);
        \Registry::put('core.tenant',          $Tenant, true);
        \Registry::put('core.tenantId',        $this->_Session->get('core.tenantId'), true);
        \Registry::put('core.tenantIds',       $this->_Session->get('core.tenantIds'), true);
        \Registry::put('core.tenantLocales',   $this->_Session->get('core.tenantLocales', [1 => config('app.fallback_locale')]), true);
        \Registry::put('core.tenants',         $this->_Session->get('core.tenants'), true);
        // \Registry::put('core.user',            $this->_Session->get('core.user'), true);
        /*
        **  Der User kommt nicht aus dem Cache, weil er mit jedem Request neu initialisiert wird. */
        \Registry::put('core.user',            $this->_User);
        \Registry::put('core.userAccessLevel', $this->_Session->get('core.userAccessLevel'), true);
        \Registry::put('core.userId',          $this->_Session->get('core.userId'), true);

    } // _initRegistry()


    /**
     * Setzt die Registry- und die Session-Daten zurück
     *
     * @return 	    void
     *
     * @version     1.0.1 / 2026-04-14
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _reset(): void
    {
        \Registry::forget('core');

        $this->_Session->forget('core');

    } // _reset()


    /**
     * Verarbeitet den Request und initialisiert den Mandanten
     *
     * @return      void
     *
     * @version     1.1.0 / 2026-03-10
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function initialize(): void
    {
        /*
        **  Reihenfolge der Abarbeitung
        **
        **  1. Prüfung, ob ein Mandant im Query-Parameter übergeben wurde.
        **     Der aktive Mandantenwechsel muss als erster Fall behandelt werden.
        **
        **  2. Prüfung der Session auf einen gespeicherten Mandanten.
        **     Das ist der Normalfall, wenn bereits einmal eine Initialisierung stattgefunden hat.
        **
        **  3. Standardfall beim ersten Aufruf.
        **     Prüfung auf Einzel-Mandanten-Modus. In einem Einzel-Mandanten-System wird der aktuelle
        **     Mandant auf Basis des in der Konfiguration hinterlegten Mandaten initialisiert.
        **     Andernfalls wird der dem User direkt zugeordnete Mandant verwendet.
        */
        $queryTenant = $this->_Request->query(self::$_tenantQueryParameter);

        if ($queryTenant !== null) {
            /*
            **  Aufruf mit einem Mandanten im Query-Parameter. */
            $Tenant = TenantRepository::getByVarious($queryTenant, ignoreRestriction: true);

            if ($Tenant === null) {
                $this->_reset();

                throw new ModelNotFoundException(__('error.auto-logout.invalid-tenant'));
            }
            /*
            **  Prüfen ob der Tenant zur übergebene Tenant-ID bereits in der Session gespeichert ist.
            **
            **  Durch diese vorgeschaltete Prüfung kann die erneute Initialisierung der Cache-Daten
            **  entfallen. */
            if ($this->_getCacheValue('core.authCheck') === $this->_authCheck && (int) $this->_getCacheValue('core.tenantId') === (int) $Tenant->id) {
                $this->_initRegistry();

            } else {
                /*
                **  Prüfung des übergebenen Mandanten mit anschließender Reinitialisierung
                **  der Cache-Daten. */
                $Tenant = $this->_handleTenantChange($Tenant);

                $this->_initCacheData($Tenant);
                $this->_initRegistry();
            }
        } elseif ($this->_hasCacheData() && $this->_getCacheValue('core.authCheck') === $this->_authCheck) {
            /*
            **  Arbeitsweise für alle Requests nach dem initialen Aufruf.
            **
            **  Wenn es Cache-Daten gibt, die zum aktuellen Auth-Zustand passen, dann wird
            **  die Registry einfach mit den Session-Daten initialisiert. */
            $this->_initRegistry();

        } else {
            /*
            **  Initialer Request.
            **  Wenn es noch keine Cache-Daten gibt, dann wird ein Mandant zum User ermittelt. */
            $Tenant = $this->_determineInitialRequestTenant();

            $this->_initCacheData($Tenant);
            $this->_initRegistry();
        }
    } // initialize()


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


} // class CoreWebTenantInitializer {}
