<?php

use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;

defined('BASEPATH') or exit('No direct script access allowed');

class Authentication_model extends App_Model
{
    public function __construct()
    {
        parent::__construct();
        $this->load->model('user_autologin');
        $this->autologin();
    }

    /**
     * @param  string Email address for login
     * @param  string User Password
     * @param  bool Set cookies for user if remember me is checked
     * @param  bool Is Staff Or Client
     * @param mixed $email
     * @param mixed $password
     * @param mixed $remember
     * @param mixed $staff
     *
     * @return bool if not redirect url found, if found redirect to the url
     */
    public function login($email, $password, $remember, $staff)
    {
        if ((! empty($email)) and (! empty($password))) {
            $table = db_prefix() . 'contacts';
            $_id   = 'id';
            if ($staff == true) {
                $table = db_prefix() . 'staff';
                $_id   = 'staffid';
            }
            $this->db->where('email', $email);
            $user = $this->db->get($table)->row();
            if ($user) {
                // Email is okey lets check the password now
                if (! $user->password || ! app_hasher()->CheckPassword($password, $user->password)) {
                    hooks()->do_action('failed_login_attempt', [
                        'user'            => $user,
                        'is_staff_member' => $staff,
                    ]);

                    log_activity('Failed Login Attempt [Email: ' . $email . ', Is Staff Member: ' . ($staff == true ? 'Yes' : 'No') . ', IP: ' . $this->input->ip_address() . ']');

                    // Password failed, return
                    return false;
                }
            } else {
                hooks()->do_action('non_existent_user_login_attempt', [
                    'email'           => $email,
                    'is_staff_member' => $staff,
                ]);

                log_activity('Non Existing User Tried to Login [Email: ' . $email . ', Is Staff Member: ' . ($staff == true ? 'Yes' : 'No') . ', IP: ' . $this->input->ip_address() . ']');

                return false;
            }

            if ($user->active == 0) {
                hooks()->do_action('inactive_user_login_attempt', [
                    'user'            => $user,
                    'is_staff_member' => $staff,
                ]);
                log_activity('Inactive User Tried to Login [Email: ' . $email . ', Is Staff Member: ' . ($staff == true ? 'Yes' : 'No') . ', IP: ' . $this->input->ip_address() . ']');

                return [
                    'memberinactive' => true,
                ];
            }

            $twoFactorAuth = false;
            if ($staff == true) {
                $twoFactorAuth = $user->two_factor_auth_enabled == 0 ? false : true;

                if (! $twoFactorAuth) {
                    hooks()->do_action('before_staff_login', [
                        'email'  => $email,
                        'userid' => $user->{$_id},
                    ]);

                    $user_data = [
                        'staff_user_id'   => $user->{$_id},
                        'staff_logged_in' => true,
                    ];
                } else {
                    $user_data                = [];
                    $user_data['tfa_staffid'] = $user->staffid;
                    if ($remember) {
                        $user_data['tfa_remember'] = true;
                    }
                }
            } else {
                hooks()->do_action('before_client_login', [
                    'email'           => $email,
                    'userid'          => $user->userid,
                    'contact_user_id' => $user->{$_id},
                ]);

                $user_data = [
                    'client_user_id'   => $user->userid,
                    'contact_user_id'  => $user->{$_id},
                    'client_logged_in' => true,
                ];
            }
            $this->session->set_userdata($user_data);

            if (! $twoFactorAuth) {
                if ($remember) {
                    $this->create_autologin($user->{$_id}, $staff);
                }

                $this->update_login_info($user->{$_id}, $staff);
            } else {
                return ['two_factor_auth' => true, 'user' => $user];
            }

            return true;
        }

        return false;
    }

    /**
     * @param  bool If Client or Staff
     * @param mixed $staff
     *
     * @return none
     */
    public function logout($staff = true)
    {
        $this->delete_autologin($staff);

        if (is_client_logged_in()) {
            hooks()->do_action('before_contact_logout', get_client_user_id());

            $this->session->unset_userdata('client_user_id');
            $this->session->unset_userdata('client_logged_in');
        } else {
            hooks()->do_action('before_staff_logout', get_staff_user_id());

            $this->session->unset_userdata('staff_user_id');
            $this->session->unset_userdata('staff_logged_in');
        }

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

    /**
     * @param  int ID to create autologin
     * @param  bool Is Client or Staff
     * @param mixed $user_id
     * @param mixed $staff
     *
     * @return bool
     */
    private function create_autologin($user_id, $staff)
    {
        $this->load->helper('cookie');
        $key = bin2hex(random_bytes(32));
        $this->user_autologin->delete($user_id, $key, $staff);

        if ($this->user_autologin->set($user_id, hash('sha256', $key), $staff)) {
            set_cookie([
                'name'  => 'autologin',
                'value' => json_encode([
                    'user_id' => $user_id,
                    'key'     => $key,
                ]),
                'expire' => 60 * 60 * 24 * 31 * 2, // 2 months
            ]);

            return true;
        }

        return false;
    }

    /**
     * @param  bool Is Client or Staff
     * @param mixed $staff
     *
     * @return none
     */
    private function delete_autologin($staff)
    {
        $this->load->helper('cookie');
        if ($cookie = get_cookie('autologin', true)) {
            $data = json_decode($cookie, true);

            // Validate decoded data
            if (! is_array($data) || ! isset($data['user_id']) || ! isset($data['key'])) {
                delete_cookie('autologin', 'aal');

                return;
            }

            // Validate data types
            if (! is_numeric($data['user_id']) || ! is_string($data['key'])) {
                delete_cookie('autologin', 'aal');

                return;
            }

            $this->user_autologin->delete($data['user_id'], hash('sha256', $data['key']), $staff);
            delete_cookie('autologin', 'aal');
        }
    }

    /**
     * @return bool
     *              Check if autologin found
     */
    public function autologin()
    {
        if (! is_logged_in()) {
            $this->load->helper('cookie');
            if ($cookie = get_cookie('autologin', true)) {
                $data = json_decode($cookie, true);

                // Validate decoded data is an array and has required keys
                if (! is_array($data)) {
                    delete_cookie('autologin', 'aal');

                    return false;
                }

                if (isset($data['key']) and isset($data['user_id'])) {
                    // Validate data types
                    if (! is_numeric($data['user_id']) || ! is_string($data['key'])) {
                        delete_cookie('autologin', 'aal');

                        return false;
                    }

                    if (! is_null($user = $this->user_autologin->get($data['user_id'], hash('sha256', $data['key'])))) {
                        // Login user
                        if ($user->staff == 1) {
                            $user_data = [
                                'staff_user_id'   => $user->id,
                                'staff_logged_in' => true,
                            ];
                        } else {
                            // Get the customer id
                            $this->db->select('userid');
                            $this->db->where('id', $user->id);
                            $contact = $this->db->get(db_prefix() . 'contacts')->row();

                            $user_data = [
                                'client_user_id'   => $contact->userid,
                                'contact_user_id'  => $user->id,
                                'client_logged_in' => true,
                            ];
                        }
                        $this->session->set_userdata($user_data);
                        // Renew users cookie to prevent it from expiring
                        set_cookie([
                            'name'   => 'autologin',
                            'value'  => $cookie,
                            'expire' => 60 * 60 * 24 * 31 * 2, // 2 months
                        ]);
                        $this->update_login_info($user->id, $user->staff);

                        return true;
                    }
                }
            }
        }

        return false;
    }

    /**
     * @param  int ID
     * @param  bool Is Client or Staff
     * @param mixed $user_id
     * @param mixed $staff
     *
     * @return none
     *              Update login info on autologin
     */
    private function update_login_info($user_id, $staff)
    {
        $table = db_prefix() . 'contacts';
        $_id   = 'id';
        if ($staff == true) {
            $table = db_prefix() . 'staff';
            $_id   = 'staffid';
        }
        $this->db->set('last_ip', $this->input->ip_address());
        $this->db->set('last_login', date('Y-m-d H:i:s'));
        $this->db->where($_id, $user_id);
        $this->db->update($table);

        log_activity('User Successfully Logged In [User Id: ' . $user_id . ', Is Staff Member: ' . ($staff == true ? 'Yes' : 'No') . ', IP: ' . $this->input->ip_address() . ']');
    }

    /**
     * Send set password email for contacts
     *
     * @param string $email
     */
    public function set_password_email($email)
    {
        $this->db->where('email', $email);
        $user = $this->db->get(db_prefix() . 'contacts')->row();

        if ($user) {
            if ($user->active == 0) {
                return [
                    'memberinactive' => true,
                ];
            }

            $new_pass_key = app_generate_hash();
            $this->db->where('id', $user->id);
            $this->db->update(db_prefix() . 'contacts', [
                'new_pass_key'           => $new_pass_key,
                'new_pass_key_requested' => date('Y-m-d H:i:s'),
            ]);
            if ($this->db->affected_rows() > 0) {
                $data['new_pass_key'] = $new_pass_key;
                $data['userid']       = $user->id;
                $data['email']        = $email;

                $sent = send_mail_template('customer_contact_set_password', $user, $data);

                if ($sent) {
                    hooks()->do_action('set_password_email_sent', ['is_staff_member' => false, 'user' => $user]);

                    return true;
                }

                return false;
            }

            return false;
        }

        return false;
    }

    /**
     * @param  string Email from the user
     * @param  Is Client or Staff
     * @param mixed $email
     * @param mixed $staff
     *
     * @return bool
     *              Generate new password key for the user to reset the password.
     */
    public function forgot_password($email, $staff = false)
    {
        $table = db_prefix() . 'contacts';
        $_id   = 'id';
        if ($staff == true) {
            $table = db_prefix() . 'staff';
            $_id   = 'staffid';
        }
        $this->db->where('email', $email);
        $user = $this->db->get($table)->row();

        if ($user) {
            if ($user->active == 0) {
                log_activity('Inactive User Tried Password Reset [Email: ' . $email . ', Is Staff Member: ' . ($staff == true ? 'Yes' : 'No') . ', IP: ' . $this->input->ip_address() . ']');

                return [
                    'memberinactive' => true,
                ];
            }

            $new_pass_key = app_generate_hash();
            $this->db->where($_id, $user->{$_id});
            $this->db->update($table, [
                'new_pass_key'           => $new_pass_key,
                'new_pass_key_requested' => date('Y-m-d H:i:s'),
            ]);

            if ($this->db->affected_rows() > 0) {
                $data['new_pass_key'] = $new_pass_key;
                $data['staff']        = $staff;
                $data['userid']       = $user->{$_id};
                $merge_fields         = [];

                if ($staff == false) {
                    $sent = send_mail_template('customer_contact_forgot_password', $user->email, $user->userid, $user->{$_id}, $data);
                } else {
                    $sent = send_mail_template('staff_forgot_password', $user->email, $user->{$_id}, $data);
                }

                if ($sent) {
                    log_activity('Password Reset Email sent [Email: ' . $email . ', Is Staff Member: ' . ($staff == true ? 'Yes' : 'No') . ', IP: ' . $this->input->ip_address() . ']');

                    hooks()->do_action('forgot_password_email_sent', ['is_staff_member' => $staff, 'user' => $user]);

                    return true;
                }

                return false;
            }

            return false;
        }
        log_activity('Non Existing User Tried Password Reset [Email: ' . $email . ', Is Staff Member: ' . ($staff == true ? 'Yes' : 'No') . ', IP: ' . $this->input->ip_address() . ']');

        return false;
    }

    /**
     * Update user password from forgot password feature or set password
     *
     * @param bool   $staff        is staff or contact
     * @param mixed  $userid
     * @param string $new_pass_key the password generate key
     * @param string $password     new password
     */
    public function set_password($staff, $userid, $new_pass_key, $password)
    {
        if (! $this->can_set_password($staff, $userid, $new_pass_key)) {
            return [
                'expired' => true,
            ];
        }

        $password = app_hash_password($password);
        $table    = db_prefix() . 'contacts';
        $_id      = 'id';

        if ($staff == true) {
            $table = db_prefix() . 'staff';
            $_id   = 'staffid';
        }

        $this->db->where($_id, $userid);
        $this->db->where('new_pass_key', $new_pass_key);
        $this->db->update($table, [
            'password' => $password,
        ]);

        if ($this->db->affected_rows() > 0) {
            log_activity('User Set Password [User ID: ' . $userid . ', Is Staff Member: ' . ($staff == true ? 'Yes' : 'No') . ', IP: ' . $this->input->ip_address() . ']');
            $this->db->set('new_pass_key', null);
            $this->db->set('new_pass_key_requested', null);
            $this->db->set('last_password_change', date('Y-m-d H:i:s'));
            $this->db->where($_id, $userid);
            $this->db->where('new_pass_key', $new_pass_key);
            $this->db->update($table);

            return true;
        }

        return null;
    }

    /**
     * @param  bool Is Client or Staff
     * @param  int ID
     * @param  string
     * @param  string
     * @param mixed $staff
     * @param mixed $userid
     * @param mixed $new_pass_key
     * @param mixed $password
     *
     * @return bool
     *              User reset password after successful validation of the key
     */
    public function reset_password($staff, $userid, $new_pass_key, $password)
    {
        if (! $this->can_reset_password($staff, $userid, $new_pass_key)) {
            return [
                'expired' => true,
            ];
        }
        $password = app_hash_password($password);
        $table    = db_prefix() . 'contacts';
        $_id      = 'id';
        if ($staff == true) {
            $table = db_prefix() . 'staff';
            $_id   = 'staffid';
        }

        $this->db->where($_id, $userid);
        $this->db->where('new_pass_key', $new_pass_key);
        $this->db->update($table, [
            'password' => $password,
        ]);
        if ($this->db->affected_rows() > 0) {
            log_activity('User Reseted Password [User ID: ' . $userid . ', Is Staff Member: ' . ($staff == true ? 'Yes' : 'No') . ', IP: ' . $this->input->ip_address() . ']');
            $this->db->set('new_pass_key', null);
            $this->db->set('new_pass_key_requested', null);
            $this->db->set('last_password_change', date('Y-m-d H:i:s'));
            $this->db->where($_id, $userid);
            $this->db->where('new_pass_key', $new_pass_key);
            $this->db->update($table);
            $this->db->where($_id, $userid);
            $user = $this->db->get($table)->row();

            if ($staff == false) {
                $sent = send_mail_template('customer_contact_password_resetted', $user->email, $user->userid, $user->{$_id});
            } else {
                $sent = send_mail_template('staff_password_resetted', $user->email, $user->{$_id});
            }

            if ($sent) {
                return true;
            }
        }

        return null;
    }

    /**
     * @param  int Is Client or Staff
     * @param  int ID
     * @param  string Password reset key
     * @param mixed $staff
     * @param mixed $userid
     * @param mixed $new_pass_key
     *
     * @return bool
     *              Check if the key is not expired or not exists in database
     */
    public function can_reset_password($staff, $userid, $new_pass_key)
    {
        $table = db_prefix() . 'contacts';
        $_id   = 'id';
        if ($staff == true) {
            $table = db_prefix() . 'staff';
            $_id   = 'staffid';
        }

        $this->db->where($_id, $userid);
        $this->db->where('new_pass_key', $new_pass_key);
        $user = $this->db->get($table)->row();

        if ($user) {
            $timestamp_now_minus_1_hour = time() - (60 * 60);
            $new_pass_key_requested     = strtotime($user->new_pass_key_requested);

            return ! ($timestamp_now_minus_1_hour > $new_pass_key_requested);
        }

        return false;
    }

    /**
     * @param  int Is Client or Staff
     * @param  int ID
     * @param  string Password reset key
     * @param mixed $staff
     * @param mixed $userid
     * @param mixed $new_pass_key
     *
     * @return bool
     *              Check if the key is not expired or not exists in database
     */
    public function can_set_password($staff, $userid, $new_pass_key)
    {
        $table = db_prefix() . 'contacts';
        $_id   = 'id';
        if ($staff == true) {
            $table = db_prefix() . 'staff';
            $_id   = 'staffid';
        }
        $this->db->where($_id, $userid);
        $this->db->where('new_pass_key', $new_pass_key);
        $user = $this->db->get($table)->row();
        if ($user) {
            $timestamp_now_minus_48_hour = time() - (3600 * 48);
            $new_pass_key_requested      = strtotime($user->new_pass_key_requested);

            return ! ($timestamp_now_minus_48_hour > $new_pass_key_requested);
        }

        return false;
    }

    /**
     * Get user from database by 2 factor authentication code
     *
     * @param string $code authentication code to search for
     *
     * @return object
     */
    public function get_user_by_two_factor_auth_code($code)
    {
        $this->db->where('two_factor_auth_code', $code);

        return $this->db->get(db_prefix() . 'staff')->row();
    }

    /**
     * Login user via two factor authentication
     *
     * @param object $user user object
     *
     * @return bool
     */
    public function two_factor_auth_login($user)
    {
        hooks()->do_action('before_staff_login', [
            'email'  => $user->email,
            'userid' => $user->staffid,
        ]);

        $this->session->set_userdata(
            [
                'staff_user_id'   => $user->staffid,
                'staff_logged_in' => true,
            ]
        );

        $remember = null;
        if ($this->session->has_userdata('tfa_remember')) {
            $remember = true;
            $this->session->unset_userdata('tfa_remember');
        }

        if ($remember) {
            $this->create_autologin($user->staffid, true);
        }

        $this->update_login_info($user->staffid, true);

        return true;
    }

    /**
     * Check if 2 factor authentication code sent to email is valid for usage
     *
     * @param string $code  auth code
     * @param string $email email of staff login in
     *
     * @return bool
     */
    public function is_two_factor_code_valid($code, $email)
    {
        $this->db->select('two_factor_auth_code_requested');
        $this->db->where('two_factor_auth_code', $code);
        $this->db->where('email', $email);
        $user = $this->db->get(db_prefix() . 'staff')->row();

        // Code not exists because no user is found
        if (! $user) {
            return false;
        }

        $timestamp_minus_1_hour = time() - (60 * 60);
        $new_code_key_requested = strtotime($user->two_factor_auth_code_requested);

        // The code is older then 1 hour and its not valid
        return ! ($timestamp_minus_1_hour > $new_code_key_requested);
        // Code is valid
    }

    /**
     * Clears 2 factor authentication code in database
     *
     * @param mixed $id
     *
     * @return bool
     */
    public function clear_two_factor_auth_code($id)
    {
        $this->db->where('staffid', $id);
        $this->db->update(db_prefix() . 'staff', [
            'two_factor_auth_code' => null,
        ]);

        return true;
    }

    /**
     * Set 2 factor authentication code for staff member
     *
     * @param mixed $id staff id
     */
    public function set_two_factor_auth_code($id)
    {
        $code = generate_two_factor_auth_key();
        $code .= $id;

        $this->db->where('staffid', $id);
        $this->db->update(db_prefix() . 'staff', [
            'two_factor_auth_code'           => $code,
            'two_factor_auth_code_requested' => date('Y-m-d H:i:s'),
        ]);

        return $code;
    }

    public function get_qr($System_name)
    {
        $staff     = get_staff(get_staff_user_id());
        $google2fa = new PragmaRX\Google2FA\Google2FA();
        $secret    = $google2fa->generateSecretKey();
        $g2faUrl   = $google2fa->getQRCodeUrl($System_name, $staff->email, $secret);
        $writer    = new Writer(
            new ImageRenderer(
                new RendererStyle(200),
                new ImagickImageBackEnd()
            )
        );

        return ['qrURL' => base64_encode($writer->writeString($g2faUrl)), 'secret' => $secret];
    }

    public function set_google_two_factor($secret)
    {
        $id     = get_staff_user_id();
        $secret = $this->encrypt($secret);

        $this->db->where('staffid', $id);
        $success = $this->db->update(db_prefix() . 'staff', [
            'two_factor_auth_enabled' => 2,
            'google_auth_secret'      => $secret,
        ]);

        return (bool) ($success);
    }

    public function is_google_two_factor_code_valid($code, $secret = null)
    {
        $g = new PragmaRX\Google2FA\Google2FA();

        if (! is_null($secret)) {
            return $g->verifyKey($secret, $code, 0);
        }

        $staffid = $this->session->userdata('tfa_staffid');

        $this->db->select('google_auth_secret')
            ->where('staffid', $staffid);

        if ($staff = $this->db->get('staff')->row()) {
            return $g->verifyKey(
                $this->decrypt($staff->google_auth_secret),
                $code,
                0
            );
        }

        return false;
    }

    public function encrypt($string)
    {
        $this->load->library('encryption');

        return $this->encryption->encrypt($string);
    }

    public function decrypt($string)
    {
        $this->load->library('encryption');

        return $this->encryption->decrypt($string);
    }
}
