<?php

namespace QUI\MasterLogin;

use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Types\Type;
use QUI;
use QUI\System\Console\Tools\MigrationV2;

use function date;

/**
 * Class Events
 *
 * Main event handler for quiqqer/masterlogin
 */
class Events
{
    /**
     * @var array<string, array{dbGroups: list<string>, objectGroups: list<string>, session: array<string, mixed>}>
     */
    protected static array $userSaveSnapshots = [];

    /**
     * quiqqer/core: onTemplateGetHeader
     *
     * Load MasterLogin control in template
     *
     * @param QUI\Template $Template
     * @return void
     */
    public static function onTemplateGetHeader(QUI\Template $Template): void
    {
        if (!QUI::getUsers()->isAuth(QUI::getUserBySession()) || !Manager::canUse()) {
            return;
        }

        $masterUser = '';
        $slaveUser = '';

        $masterUserData = [];
        $slaveUserData = [];

        if (Manager::isLoggedInAsSlave()) {
            // Fetch master user info
            $masterUserId = QUI::getSession()->get(Manager::SESSION_MASTER_USER_ID);

            try {
                if (!is_string($masterUserId) && !is_int($masterUserId)) {
                    throw new QUI\Exception('Master user id not found in session.');
                }

                $MasterUser = QUI::getUsers()->get($masterUserId);

                $masterUser = $MasterUser->getName() . ' (#' . $MasterUser->getId() . ')';
                $masterUserData = [
                    'id' => $MasterUser->getId(),
                    'name' => $MasterUser->getName(),
                    'email' => $MasterUser->getAttribute('email')
                ];
            } catch (\Exception $Exception) {
                QUI\System\Log::writeException($Exception);
            }

            // Fetch slave user info (session user)
            /** @var QUI\Users\User $SlaveUser */
            $SlaveUser = QUI::getUserBySession();
            $slaveName = $SlaveUser->getName();
            $slaveCompany = '';

            try {
                $SlaveUserAddress = $SlaveUser->getStandardAddress();
                $slaveCompany = $SlaveUserAddress?->getAttribute('company') ?: '';
                $slaveAddressName = $SlaveUserAddress?->getName();

                if (!empty($slaveAddressName)) {
                    $slaveName = $slaveAddressName;
                }
            } catch (\Exception $Exception) {
                QUI\System\Log::writeDebugException($Exception);
            }

            $slaveUser = $slaveName . ' - ' . $slaveCompany . ' (#' . $SlaveUser->getId() . ')';

            $slaveUserData = [
                'id' => $SlaveUser->getId(),
                'name' => $slaveName,
                'email' => $SlaveUser->getAttribute('email'),
                'company' => $slaveCompany
            ];
        } else {
            // no slave user, get admin user data
            $MasterUser = QUI::getUserBySession();
            $masterUser = $MasterUser->getName() . ' (#' . $MasterUser->getId() . ')';
            $masterUserData = [
                'id' => $MasterUser->getId(),
                'name' => $MasterUser->getName(),
                'email' => $MasterUser->getAttribute('email')
            ];
        }

        $masterUserData = json_encode($masterUserData) ?: '';
        $slaveUserData = json_encode($slaveUserData) ?: '';

        $Template->extendFooter(
            '<div data-qui="package/quiqqer/masterlogin/bin/js/frontend/controls/MasterLogin"
                  data-qui-options-slave="' . (Manager::isLoggedInAsSlave() ? 1 : 0) . '"
                  data-qui-options-masteruser="' . self::escapeHtmlAttribute($masterUser) . '"
                  data-qui-options-slaveuser="' . self::escapeHtmlAttribute($slaveUser) . '"
                  data-qui-options-masteruserdata="' . self::escapeHtmlAttribute($masterUserData) . '"
                  data-qui-options-slaveuserdata="' . self::escapeHtmlAttribute($slaveUserData) . '"
                  data-qui-options-csrftoken="' . self::escapeHtmlAttribute(Manager::getCsrfToken()) . '"
             ></div>'
        );
    }

    /**
     * quiqqer/core: onAdminLoad
     *
     * If the user accesses the backend -> log out of any possible slave user
     *
     * @return void
     */
    public static function onAdminLoad(): void
    {
        Manager::log('masterlogin.admin_load.log_back_requested', [
            'session' => Manager::getSessionLogContext()
        ]);

        Manager::logBack();
    }

    public static function onUserLoginStart(int | string $userId): void
    {
        Manager::log('user.login.start', [
            'userId' => $userId,
            'session' => Manager::getSessionLogContext()
        ]);
    }

    public static function onUserLogin(QUI\Users\User $User): void
    {
        Manager::log('user.login.success', [
            'user' => Manager::getUserLogContext($User),
            'dbGroups' => Manager::getUserGroupsFromDatabase($User),
            'session' => Manager::getSessionLogContext()
        ]);
    }

    public static function onUserLoginError(int | string $userId, \Exception $Exception): void
    {
        $reason = null;

        if (method_exists($Exception, 'getAttribute')) {
            $reason = $Exception->getAttribute('reason');
        }

        Manager::log('user.login.error', [
            'userId' => $userId,
            'reason' => $reason,
            'exception' => $Exception->getMessage(),
            'session' => Manager::getSessionLogContext()
        ], QUI\System\Log::LEVEL_WARNING);
    }

    public static function onUserSaveBegin(QUI\Users\User $User): void
    {
        /** @var array{dbGroups: list<string>, objectGroups: list<string>, session: array<string, mixed>} $snapshot */
        $snapshot = [
            'dbGroups' => Manager::getUserGroupsFromDatabase($User),
            'objectGroups' => Manager::normalizeGroupIds($User->getGroups(false)),
            'session' => Manager::getSessionLogContext()
        ];

        self::$userSaveSnapshots[(string)$User->getUUID()] = $snapshot;

        Manager::log('user.save.begin', [
            'user' => Manager::getUserLogContext($User),
            'dbGroupsBefore' => $snapshot['dbGroups'],
            'objectGroupsBefore' => $snapshot['objectGroups'],
            'session' => $snapshot['session']
        ]);
    }

    public static function onUserSave(QUI\Users\User $User): void
    {
        Manager::log('user.save.before_database_write', [
            'user' => Manager::getUserLogContext($User),
            'objectGroups' => $User->getGroups(false),
            'session' => Manager::getSessionLogContext()
        ]);
    }

    public static function onUserSaveEnd(QUI\Users\User $User): void
    {
        $snapshotKey = (string)$User->getUUID();
        $snapshot = self::$userSaveSnapshots[$snapshotKey] ?? [
            'dbGroups' => [],
            'objectGroups' => [],
            'session' => []
        ];

        unset(self::$userSaveSnapshots[$snapshotKey]);

        $dbGroupsBefore = $snapshot['dbGroups'];
        $dbGroupsAfter = Manager::getUserGroupsFromDatabase($User);
        $normalizedBefore = Manager::normalizeGroupIds($dbGroupsBefore);
        $normalizedAfter = Manager::normalizeGroupIds($dbGroupsAfter);
        $groupsChanged = $normalizedBefore !== $normalizedAfter;

        $context = [
            'user' => Manager::getUserLogContext($User),
            'dbGroupsBefore' => $dbGroupsBefore,
            'dbGroupsAfter' => $dbGroupsAfter,
            'objectGroupsBefore' => $snapshot['objectGroups'],
            'objectGroupsAfter' => $User->getGroups(false),
            'groupsChanged' => $groupsChanged,
            'sessionBefore' => $snapshot['session'],
            'sessionAfter' => Manager::getSessionLogContext()
        ];

        if (self::groupsWereReducedToEveryone($dbGroupsBefore, $dbGroupsAfter)) {
            Manager::log(
                'user.save.groups_reduced_to_everyone',
                $context,
                QUI\System\Log::LEVEL_WARNING
            );
        }

        Manager::log(
            $groupsChanged ? 'user.save.end.groups_changed' : 'user.save.end',
            $context,
            $groupsChanged ? QUI\System\Log::LEVEL_WARNING : QUI\System\Log::LEVEL_INFO
        );
    }

    public static function onUserLogoutBegin(QUI\Users\User $User): void
    {
        Manager::log('user.logout.begin', [
            'user' => Manager::getUserLogContext($User),
            'dbGroups' => Manager::getUserGroupsFromDatabase($User),
            'session' => Manager::getSessionLogContext()
        ]);
    }

    /**
     * quiqqer/core: onUserLogout
     *
     * @param QUI\Users\User $User
     * @return void
     */
    public static function onUserLogout(QUI\Users\User $User): void
    {
        Manager::log('user.logout', [
            'user' => Manager::getUserLogContext($User),
            'dbGroups' => Manager::getUserGroupsFromDatabase($User),
            'session' => Manager::getSessionLogContext()
        ]);

        // Update log entry
        try {
            $Connection = QUI::getDataBaseConnection();
            $Platform = $Connection->getDatabasePlatform();

            $Connection->update(
                QUI\Utils\Doctrine::quoteIdentifier(Manager::getLogTable()),
                [$Platform->quoteSingleIdentifier('end') => date('Y-m-d H:i:s')],
                [
                    $Platform->quoteSingleIdentifier('end') => null,
                    $Platform->quoteSingleIdentifier('slave_id') => $User->getUUID(),
                    $Platform->quoteSingleIdentifier('sec_hash') => QUI::getUsers()->getSecHash()
                ]
            );
        } catch (\Exception $Exception) {
            Manager::log('user.logout.masterlogin_log_update_failed', [
                'user' => Manager::getUserLogContext($User),
                'exception' => $Exception->getMessage(),
                'session' => Manager::getSessionLogContext()
            ], QUI\System\Log::LEVEL_ERROR);

            QUI\System\Log::writeException($Exception);
        }
    }

    public static function onQuiqqerMigrationV2(MigrationV2 $Console): void
    {
        $Console->writeLn('- Migrate master login');
        $table = QUI::getDBTableName('masterlogin_log');
        $SchemaManager = QUI::getSchemaManager();

        if ($SchemaManager->tablesExist([$table])) {
            $Table = $SchemaManager->introspectTable($table);
            $changedColumns = [];

            foreach (['id', 'master_id', 'slave_id'] as $columnName) {
                if (!$Table->hasColumn($columnName)) {
                    continue;
                }

                $changedColumns[$columnName] = new ColumnDiff(
                    $Table->getColumn($columnName),
                    new Column(
                        $columnName,
                        Type::getType('string'),
                        ['length' => 50, 'notnull' => true]
                    )
                );
            }

            if ($changedColumns !== []) {
                $SchemaManager->alterTable(new TableDiff($Table, changedColumns: $changedColumns));
            }
        }

        QUI\Utils\MigrationV1ToV2::migrateUsers($table, [
            'master_id',
            'slave_id',
        ]);
    }

    /**
     * @param list<string> $before
     * @param list<string> $after
     */
    protected static function groupsWereReducedToEveryone(array $before, array $after): bool
    {
        if (count(Manager::normalizeGroupIds($before)) <= 1) {
            return false;
        }

        try {
            $Everyone = new QUI\Groups\Everyone();
            $everyoneId = $Everyone->getUUID();
        } catch (\Throwable) {
            return false;
        }

        return Manager::normalizeGroupIds($after) === [$everyoneId];
    }

    private static function escapeHtmlAttribute(string $value): string
    {
        return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }
}
