<?php

namespace Mtc\Modules\Members\Classes;

use App\Src\Encryption;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use Mtc\Modules\Members\Models\Member;

/**
 * Authenticator class.
 * Performs member authentication. This is mostly authentication methods moved from the former Member class.
 *
 * @author mtc.
 * @author Aleksey Lavrinenko
 * @version 06.09.2016.
 */
class Authenticator
{
    /**
     * Validates credentials, performs bruteforce checks and logs in a member. If a member object is passed, this will
     * force login of the member without any checks. Otherwise all checks will be performed against the email and
     * password provided.
     *
     * @param Member|string $member A member object or email to identify the account
     * @param null|string $password
     * @return bool
     * @throws AuthenticationException
     */
    public function login($member, $password = null)
    {
        if ($member instanceof Member) {
            if (!$member->exists) {
                throw new \RuntimeException("Member does not exist");
            }

            // Clear any previous member's basket from the session
            unset($_SESSION['basket_id'], $_SESSION['basket']);

            $_SESSION['member_id'] = $member->id;
            $_SESSION['logged_in'] = true;
        } else {
            // expecting en email in this case
            $email = $member;

            if (empty($password)) {
                throw AuthenticationException::wrongUsernameOrPassword();
            }

            try {
                $member = Member::where('email_hash', Encryption::makeHash($email))
                    ->firstOrFail();
            } catch (ModelNotFoundException $e) {
                // email not found
                throw AuthenticationException::wrongUsernameOrPassword();
            }

            if (empty($member->first_login)) {
                self::sendNewWebsiteEmail($member);
                throw new AuthenticationException('Your password has expired. Please check your mailbox for password change instructions.');
            }

            if ($member->failed_login_count >= config('auth.bruteforce_failed_login_threshold') && time() - strtotime($member->last_failed_login) <= config('auth.bruteforce_cooldown_time')){
                $this->saveLoginFailure($email);
                throw new AuthenticationException('You cannot login just now, please try again in ' . config('auth.bruteforce_cooldown_time') . ' seconds');
            }

            if (!$this->matchesPassword($password, $member->password)) {
                if ($member->failed_login_count == (config('auth.bruteforce_failed_login_threshold') - 2)) {
                    $this->saveLoginFailure($email);
                    throw new AuthenticationException('If you enter your details incorrectly again, your account will be suspended from logins for ' . config('auth.bruteforce_cooldown_time') . ' seconds');
                }

                // Bad password
                $this->saveLoginFailure($email);
                throw AuthenticationException::wrongUsernameOrPassword();
            }

            // all checks passed, go on log in

            $member->last_login = date('Y-m-d H:i:s');
            $member->save();

            if (!empty($_SESSION['basket_id'])) {
                $member->setBasketDetails($_SESSION['basket_id']);
            }

            $member->linkAssessmentsFromSession();

            $_SESSION['member_id'] = $member->id;
            $_SESSION['logged_in'] = true;


            if ($member->failed_login_count > 0) {
                $this->resetLoginFailures($member);
            }

            return true;
        }
    }

    /**
     * Logs out current member and cleans up session and some special cookies
     */
    public function logout(): void
    {
        if (session_status() !== PHP_SESSION_ACTIVE) { @session_start(); }
        unset($_SESSION['member_id'], $_SESSION['logged_in'], $_SESSION['basket_id'], $_SESSION['basket']);
        session_regenerate_id(true);
    }

    /**
     * Return currently logged in member if a member is logged in, an empty Member otherwise.
     *
     * @return Member
     */
    public function getLoggedInMember()
    {
        if (!$this->isLoggedIn()) {
            return new Member();
        }

        // just in case there is member left in session that no longer exists, return a new Member also
        $member = Member::findOrNew($_SESSION['member_id']);

        // Trigger event only if member has actually logged in
        if ($member->exists) {
            Event::dispatch(new Events\MemberLoggedInEvent($member));
        }

        return $member;
    }

    /**
     * Returns true if there is a logged in member in the session.
     *
     * @return bool
     */
    public function isLoggedIn()
    {
        return !empty($_SESSION['logged_in']);
    }

    /**
     * Returns an hash for a password.
     *
     * @param $password
     * @return bool|string
     */
    public function hashPassword($password)
    {
        return password_hash($password, PASSWORD_DEFAULT);
    }

    /**
     * Returns true if the password matches the hash, false otherwise.
     *
     * @param $password
     * @param $hash
     * @return bool
     */
    public function matchesPassword($password, $hash)
    {
        return password_verify($password, $hash);
    }

    /**
     * Save to database datetime of last login failure
     * and increment failed login count
     *
     * @param string $email
     * @return bool
     */
    public function saveLoginFailure($email = '')
    {
        if (!$email) {
            return false;
        }

        try {
            $member = Member::withEmail($email)->firstOrFail();
        } catch (ModelNotFoundException $e) {
            // member not found
            return false;
        }
        /* @var $member Member */

        if ((time() - strtotime($member->last_failed_login) <= BRUTEFORCE_FAILED_LOGIN_MAXTIME
                && $member->failed_login_count < config('auth.bruteforce_failed_login_threshold')
            ) || $member->failed_login_count == 0
        ) {
            // increment failed login count and update date
            $member->last_failed_login = Carbon::now();
            $member->failed_login_count += 1;
        } else {
            $member->last_failed_login = Carbon::now();
        }

        return $member->save();
    }

    /**
     * Set failed login count to 0 after successful login
     *
     * @param Member $member
     */
    protected function resetLoginFailures(Member $member)
    {
        $member->failed_login_count = 0;
        $member->save();
    }

    /**
     * Generate and store new password reset hash for a member
     *
     * @param Member $member
     * @return string
     */
    public function initiatePasswordReset(Member $member)
    {
        $member->hash = $this->generateRandomHash();
        $member->save();

        return $member->hash;
    }

    /**
     * Generate a random hash string
     * @return string
     */
    public function generateRandomHash()
    {
        return sha1(microtime());
    }

    /**
     * Matches members by email and reset password hash and returns found member or null
     *
     * @param $email
     * @param $hash
     * @return Member|null
     */
    public function matchMemberByHash($email, $hash, $check_update_time = true)
    {
        $member = Member::withEmail($email)
            ->where('hash', '=', $hash)
            ->first();
        /* @var $member Member */

        if ($member === null) {
            return $member;
        }


        // concider hash created at the updated_at time
        if ($check_update_time && $member->updated_at->diffInMinutes() > 60 && $member->first_login != null) {
            return null;
        }

        return $member;
    }

    /**
     * Sends an email about the new website with password change link
     *
     * @param $member
     * @param null $twig Twig. Pass this if there's bulk email send, so it's not created each tim
     * @return bool
     */
    public static function sendNewWebsiteEmail(Member $member, $twig = null) : bool
    {

        if (empty($twig)) {
            $twig = App::make('twig');
        }

        if (!$member instanceof Member) {
            return false;
        }

        if (empty($member->hash)) {
            $hash = Auth::initiatePasswordReset($member);
        } else {
            $hash = $member->hash;
        }

        $params = [
            'reset_email' => $member->email,
            'reset_code'  => $hash,
        ];

        $content = $twig->render('emails/new_website_email.twig', [
            'member' => $member,
            'url' => route('members-reset-password', $params),
        ]);

        $to = $member->email;
        $subject = config('app.name') . ' Password update';

        email($to, $subject, $content);
        return true;
    }
}
