<?php

if (!defined('BASEPATH')) {
    exit('No direct script access allowed');
}

/**
 * Name:  Ion Auth
 *
 * Author: Ben Edmunds
 * 		  ben.edmunds@gmail.com
 *         @benedmunds
 *
 * Added Awesomeness: Phil Sturgeon
 *
 * Location: http://github.com/benedmunds/CodeIgniter-Ion-Auth
 *
 * Created:  10.01.2009
 *
 * Description:  Modified auth system based on redux_auth with extensive customization.  This is basically what Redux Auth 2 should be.
 * Original Author name has been kept but that does not mean that the method has not been modified.
 *
 * Requirements: PHP5 or above
 *
 */
class Ion_auth
{
    /**
     * CodeIgniter global
     *
     * @var string
     * */
    protected $ci;

    /**
     * account status ('not_activated', etc ...)
     *
     * @var string
     * */
    protected $status;

    /**
     * message (uses lang file)
     *
     * @var string
     * */
    protected $messages;

    /**
     * error message (uses lang file)
     *
     * @var string
     * */
    protected $errors = [];

    /**
     * error start delimiter
     *
     * @var string
     * */
    protected $error_start_delimiter;

    /**
     * error end delimiter
     *
     * @var string
     * */
    protected $error_end_delimiter;

    /**
     * extra where
     *
     * @var array
     * */
    public $_extra_where = [];

    /**
     * extra set
     *
     * @var array
     * */
    public $_extra_set = [];
    public $mode = 'internal';
    public $restClient = null;
    private $admin_role = 'admin';

    public const TWO_FACTOR_UNSUCCESSFUL_ERROR_MESSAGE = 'Invalid two-factor authentication code';
    public const TWO_FACTOR_UNSUCCESSFUL_ERROR = 'two_factor_unsuccessful';

    /**
     * __construct
     *
     * @return void
     * @author Mathew
     * */
    public function __construct()
    {
        $this->ci = &get_instance();
        $this->ci->load->config('ion_auth', true);
        $this->ci->load->config('appsettings', true);
        $this->ci->load->library('email');
        $this->ci->load->library('session');

        // $this->ci->load->library('Auth_Ldap');
        $this->ci->load->library('encryption');
        $this->ci->lang->load('ion_auth');
        $this->ci->load->model([
            'astrolabe_model',
            'api_rest_model',
            'authentication_model',
            'authentication_model_pgsql',
            'settings_rest_model',
        ]);
        $this->ci->load->helper('cookie');

        $this->messages = [];
        $this->errors = [];
        $this->infos = [];
        $this->email = $this->ci->config->item('mission_portal_email', 'ion_auth');
        $this->message_start_delimiter = $this->ci->config->item('message_start_delimiter', 'ion_auth');
        $this->message_end_delimiter = $this->ci->config->item('message_end_delimiter', 'ion_auth');
        $this->error_start_delimiter = $this->ci->config->item('error_start_delimiter', 'ion_auth');
        $this->error_end_delimiter = $this->ci->config->item('error_end_delimiter', 'ion_auth');
        $this->info_start_delimiter = $this->ci->config->item('info_start_delimiter', 'ion_auth');
        $this->info_end_delimiter = $this->ci->config->item('info_end_delimiter', 'ion_auth');
        //load the mode of authentication

        $this->auth_model_pgsql = $this->ci->authentication_model_pgsql;
        $this->auth_model = $this->ci->authentication_model;

        if (!$this->mode) {
            $this->set_error('backend_error');
            log_message('info', 'cannot find any mode switching to internal database');
            $this->mode = 'internal';
            //return FALSE;
        }

        if ($this->ci->session->userdata('mode') !== false) {
            $this->mode = $this->ci->session->userdata('mode');
        }
    }

    public function setRestClient($restClient)
    {
        $this->restClient = $restClient;
        $this->auth_model->setRestClient($this->getRestClient());
    }

    public function getRestClient()
    {
        return $this->restClient;
    }

    /**
     * __call
     *
     * Acts as a simple way to call model methods without loads of stupid alias'
     *
     * */
    public function __call($method, $arguments)
    {
        if (!method_exists($this->ci->authentication_model, $method)) {
            throw new Exception('Undefined method Ion_auth::' . $method . '() called');
        }

        return call_user_func_array([$this->ci->authentication_model, $method], $arguments);
    }

    /**
     * Change password.
     *
     * @return void
     * @author Mathew
     * */
    public function change_password($identity, $old, $new, $twoFaCode = '')
    {
        try {
            if ($this->auth_model->change_password($identity, $old, $new, $twoFaCode)) {
                $this->set_message('password_change_successful');
                return true;
            }

            $this->set_error('password_change_unsuccessful');
            return false;
        } catch (Exception $e) {
            $this->set_error($e->getMessage());
            return false;
        }
    }

    /**
     * register
     *
     * @return void
     * @author Mathew
     * */
    public function register($data)
    {
        //need to test email activation
        try {
            $id = $this->auth_model->createUser($data);
            if ($id !== false) {
                $this->set_message('account_creation_successful');
                return true;
            } else {
                $this->set_error('account_creation_unsuccessful');
                return false;
            }
        } catch (Exception $e) {
            $this->set_error($e->getMessage());
            return false;
        }
    }

    /**
     * login
     *
     * @return bool
     * @author Mathew
     * */
    public function login(string $username, string $password, bool $remember = false, ?string $twoFaCode = '')
    {
        $return = false;
        try {
            $accessTokenLifeTime = $remember
                ? $this->ci->config->item('remember_user_expire', 'ion_auth')
                : $this->ci->config->item('user_expire', 'ion_auth');

            $val = $this->auth_model->login(
                $username,
                $password,
                life_time: $accessTokenLifeTime,
                twoFaCode: $twoFaCode,
            );
            if (is_array($val) && !empty($val)) {
                $data = $this->ci->api_rest_model->get_api_details();
                $isLdapEnabled = $this->ci->settings_rest_model->app_settings_get_item('ldapEnabled');
                $this->mode = $isLdapEnabled ? 'external' : 'internal';
                $this->ci->session->set_userdata('username', $data['userId']);
                if (isset($data['timezone'])) {
                    $this->ci->session->set_userdata('timezone', $data['timezone']);
                }
                $this->ci->session->set_userdata('access_token', $val['access_token']);
                $this->ci->session->set_userdata('mode', $this->mode);
                $this->on_login_successful($data['userId']);
                $return = true;
            }
        } catch (HttpClient_TooManyRequests | HttpClient_Forbidden $e) {
            $this->set_error($e->getMessage());
        } catch (HttpClient_NotFound $e) {
            $this->set_error('cannot_contact_rest_server');
        } catch (HttpClient_Unauthorized $e) {
            if (str_contains($e->getMessage(), self::TWO_FACTOR_UNSUCCESSFUL_ERROR_MESSAGE)) {
                $this->set_error(self::TWO_FACTOR_UNSUCCESSFUL_ERROR);
            } else {
                $this->set_error('login_unsuccessful');
            }
        } catch (Exception $e) {
            log_message(log_level_for_exception($e), $e->getMessage());
            $this->set_error('login_unsuccessful_unknown_reason');
        }
        return $return;
    }

    /**
     * basic rest login
     *
     * @return void
     * @author Mathew
     * */
    public function basic_rest_login($headers)
    {
        try {
            if (!isset($headers['Authorization']) || empty($headers['Authorization'])) {
                log_message('debug', 'Basic auth login failure. Authorization header is not set.');
                return false;
            }

            $authorizationHeader = $headers['Authorization'];
            $credentials = str_replace('Basic ', '', $authorizationHeader);
            $credentials = explode(':', base64_decode($credentials), 2);
            $val = $this->auth_model->login($credentials[0], $credentials[1]);
            if (is_array($val) && !empty($val)) {
                // get username form api details and set roles and isAdmin flag
                $data = $this->ci->api_rest_model->get_api_details();
                $user = [
                    'id' => $data['userId'],
                    'role' => $this->get_user_role($data['userId']),
                    'isAdmin' => in_array($this->admin_role, $this->get_user_role($data['userId'])),
                ];

                return $user;
            }

            return false;
        } catch (Exception $e) {
            log_message(log_level_for_exception($e), $e->getMessage());
            if ($e instanceof HttpClient_NotFound) {
                $this->set_error('cannot_contact_rest_server');
            } elseif ($e instanceof HttpClient_Unauthorized) {
                $this->set_error('login_unsuccessful');
            } else {
                $this->set_error('login_unsuccessful_unknown_reason');
            }
            return false;
        }
    }

    protected function on_login_successful($username)
    {
        $isFirstLogin = $this->auth_model_pgsql->is_first_login($username, $this->mode);

        if ($isFirstLogin) {
            $newdata = [
                'firstLogin' => true,
            ];
            $this->ci->session->set_userdata($newdata);
            $this->ci->astrolabe_model->add_builtin_profiles($username);
            $this->auth_model_pgsql->add_last_login($username, $this->mode);
        } else {
            $this->ci->session->unset_userdata('firstLogin');
            $this->auth_model_pgsql->update_last_login($username);
        }

        $session_data = [
            'roles' => $this->get_user_role($username),
        ];
        $this->ci->session->set_userdata($session_data);
    }

    /**
     * logout
     *
     * @return void
     * @author Mathew
     * */
    public function logout()
    {
        $token = $this->ci->session->userdata('access_token');
        if ($token) {
            $this->getRestClient()->setupOauthAuth($token);
            $this->auth_model->unsetToken($token);
        }

        $this->ci->session->unset_userdata('username');
        $this->ci->session->unset_userdata('role');
        $this->ci->session->unset_userdata('id');
        $this->ci->session->unset_userdata('user_id');

        //delete the remember me cookies if they exist
        if (get_cookie('identity')) {
            delete_cookie('identity');
        }
        if (get_cookie('remember_code')) {
            delete_cookie('remember_code');
        }
        if (get_cookie('mode')) {
            delete_cookie('mode');
        }

        $this->ci->session->sess_destroy();

        $this->set_message('logout_successful');
        return true;
    }

    /**
     * logged_in
     *
     * @return bool
     * @author Mathew
     * */
    public function logged_in()
    {
        return (bool) $this->ci->session->userdata('username');
    }

    /**
     * is_admin
     *
     * @return bool
     * @author Ben Edmunds
     * */
    public function is_admin($check_real_assigned_roles = false)
    {
        if ($check_real_assigned_roles == false) {
            $user_role = $this->ci->session->userdata('roles');
        } else {
            $user_role = $this->get_user_rolelist($this->ci->session->userdata('username'));
        }

        if ($user_role === false || empty($user_role)) {
            return false;
        }

        return in_array($this->admin_role, $user_role);
    }

    /**
     * is_role
     *
     * @return bool
     * @author Phil Sturgeon
     * */
    public function is_role($check_role)
    {
        $user_role = $this->ci->session->userdata('role');

        if (is_array($check_role)) {
            return in_array($user_role, $check_role);
        }

        return $user_role == $check_role;
    }

    public function get_user_role($username)
    {
        return $this->auth_model->getRolesForUser($username);
    }

    public function get_user_rolelist($id)
    {
        $roles = $this->get_user_role($id);
        if (!empty($roles) && $roles !== false) {
            return $roles;
        }
        return [];
    }

    /**
     *
     * @return array of arrays
     */
    public function get_users_array($page, $count, $usertype, $returnWithMeta = false, $determineInactive = false)
    {
        return $this->auth_model->getAllUsers($page, $count, $usertype, $returnWithMeta, $determineInactive);
    }

    public function get_total_users($usertype)
    {
        $users = $this->auth_model->getAllUsers(1, 1, $usertype, true);
        return $users['total'];
    }

    public function search_user($usertype, $expression, $returnWithMeta = false)
    {
        $users = $this->auth_model->searchUser($usertype, $expression, $returnWithMeta);
        return $users;
    }

    /**
     *
     * @param type $id
     * @return associative array
     */
    public function get_user($id = false)
    {
        try {
            return $this->auth_model->getUserDetails($id);
        } catch (Exception $e) {
            return false;
        }
    }

    /**
     * update_user
     *
     * @param <string> $id user id (name right now)
     * @param <data>   $data user data
     * @return boolean
     *
     * */
    public function update_user($id, $data, $twoFaCode = '')
    {
        $roles = $this->get_user_rolelist($id);
        $admin_role = 'admin';

        /*$count = $this->count_users_in_role($admin_role);
        if ($count <= 1 && in_array($admin_role, $roles) && !in_array($admin_role, $data['roles']))
        {
            $this->set_error('one_admin_required');
            return FALSE;
        }*/

        try {
            if ($this->auth_model->update_user($id, $data, [Cf_RestInstance::CF_2FA_TOKEN_HEADER => $twoFaCode])) {
                $this->set_message('update_successful');
                return true;
            }
        } catch (Exception $e) {
            log_message(
                log_level_for_exception($e),
                sprintf(
                    'Update unsuccessful for user %s with data %s. Error:: %s ',
                    $id,
                    json_encode($data),
                    $e->getMessage(),
                ),
            );
            $this->set_error('update_unsuccessful');
            return false;
        }
    }

    /*public function count_users_in_role($role)
    {
        $users = $this->get_users_array();
        $count = 0;
        foreach ((array) $users as $user)
        {
            if (key_exists('roles',$user) && in_array($role, $user['roles']))
            {
                $count++;
            }
        }
        return $count;
    }*/

    /**
     * delete_user
     *
     * @return void
     * @author Phil Sturgeon
     * */
    public function delete_user($id)
    {
        $roles = $this->get_user_rolelist($id);
        $admin_role = 'admin';

        try {
            //delete from phpcfengine users table
            if (!$this->auth_model_pgsql->delete_user_from_logged_in_table($id)) {
                $this->set_error('user_delete_unsuccessful');
                return false;
            }

            if ($this->auth_model->deleteUser($id)) {
                $this->ci->astrolabe_model->profile_delete_all($id);

                $this->set_message('user_delete_successful');
                return true;
            }
            $this->set_error('user_delete_unsuccessful');
            return false;
        } catch (Exception $e) {
            $this->set_error($e->getMessage());
            return false;
        }
    }

    /**
     * Delete role
     *
     * @param type $name role name
     * @return type bool
     */
    public function delete_role($rolename)
    {
        try {
            if ($this->auth_model->deleteRole($rolename)) {
                $this->set_message('role_delete_successful');
                return true;
            }
            $this->set_error('role_delete_unsuccessful');
            return false;
        } catch (Exception $e) {
            $this->set_error($e->getMessage());
            return false;
        }
    }

    /**
     * set_message_delimiters
     *
     * Set the message delimiters
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function set_message_delimiters($start_delimiter, $end_delimiter)
    {
        $this->message_start_delimiter = $start_delimiter;
        $this->message_end_delimiter = $end_delimiter;

        return true;
    }

    /**
     * set_info_delimiters
     */
    public function set_info_delimiters($start_delimiter, $end_delimiter)
    {
        $this->info_start_delimiter = $start_delimiter;
        $this->info_end_delimiter = $end_delimiter;
        return true;
    }

    /**
     * set_error_delimiters
     *
     * Set the error delimiters
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function set_error_delimiters($start_delimiter, $end_delimiter)
    {
        $this->error_start_delimiter = $start_delimiter;
        $this->error_end_delimiter = $end_delimiter;

        return true;
    }

    /**
     * set_message
     *
     * Set a message
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function set_message($message)
    {
        $this->messages[] = $message;

        return $message;
    }

    /**
     * messages
     *
     * Get the messages
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function messages()
    {
        $_output = '';
        foreach ($this->messages as $message) {
            if ($this->ci->lang->line($message, log_errors: false) != '') {
                $_output .=
                    $this->message_start_delimiter . $this->ci->lang->line($message) . $this->message_end_delimiter;
            } else {
                $_output .= $this->error_start_delimiter . $message . $this->error_end_delimiter;
            }
        }

        return $_output;
    }

    /**
     * set_error
     *
     * Set an error message
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function set_error($error)
    {
        $this->errors[] = $error;

        return $error;
    }

    /**
     * errors
     *
     * Get the error message
     *
     * @return void
     * @author Ben Edmunds
     * */
    public function errors()
    {
        $_output = '';
        foreach ($this->errors as $error) {
            if ($this->ci->lang->line($error, log_errors: false) != '') {
                $_output .= $this->error_start_delimiter . $this->ci->lang->line($error) . $this->error_end_delimiter;
            } else {
                $_output .= $this->error_start_delimiter . $error . $this->error_end_delimiter;
            }
        }

        return $_output;
    }

    /**
     * To set the information to be displayed to user
     * @param <type> $info
     * @return <type>
     */
    public function set_info($info)
    {
        $this->infos[] = $info;
        return $info;
    }

    /**
     * get all the information to be displayed
     * @return string
     */
    public function infos()
    {
        $_output = '';
        foreach ($this->infos as $info) {
            // do not log errors if line not found, otherwise private information can be leaked
            if ($this->ci->lang->line($info, log_errors: false) != '') {
                $_output .=
                    $this->message_start_delimiter . $this->ci->lang->line($info) . $this->message_end_delimiter;
            } else {
                $_output .= $this->error_start_delimiter . $info . $this->error_end_delimiter;
            }
        }
        return $_output;
    }

    public function get_roles()
    {
        return $this->auth_model->getAllRoles();
    }

    public function get_default_role()
    {
        return $this->auth_model->getDefaultRole();
    }

    public function set_default_role($name)
    {
        return $this->auth_model->setDefaultRole($name);
    }

    /**
     * Returns the details of a role in arrAY.
     * @param type $rolename
     * @return type
     */
    public function get_role_detail($rolename)
    {
        try {
            $data = $this->auth_model->getRoleDetails($rolename);
            if (is_array($data)) {
                return $data;
            }
            $this->set_error('error_fetching_details');
            return false;
        } catch (Exception $e) {
            $this->set_error($e->getMessage());
        }
    }

    public function create_role($data)
    {
        if ($this->auth_model->createRole($data)) {
            $this->set_message('role_creation_successful');
            return true;
        } else {
            $this->set_error('role_creation_unsuccessful');
            return false;
        }
    }

    public function update_role($rolename, $data)
    {
        try {
            if ($this->auth_model->updateRole($rolename, $data)) {
                $this->set_message('role_update_successful');
                return true;
            }

            $this->set_error('role_update_unsuccessful');
            return false;
        } catch (Exception $e) {
            $this->set_error($e->getMessage());
            return false;
        }
    }

    public function set_mode($mode)
    {
        $this->mode = $mode;
    }

    public function reset_password($username)
    {
        try {
            $newPassword = $this->auth_model->resetPassword($username);
        } catch (Exception $e) {
            if (str_contains($e->getMessage(), self::TWO_FACTOR_UNSUCCESSFUL_ERROR_MESSAGE)) {
                $this->set_error($e->getMessage());
            } else {
                $this->set_error('password_reset_unsucessful');
            }
            return false;
        }

        if ($newPassword !== false) {
            $status = $this->send_email($username, $newPassword);
            if ($status) {
                $this->set_message('password_reset_emailed');
                return true;
            } else {
                $this->set_info(
                    ' <strong>Warning!</strong> New password could not be emailed to user, the generated password is <b>' .
                        $newPassword .
                        '</b>. Please mail it manually to the user',
                );
                return true;
            }
        } else {
            $this->set_error('password_reset_unsucessful');
            return false;
        }
    }

    public function send_email($username, $newpassword)
    {
        $user = $this->get_user($username);

        if (!key_exists('email', $user)) {
            return false;
        }

        $data = [
            'identity' => $username,
            'new_password' => $newpassword,
            'expiration' => $this->ci->config->item('RESET_PASSWORD_EXPIRE_AFTER_HOURS'),
        ];
        $message = $this->ci->load->view(
            $this->ci->config->item('email_templates', 'ion_auth') .
                $this->ci->config->item('email_forgot_password_complete', 'ion_auth'),
            $data,
            true,
        );
        $this->ci->email->clear();
        $config['mailtype'] = $this->ci->config->item('email_type', 'ion_auth');
        $this->ci->email->initialize($config);
        $this->ci->email->set_newline("\r\n");
        $this->ci->email->from($this->email, $this->ci->config->item('site_title', 'ion_auth'));
        $this->ci->email->to($user['email']);
        $this->ci->email->subject($this->ci->config->item('site_title', 'ion_auth') . ' - New Password');
        $this->ci->email->message($message);
        if ($this->ci->email->send()) {
            return true;
        } else {
            return false;
        }
    }
}
