<?php
/**
 * Repository Class
 *
 * @version     1.0.$Revision:$
 * @version     SVN: $Id:$
 * @package     bplan-base/globals
 * @subpackage  Repositories
 * @author      Emilio Cannarozzo <emilio.cannarozzo@bplan-solutions.de>
 * @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\Repositories\Core;


use BplanBase\Globals\Models\Tenant;
use BplanBase\Globals\Models\User;
use BplanBase\Globals\Repositories\TenantRepository as BaseRepository;
use BplanBase\Globals\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;


/**
 * Repository Class
 *
 * @version     6.0.0 / 2026-05-26
 * @author      Emilio Cannarozzo <emilio.cannarozzo@bplan-solutions.de>
 * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
 */
class TenantRepository extends BaseRepository
{

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


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


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


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


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


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


    /**
     * Determines the IDs of all superior tenants
     *
     * @todo        Collection zurückgeben. Wo erforderlich kann toArray() angewendet werden.
     *
     * @param       int|string $tenantId
     *
     * @param       bool $ignoreCache
     *              Controls the use of the method cache when determining the result.
     *              If TRUE is passed, a database access is carried out even if the method cache
     *              already contains a suitable entry. The storage of a value in the method cache
     *              is not affected by this parameter.
     *
     * @version     2.0.0 / 2025-05-20
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public static function determineParentTenantIds(int|string $tenantId, bool $ignoreCache = false): array
    {
        static $cache = [];

        $cacheKey = md5(serialize(func_get_args()));

        if ($ignoreCache === false && isset($cache[$cacheKey])) {
            return $cache[$cacheKey];
        }
        $parentTenants = [];

        $TenantCollection = self::getAll(withInactive: true);

        $tenantMasters = $TenantCollection->pluck('master_id', 'id')->toArray();

        $masterId = $tenantMasters[$tenantId];

        if ($masterId === null) {
            return $cache[$cacheKey] = [];
        }
        while ($masterId !== null) {
            $parentTenants[] = $masterId;

            $masterId = $tenantMasters[$masterId];
        }
        $parentTenants = array_reverse($parentTenants);

        return $cache[$cacheKey] = $parentTenants;

    } // determineParentTenantIds()


    /**
     * Determines all sub tenants
     *
     * The transferred tenant is also included in the result by default.
     *
     * @todo        Collection zurückgeben. Wo erforderlich kann toArray() angewendet werden.
     *
     * @param       int|string $tenantId
     *
     * @param       bool $ignoreCache
     *              Controls the use of the method cache when determining the result.
     *              If TRUE is passed, a database access is carried out even if the method cache
     *              already contains a suitable entry. The storage of a value in the method cache
     *              is not affected by this parameter.
     *
     * @param       bool $withCurrent
     *
     * @return      array
     *
     * @version     3.0.0 / 2025-05-20
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public static function determineSubTenants(int|string $tenantId, bool $ignoreCache = false, bool $withCurrent = true): array
    {
        static $cache = [];

        $cacheKey = md5(serialize(func_get_args()));

        if ($ignoreCache === false && isset($cache[$cacheKey])) {
            return $cache[$cacheKey];
        }
        /*
        **  Alle Tenants ermitteln und abarbeiten. */
        $Tenants = static::$_modelClass::orderBy('master_id')
            ->get();

        $tenants = [];

        foreach ($Tenants as $Tenant) {
            $id = $Tenant->id;
            $masterId = $Tenant->masterId;
            /*
            **  Wenn es sich beim aktuellen Tenant um den Tenant mit der übergebenen ID oder um
            **  einen der bereits identifizierten Tenants handelt, dann wird er zur Liste hinzugefügt. */
            if ($id == $tenantId || isset($tenants[$masterId])) {
                $tenants[$id] = $Tenant;
            }
        }
        if ($withCurrent === false) {
            unset($tenants[$tenantId]);
        }
        return $cache[$cacheKey] = $tenants;

    } // determineSubTenants()


    /**
     * Determines all sub tenants using the materialized path column
     *
     * More efficient than determineSubTenants() as it uses a single indexed
     * LIKE query on the path column instead of iterating all tenants in PHP.
     *
     * @param       int|string $tenantId
     *
     * @param       bool $withCurrent
     *
     * @return      array
     *
     * @version     1.0.0 / 2026-05-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public static function determineSubTenantsByPath(int|string $tenantId, bool $withCurrent = true): array
    {
        $CurrentTenant = static::$_modelClass::withoutGlobalScopes()->find($tenantId);

        $path = $CurrentTenant->path;

        $Query = static::$_modelClass::withoutGlobalScopes()
            ->where('path', 'like', $path.'%')
            ->orderBy('path');

        if ($withCurrent === false) {
            $Query->where('path', '!=', $path);
        }
        return $Query->get()->keyBy('id')->all();

    } // determineSubTenantsByPath()


    /**
     * Returns an array with all tenants to which the given user may have access
     *
     * A numerically indexed array with the tenant IDs as keys is returned.
     *
     * The method implements the following logic:
     *
     * - By default, a user has access to the client to which he is directly assigned and all
     *   existing sub-clients (even across several levels).
     * - The "restricted" flag can be used to prevent access to the sub-clients. If the flag is set
     *   to TRUE, the user's access is restricted to the (master) client to which he is assigned.
     * - If the flag is TRUE and links are also found in the "TenantUserAccess" table, then the user
     *   loses access to the master client and their access options are limited to the clients linked
     *   there. To gain access to the master client, the master client must also be linked.
     *
     * The tenant data is supplemented by the access level that the user has for the respective tenant.
     *
     * @param       int|string|User $user
     *              Optionally the ID of a user or a user object.
     *
     * @param       bool $ignoreRestriction
     *              By default, the result is limited to the tenants of the current user. The
     *              restriction can be removed by setting this parameter to TRUE.
     *
     * @return      array An array with all tenants to which the user has access.
     *              The keys of the elements correspond to the IDs of the tenants.
     *
     * @version     3.0.0 / 2025-07-16
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public static function determineUserTenants(int|string|Model $user, bool $ignoreRestriction = false): array
    {
        static $cache = [];
        /*
        **  User ermitteln. */
        if ($user instanceof User) {
            $User = $user;

            $userId = $User->id;

        } else {
            $User = UserRepository::getById($user, ignoreRestriction: $ignoreRestriction);

            $userId = $User->id;
        }
        $cacheKey = md5(serialize([$userId, $ignoreRestriction]));

        if (isset($cache[$cacheKey])) {
            return $cache[$cacheKey];
        }
        if ((bool) $User->restricted === false) {
            /*
            **  Ohne "restricted"-Flag (restricted = 0) hat der User Zugriff auf den Mandanten, dem
            **  er direkt zugeordnet ist, und alle zugehörigen Sub-Mandanten.
            **
            **  Die Mandanten werden hier nach der ID sortiert ausgelesen, weil davon auszugehen ist,
            **  dass ein Sub-Mandant immer erst nach seinem Master angelegt werden kann. Der umgekehrte
            **  Fall kann nur dann eintreten, wenn Änderungen direkt in der Datenbank vorgenommen werden,
            **  was tunlichst vermieden werden sollte.
            **
            **  Der Aktivstatus der Mandanten wird an dieser Stelle nicht berücksichtigt. Es werden alle
            **  Mandanten zum User ermittelt, unabhängig von ihrem Status.
            **
            **  @todo   Dieser Teil kann performanter gemacht werden, indem das Tenant-Model um eine
            **          Hex-Zeichenkette erweitert wird, die die Hierarchie der Tenants widerspiegelt.
            **          Beispiel:

            **          ID  Name        Master  Hex
            **          1   System      NULL    00
            **          2   Mandant 2   1       00 01
            **          3   Mandant 3   1       00 02
            **          4   Mandant 4   2       00 02 01
            **          5   Mandant 5   3       00 03 01
            **          6   Mandant 6   3       00 03 02
            **          7   Mandant 7   6       00 03 02 01
            **
            **          Um alle Untermandanten zu einem bekannten Mandanten zu ermitteln kann dann einfach
            **          folgendes gemacht werden:
            **
            **              ... ->where('hex' '!=', $masterHex)
            **                  ->whereLike('hex', $masterHex.'%')
            */
            $Tenants = static::$_modelClass::orderBy('id')->get();

            $userTenants = [];

            foreach ($Tenants as $Tenant) {
                $tenantId = $Tenant->id;
                $masterId = $Tenant->master_id;
                /*
                **  Den Mandanten, dem der User direkt zugeordnet ist, verarbeiten. */
                if ($tenantId === (int) $User->tenant_id) {
                    $tenantData = $Tenant->toArray();
                    $tenantData['access_level'] = $User->access_level;

                    $userTenants[$tenantId] = $tenantData;

                    continue;
                }
                /*
                **  Prüfen ob die aktuelle Master-ID bereits als Schlüssel im Ergebnis-Array
                **  enthalten ist. Tenant überspringen, wenn das nicht der Fall ist. */
                if (!isset($userTenants[$masterId])) {
                    continue;
                }
                $tenantData = $Tenant->toArray();
                $tenantData['access_level'] = $User->access_level;

                $userTenants[$tenantId] = $tenantData;
            }
            return $cache[$cacheKey] = $userTenants;
        }
        /*
        **  Bei einem "restricted" User muss überprüft werden ob es Mandanten-Verknüpfungen gibt. */
        $TenantAccess = TenantUserRepository::getUserTenants($User->id);

        if ($TenantAccess->isEmpty()) {
            /*
            **  Wenn es keine Verknüpfungen gibt, dann beschränkt sich der Zugriff des Users auf den
            **  Mandanten unter dem er angelegt wurde. */
            $Tenant = static::$_modelClass::find($User->tenant_id);

            $tenantData = $Tenant->toArray();
            $tenantData['access_level'] = $User->access_level;

            return $cache[$cacheKey] = [
                $Tenant->id => $tenantData,
            ];
        }
        /*
        **  Wenn es Verknüpfungen gibt, dann verfällt der Zugriff des Users auf den Mandanten unter
        **  dem er angelegt wurde und er hat nur Zugriff auf die verknüpften Mandanten.
        **  Zugriff auf den Master-Mandanten hat er nur dann noch, wenn dieser  ebenfalls verknüpft
        **  ist.
        **
        **  IDs und AccessLevel zu den verknüpften Mandanten ermitteln. */
        $tenantIDs = $TenantAccess->pluck('tenant_id')->toArray();
        $accessLevels = array_combine($tenantIDs, $TenantAccess->pluck('access_level')->toArray());
        /*
        **  Mandanten zu den Verknüpfungs-IDs auslesen und abarbeiten. */
        $Tenants = static::$_modelClass::whereIn('id', $tenantIDs)
            ->orderBy('id')
            ->get();

        $userTenants = [];

        foreach ($Tenants as $Tenant) {
            $tenantId = $Tenant->id;

            $tenant = $Tenant->toArray();

            $tenant['access_level'] = $accessLevels[$tenantId];

            $userTenants[$tenantId] = $tenant;
        }
        return $cache[$cacheKey] = $userTenants;

    } // determineUserTenants()


    /**
     * Returns a collection of models
     *
     * @param       bool $ignoreCache
     *              Controls the use of the method cache when determining the result.
     *              If TRUE is passed, a database access is carried out even if the method cache
     *              already contains a suitable entry. The storage of a value in the method cache
     *              is not affected by this parameter.
     *
     * @param       bool $ignoreRestriction
     *              By default, the result is limited to the tenants of the current user. The
     *              restriction can be removed by setting this parameter to TRUE.
     *
     * @param       array|string|null $orderBy
     *              @see BplanBase\Globals\Helpers\QueryHelper::applyOrderBy() for
     *              details.
     *
     * @param       array|bool|int $paginate
     *              Can be either the number of elements, TRUE or an array. If
     *              TRUE is passed, the default number from the configuration is
     *              used. If an array is passed, then all known arguments for
     *              paginate() (columns, page, pageName, perPage, total) can be
     *              specified in it (all optional).
     *              @see https://api.laravel.com/docs/12.x/Illuminate/Database/Eloquent/Builder.html#method_paginate
     *
     * @param       Builder|null $Query
     *
     * @param       bool $withInactive
     *              By default, only the active data records are returned. By setting this parameter
     *              to TRUE, the inactive data records can also be read out.
     *
     * @param       bool $withInternal
     *              By default, only the data records marked as "internal = 0" are read out. By
     *              setting this parameter to TRUE, the internal status is ignored and all data
     *              records are read.
     *
     * @return      Collection|LengthAwarePaginator
     *
     * @version     2.0.0 / 2025-06-04
     * @author      Emilio Cannarozzo <emilio.cannarozzo@bplan-solutions.de>
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public static function getAll(bool $ignoreCache = false, bool $ignoreRestriction = false, array|string|null $orderBy = null, array|bool|int $paginate = false, Builder|null $Query = null, bool $withInactive = false, bool $withInternal = false): Collection|LengthAwarePaginator
    {
        return parent::getAll($ignoreCache, $ignoreRestriction, $orderBy, $paginate, $Query, true, true);

    } // getAll()


    /**
     * Returns the model instance for the given identifier
     *
     * @param       int|string $identifier
     *
     * @param       bool $ignoreCache
     *              Controls the use of the method cache when determining the result.
     *              If TRUE is passed, a database access is carried out even if the method cache
     *              already contains a suitable entry. The storage of a value in the method cache
     *              is not affected by this parameter.
     *
     * @param       bool $ignoreRestriction
     *              By default, the result is limited to the tenants of the current user. The
     *              restriction can be removed by setting this parameter to TRUE.
     *
     * @param       bool $returnOrFail
     *              Controls the return of the method.
     *              If no suitable entry can be found in the database, either NULL is returned (TRUE)
     *              or an exception is thrown (FALSE). This allows the result of the method call to
     *              be used directly in the Api controller.
     *
     * @throws      Illuminate\Database\Eloquent\ModelNotFoundException
     *
     * @return      Tenant|null
     *
     * @version     2.1.0 / 2025-07-16
     * @author      Emilio Cannarozzo <emilio.cannarozzo@bplan-solutions.de>
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public static function getByIdentifier(int|string $identifier, bool $ignoreCache = false, bool $ignoreRestriction = false, bool $returnOrFail = true): Tenant|null
    {
        static $cache = [];

        $cacheKey = md5(serialize(func_get_args()));

        if ($ignoreCache === false && isset($cache[$cacheKey])) {
            return $cache[$cacheKey];
        }
        $Query = static::$_modelClass::query();

        if ($ignoreRestriction === false) {
            $AuthUser = auth()->user();
            /*
            **  Hier wird bewusst nicht mit der Liste der Tenants aus der Registry gearbeitet, weil im
            **  Falle einer Änderung nicht sichergestellt ist, dass diese Liste zum Zeitpunkt der Abfrage
            **  auf dem aktuellen Stand, geschweige denn überhaupt schon verfügbar, ist. */
            $userTenants = self::determineUserTenants($AuthUser, ignoreRestriction: $ignoreRestriction);

            $Query->whereIn('id', array_keys($userTenants));
        }
        $Query->where('identifier', '=', $identifier);

        if ($returnOrFail === true) {
            return $cache[$cacheKey] = $Query->first();
        }
        return $cache[$cacheKey] = $Query->firstOrFail();

    } // getByIdentifier()


    /**
     * Returns the model instance for the given id, uuid or identifier
     *
     * @param       int|string $various
     *
     * @param       bool $ignoreCache
     *              Controls the use of the method cache when determining the result.
     *              If TRUE is passed, a database access is carried out even if the method cache
     *              already contains a suitable entry. The storage of a value in the method cache
     *              is not affected by this parameter.
     *
     * @param       bool $ignoreRestriction
     *              By default, the result is limited to the tenants of the current user. The
     *              restriction can be removed by setting this parameter to TRUE.
     *
     * @param       bool $returnOrFail
     *              Controls the return of the method.
     *              If no suitable entry can be found in the database, either NULL is returned (TRUE)
     *              or an exception is thrown (FALSE). This allows the result of the method call to
     *              be used directly in the Api controller.
     *
     * @throws      Illuminate\Database\Eloquent\ModelNotFoundException
     *
     * @return      Tenant|null
     *
     * @version     1.0.0 / 2025-07-16
     * @author      Emilio Cannarozzo <emilio.cannarozzo@bplan-solutions.de>
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public static function getByVarious(int|string $various, bool $ignoreCache = false, bool $ignoreRestriction = false, bool $returnOrFail = true): Tenant|null
    {
        if (is_numeric($various)) {
            return self::getById($various, $ignoreCache, $ignoreRestriction, $returnOrFail);
        }
        if (strlen($various) === 36) {
            return self::getByUuid($various, $ignoreCache, $ignoreRestriction, $returnOrFail);
        }
        return self::getByIdentifier($various, $ignoreCache, $ignoreRestriction, $returnOrFail);

    } // getByVarious()


} // class TenantRepository extends BaseRepository {}
