<?php

/**
 * Copyright Maarch since 2008 under license.
 * See LICENSE.txt file at the root folder for more details.
 * This file is part of Maarch software.
 */

/**
 * @brief Authentication Controller
 *
 * @author dev@maarch.org
 */

namespace SrcCore\controllers;

use Configuration\models\ConfigurationModel;
use Email\controllers\EmailController;
use Firebase\JWT\JWT;
use History\controllers\HistoryController;
use Respect\Validation\Validator;
use Slim\Http\Request;
use Slim\Http\Response;
use SrcCore\models\AuthenticationModel;
use SrcCore\models\CoreConfigModel;
use SrcCore\models\PasswordModel;
use SrcCore\models\ValidatorModel;
use User\controllers\UserController;
use User\models\UserModel;

class AuthenticationController
{
    const MAX_DURATION_TOKEN = 30; //Minutes
    const ROUTES_WITHOUT_AUTHENTICATION = [
        'GET/authenticationInformations', 'POST/authenticate', 'GET/authenticate/token',
        'POST/password', 'PUT/password', 'GET/passwordRules', 'GET/languages/{lang}', 'GET/commitInformation',
        'GET/documents/{id}/workflows/{workflowId}/files/{fileId}'
    ];


    public function getInformations(Request $request, Response $response)
    {
        $connection = ConfigurationModel::getConnection();
        $encryptKey = CoreConfigModel::getEncryptKey();
        $path = CoreConfigModel::getConfigPath();
        $coreUrl = UrlController::getCoreUrl();
        $hashedPath = md5($path);

        $emailConfiguration = ConfigurationModel::getByIdentifier(['identifier' => 'emailServer', 'select' => ['value']]);
        $emailConfiguration = !empty($emailConfiguration[0]['value']) ? json_decode($emailConfiguration[0]['value'], true) : null;

        $mailServerOnline = !empty($emailConfiguration['online']);

        $customization = ConfigurationModel::getByIdentifier(['identifier' => 'customization', 'select' => ['value']]);
        $customization = !empty($customization[0]['value']) ? json_decode($customization[0]['value'], true) : null;

        return $response->withJson([
            'connection'       => $connection,
            'changeKey'        => $encryptKey == 'Security Key Maarch Parapheur #2008',
            'instanceId'       => $hashedPath,
            'coreUrl'          => $coreUrl,
            'mailServerOnline' => $mailServerOnline,
            'loginMessage'     => $customization['loginMessage'],
            'applicationUrl'   => $customization['applicationUrl']
        ]);
    }

    public static function authentication($authorizationHeaders = [])
    {
        $id = null;
        if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
            $login = strtolower($_SERVER['PHP_AUTH_USER']);
            if (AuthenticationModel::authentication(['login' => $login, 'password' => $_SERVER['PHP_AUTH_PW']])) {
                $user = UserModel::getByLogin(['select' => ['id'], 'login' => $login]);
                $id = $user['id'];
            }
        } else {
            if (!empty($authorizationHeaders)) {
                $token = null;
                foreach ($authorizationHeaders as $authorizationHeader) {
                    if (strpos($authorizationHeader, 'Bearer') === 0) {
                        $token = str_replace('Bearer ', '', $authorizationHeader);
                    }
                }
                if (!empty($token)) {
                    try {
                        $jwt = (array)JWT::decode($token, CoreConfigModel::getEncryptKey(), ['HS256']);
                    } catch (\Exception $e) {
                        return null;
                    }
                    $jwt['user'] = (array)$jwt['user'];
                    if (!empty($jwt) && !empty($jwt['user']['id'])) {
                        $id = $jwt['user']['id'];
                    }
                }
            }
        }

        return $id;
    }

    public function authenticate(Request $request, Response $response)
    {
        $body = $request->getParsedBody();

        $connection = ConfigurationModel::getConnection();
        if (in_array($connection, ['default', 'ldap'])) {
            if (!array_key_exists('login', $body) || !array_key_exists('password', $body) || !Validator::stringType()->notEmpty()->validate($body['login']) || !Validator::stringType()->notEmpty()->validate($body['password'])) {
                return $response->withStatus(400)->withJson(['errors' => 'Bad Request']);
            }
        }

        $login = strtolower($body['login']);

        if ($connection == 'ldap') {
            $ldapConfigurations = ConfigurationModel::getByIdentifier(['identifier' => 'ldapServer', 'select' => ['value']]);
            if (empty($ldapConfigurations)) {
                return $response->withStatus(400)->withJson(['errors' => 'Ldap configuration is missing']);
            }
            foreach ($ldapConfigurations as $ldapConfiguration) {
                $ldapConfiguration = json_decode($ldapConfiguration['value'], true);
                $uri = ($ldapConfiguration['ssl'] === true ? "LDAPS://{$ldapConfiguration['uri']}" : $ldapConfiguration['uri']);
                $ldap = @ldap_connect($uri);
                if ($ldap === false) {
                    $error = 'Ldap connect failed : uri is maybe wrong';
                    continue;
                }
                ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
                ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
                ldap_set_option($ldap, LDAP_OPT_NETWORK_TIMEOUT, 10);
                $ldapLogin = (!empty($ldapConfiguration['prefix']) ? $ldapConfiguration['prefix'] . '\\' . $body['login'] : $body['login']);
                $ldapLogin = (!empty($ldapConfiguration['suffix']) ? $ldapLogin . $ldapConfiguration['suffix'] : $ldapLogin);
                if (!empty($ldapConfiguration['baseDN'])) { //OpenLDAP
                    $search = @ldap_search($ldap, $ldapConfiguration['baseDN'], "(uid={$ldapLogin})", ['dn']);
                    if ($search === false) {
                        $error = 'Ldap search failed : baseDN is maybe wrong => ' . ldap_error($ldap);
                        continue;
                    }
                    $entries = @ldap_get_entries($ldap, $search);
                    if ($entries === false) {
                        $error = 'Ldap fetching failed : ' . ldap_error($ldap);
                        continue;
                    }
                    if ($entries['count'] < 1) {
                        $error = 'No entries found in ldap search : invalid user DN or ldap configuration';
                        continue;
                    }
                    $ldapLogin = $entries[0]['dn'];
                }
                $authenticated = @ldap_bind($ldap, $ldapLogin, $body['password']);
                if (!$authenticated) {
                    $error = ldap_error($ldap);
                }
            }
            if (empty($authenticated) && !empty($error) && $error != 'Invalid credentials') {
                return $response->withStatus(400)->withJson(['errors' => $error]);
            }
        } elseif ($connection == 'x509') {
            if (!empty($_SERVER['SSL_CLIENT_CERT'])) {
                $x509Fingerprint = openssl_x509_fingerprint($_SERVER["SSL_CLIENT_CERT"]);
                $user = UserModel::get(['select' => ['login'], 'where' => ['x509_fingerprint = ?'], 'data' => [strtoupper(wordwrap($x509Fingerprint, 2, " ", true))]]);
                if (empty($user)) {
                    return $response->withStatus(401)->withJson(['errors' => 'Authentication unauthorized']);
                }
                $login = $user[0]['login'];
            } else {
                return $response->withStatus(401)->withJson(['errors' => 'No certificate detected']);
            }
            $authenticated = true;
        } elseif ($connection == 'kerberos') {
            if (!empty($_SERVER['REMOTE_USER']) && $_SERVER['AUTH_TYPE'] == 'Negotiate') {
                $login = strtolower($_SERVER['REMOTE_USER']);
            } else {
                return $response->withStatus(401)->withJson(['errors' => 'No identifier detected for kerberos']);
            }
            $authenticated = true;
        } else if ($connection == 'azure_saml') {
            $authenticated = AuthenticationController::azureSamlConnection();
            if (!empty($authenticated['errors'])) {
                return $response->withStatus(401)->withJson(['errors' => $authenticated['errors']]);
            }
            $login = strtolower($authenticated['login']);
            $authenticated = true;
        } else {
            $authenticated = AuthenticationModel::authentication(['login' => $login, 'password' => $body['password']]);
        }
        if (empty($authenticated)) {
            $user = UserModel::getByLogin(['login' => $login, 'select' => ['id']]);

            if (!empty($user)) {
                $handle = AuthenticationController::handleFailedAuthentication(['userId' => $user['id']]);
                if (!empty($handle['accountLocked'])) {
                    return $response->withStatus(401)->withJson(['errors' => 'Account Locked', 'date' => $handle['lockedDate']]);
                }
            }
            return $response->withStatus(401)->withJson(['errors' => 'Authentication Failed']);
        }

        $user = UserModel::getByLogin(['login' => $login, 'select' => ['id', '"isRest"', 'refresh_token']]);
        if (empty($user) || $user['isRest']) {
            return $response->withStatus(403)->withJson(['errors' => 'Authentication unauthorized']);
        }

        $GLOBALS['id'] = $user['id'];

        $user['refresh_token'] = json_decode($user['refresh_token'], true);
        foreach ($user['refresh_token'] as $key => $refreshToken) {
            try {
                JWT::decode($refreshToken, CoreConfigModel::getEncryptKey(), ['HS256']);
            } catch (\Exception $e) {
                unset($user['refresh_token'][$key]);
            }
        }
        $user['refresh_token'] = array_values($user['refresh_token']);
        if (count($user['refresh_token']) > 10) {
            array_shift($user['refresh_token']);
        }

        $refreshToken = AuthenticationController::getRefreshJWT();
        $user['refresh_token'][] = $refreshToken;
        UserModel::update([
            'set'   => ['reset_token' => null, 'refresh_token' => json_encode($user['refresh_token'])],
            'where' => ['id = ?'],
            'data'  => [$user['id']]
        ]);
        $response = $response->withHeader('Token', AuthenticationController::getJWT());
        $response = $response->withHeader('Refresh-Token', $refreshToken);

        HistoryController::add([
            'code'          => 'OK',
            'objectType'    => 'users',
            'objectId'      => $user['id'],
            'type'          => 'LOGIN',
            'message'       => '{userLogIn}'
        ]);

        return $response->withStatus(204);
    }

    private static function azureSamlConnection()
    {
        $libDir = CoreConfigModel::getLibrariesDirectory();
        if (!is_file($libDir . 'simplesamlphp/lib/_autoload.php')) {
            return ['errors' => 'Library simplesamlphp not present'];
        }

        require_once($libDir . 'simplesamlphp/lib/_autoload.php');
        $as = new \SimpleSAML\Auth\Simple('default-sp');
        $as->requireAuth([
            'ReturnTo'          => UrlController::getCoreUrl(),
            'skipRedirection'   => true
        ]);

        $attributes = $as->getAttributes();
        $login = $attributes['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'][0];
        if (empty($login)) {
            return ['errors' => 'Authentication Failed : login not present in attributes'];
        }

        return ['login' => $login];
    }

    public function getRefreshedToken(Request $request, Response $response)
    {
        $queryParams = $request->getQueryParams();

        if (!Validator::stringType()->notEmpty()->validate($queryParams['refreshToken'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Refresh Token is empty']);
        }

        try {
            $jwt = JWT::decode($queryParams['refreshToken'], CoreConfigModel::getEncryptKey(), ['HS256']);
        } catch (\Exception $e) {
            return $response->withStatus(401)->withJson(['errors' => 'Authentication Failed']);
        }

        $user = UserModel::getById(['select' => ['id', 'refresh_token'], 'id' => $jwt->user->id]);
        if (empty($user['refresh_token'])) {
            return $response->withStatus(401)->withJson(['errors' => 'Authentication Failed']);
        }

        $user['refresh_token'] = json_decode($user['refresh_token'], true);
        if (!in_array($queryParams['refreshToken'], $user['refresh_token'])) {
            return $response->withStatus(401)->withJson(['errors' => 'Authentication Failed']);
        }

        $GLOBALS['id'] = $user['id'];

        return $response->withJson(['token' => AuthenticationController::getJWT()]);
    }

    public function logout(Request $request, Response $response)
    {
        HistoryController::add([
            'code'          => 'OK',
            'objectType'    => 'users',
            'objectId'      => $GLOBALS['id'],
            'type'          => 'LOGOUT',
            'message'       => '{userLogOut}'
        ]);

        $response->withStatus(204);
    }

    public static function getJWT()
    {
        $sessionTime = AuthenticationController::MAX_DURATION_TOKEN;

        $loadedXml = CoreConfigModel::getConfig();
        if ($loadedXml) {
            if (!empty($loadedXml->config->sessionTime)) {
                if ($sessionTime > (int)$loadedXml->config->sessionTime) {
                    $sessionTime = (int)$loadedXml->config->sessionTime;
                }
            }
        }

        $user = UserController::getUserInformationsById(['id' => $GLOBALS['id']]);
        unset($user['picture']);

        $token = [
            'exp'        => time() + 60 * $sessionTime,
            'user'       => $user,
            'connection' => ConfigurationModel::getConnection()
        ];

        $jwt = JWT::encode($token, CoreConfigModel::getEncryptKey());

        return $jwt;
    }

    public static function getRefreshJWT()
    {
        $sessionTime = AuthenticationController::MAX_DURATION_TOKEN;

        $loadedXml = CoreConfigModel::getConfig();
        if ($loadedXml) {
            if (!empty($loadedXml->config->sessionTime)) {
                $sessionTime = (int)$loadedXml->config->sessionTime;
            }
        }

        $token = [
            'exp'   => time() + 60 * $sessionTime,
            'user'  => [
                'id' => $GLOBALS['id']
            ]
        ];

        $jwt = JWT::encode($token, CoreConfigModel::getEncryptKey());

        return $jwt;
    }

    public static function getResetJWT($args = [])
    {
        $token = [
            'exp'   => time() + $args['expirationTime'],
            'user'  => [
                'id' => $args['id']
            ],
            'connection' => ConfigurationModel::getConnection()
        ];

        $jwt = JWT::encode($token, CoreConfigModel::getEncryptKey());

        return $jwt;
    }

    public static function sendAccountActivationNotification(array $args)
    {
        $connection = ConfigurationModel::getConnection();
        if ($connection == 'default') {
            $resetToken = AuthenticationController::getResetJWT(['id' => $args['userId'], 'expirationTime' => 1209600]); // 14 days
            UserModel::update(['set' => ['reset_token' => $resetToken], 'where' => ['id = ?'], 'data' => [$args['userId']]]);
    
            $user = UserModel::getById(['select' => ['login'], 'id' => $args['userId']]);
            $lang = LanguageController::get(['lang' => 'fr']);
    
            $url = ConfigurationModel::getApplicationUrl() . 'dist/#/update-password?token=' . $resetToken;
            EmailController::createEmail([
                'userId' => $args['userId'],
                'data'   => [
                    'sender'     => 'Notification',
                    'recipients' => [$args['userEmail']],
                    'subject'    => $lang['notificationNewAccountSubject'],
                    'body'       => $lang['notificationNewAccountBody'] . '<a href="' . $url . '">'.$url.'</a>' . $lang['notificationNewAccountId'] . ' ' . $user['login'] . $lang['notificationNewAccountFooter'],
                    'isHtml'     => true
                ]
            ]);
        }

        return true;
    }

    public static function isRouteAvailable(array $args)
    {
        ValidatorModel::notEmpty($args, ['userId', 'currentRoute']);
        ValidatorModel::intVal($args, ['userId']);
        ValidatorModel::stringType($args, ['currentRoute']);

        $user = UserModel::getById(['select' => ['password_modification_date', '"isRest"'], 'id' => $args['userId']]);

        if (!in_array($args['currentRoute'], ['/passwordRules', '/users/{id}/password']) && empty($user['isRest'])) {
            $connectionConfiguration = ConfigurationModel::getByIdentifier(['identifier' => 'connection', 'select' => ['value']]);

            if ($connectionConfiguration[0]['value'] == '"default"') {
                $passwordRules = PasswordModel::getEnabledRules();
                if (!empty($passwordRules['renewal'])) {
                    $currentDate = new \DateTime();
                    $lastModificationDate = new \DateTime($user['password_modification_date']);
                    $lastModificationDate->add(new \DateInterval("P{$passwordRules['renewal']}D"));

                    if ($currentDate > $lastModificationDate) {
                        return ['isRouteAvailable' => false, 'errors' => 'Password expired : User must change his password'];
                    }
                }
            }
        }

        return ['isRouteAvailable' => true];
    }

    public static function handleFailedAuthentication(array $args)
    {
        ValidatorModel::notEmpty($args, ['userId']);
        ValidatorModel::intVal($args, ['userId']);

        $passwordRules = PasswordModel::getEnabledRules();

        if (!empty($passwordRules['lockAttempts'])) {
            $user = UserModel::getById(['select' => ['failed_authentication', 'locked_until'], 'id' => $args['userId']]);
            $set = [];
            if (!empty($user['locked_until'])) {
                $currentDate = new \DateTime();
                $lockedUntil = new \DateTime($user['locked_until']);
                if ($lockedUntil < $currentDate) {
                    $set['locked_until'] = null;
                    $user['failed_authentication'] = 0;
                } else {
                    return ['accountLocked' => true, 'lockedDate' => $user['locked_until']];
                }
            }

            $set['failed_authentication'] = $user['failed_authentication'] + 1;
            UserModel::update([
                'set'       => $set,
                'where'     => ['id = ?'],
                'data'      => [$args['userId']]
            ]);

            if (!empty($user['failed_authentication']) && ($user['failed_authentication'] + 1) >= $passwordRules['lockAttempts'] && !empty($passwordRules['lockTime'])) {
                $lockedUntil = time() + 60 * $passwordRules['lockTime'];
                UserModel::update([
                    'set'       => ['locked_until'  => date('Y-m-d H:i:s', $lockedUntil)],
                    'where'     => ['id = ?'],
                    'data'      => [$args['userId']]
                ]);
                return ['accountLocked' => true, 'lockedDate' => date('Y-m-d H:i:s', $lockedUntil)];
            }
        }

        return true;
    }

    public function getGitCommitInformation(Request $request, Response $response)
    {
        if (!file_exists('.git/HEAD')) {
            return $response->withJson(['hash' => null]);
        }

        $head = file_get_contents('.git/HEAD');

        if ($head === false) {
            return $response->withJson(['hash' => null]);
        }
        preg_match('#^ref:(.+)$#', $head, $matches);
        $currentHead = trim($matches[1]);

        if (empty($currentHead)) {
            return $response->withJson(['hash' => null]);
        }

        $hash = file_get_contents('.git/' . $currentHead);
        if ($hash === false) {
            return $response->withJson(['hash' => null]);
        }

        $hash = explode("\n", $hash)[0];

        return $response->withJson(['hash' => $hash]);
    }
}