<?php

namespace Auth;

use Core\Request;
use Event\AuthEvent;
use Core\Security;

/**
 * RememberMe model
 *
 * @package  auth
 * @author   Frederic Guillot
 */
class RememberMe extends Base
{
    /**
     * Backend name
     *
     * @var string
     */
    const AUTH_NAME = 'RememberMe';

    /**
     * SQL table name
     *
     * @var string
     */
    const TABLE = 'remember_me';

    /**
     * Cookie name
     *
     * @var string
     */
    const COOKIE_NAME = '__R';

    /**
     * Expiration (60 days)
     *
     * @var integer
     */
    const EXPIRATION = 5184000;

    /**
     * Get a remember me record
     *
     * @access public
     * @param $token
     * @param $sequence
     * @return mixed
     */
    public function find($token, $sequence)
    {
        return $this->db
                    ->table(self::TABLE)
                    ->eq('token', $token)
                    ->eq('sequence', $sequence)
                    ->gt('expiration', time())
                    ->findOne();
    }

    /**
     * Get all sessions for a given user
     *
     * @access public
     * @param  integer  $user_id  User id
     * @return array
     */
    public function getAll($user_id)
    {
        return $this->db
                    ->table(self::TABLE)
                    ->eq('user_id', $user_id)
                    ->desc('date_creation')
                    ->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
                    ->findAll();
    }

    /**
     * Authenticate the user with the cookie
     *
     * @access public
     * @return bool
     */
    public function authenticate()
    {
        $credentials = $this->readCookie();

        if ($credentials !== false) {

            $record = $this->find($credentials['token'], $credentials['sequence']);

            if ($record) {

                // Update the sequence
                $this->writeCookie(
                    $record['token'],
                    $this->update($record['token']),
                    $record['expiration']
                );

                // Create the session
                $this->userSession->refresh($this->user->getById($record['user_id']));

                // Do not ask 2FA for remember me session
                $this->session['2fa_validated'] = true;

                $this->container['dispatcher']->dispatch(
                    'auth.success',
                    new AuthEvent(self::AUTH_NAME, $this->userSession->getId())
                );

                return true;
            }
        }

        return false;
    }

    /**
     * Remove a session record
     *
     * @access public
     * @param  integer  $session_id   Session id
     * @return mixed
     */
    public function remove($session_id)
    {
        return $this->db
                    ->table(self::TABLE)
                    ->eq('id', $session_id)
                    ->remove();
    }

    /**
     * Remove the current RememberMe session and the cookie
     *
     * @access public
     * @param  integer  $user_id  User id
     */
    public function destroy($user_id)
    {
        $credentials = $this->readCookie();

        if ($credentials !== false) {

            $this->deleteCookie();

            $this->db
                 ->table(self::TABLE)
                 ->eq('user_id', $user_id)
                 ->eq('token', $credentials['token'])
                 ->remove();
        }
    }

    /**
     * Create a new RememberMe session
     *
     * @access public
     * @param  integer  $user_id     User id
     * @param  string   $ip          IP Address
     * @param  string   $user_agent  User Agent
     * @return array
     */
    public function create($user_id, $ip, $user_agent)
    {
        $token = hash('sha256', $user_id.$user_agent.$ip.Security::generateToken());
        $sequence = Security::generateToken();
        $expiration = time() + self::EXPIRATION;

        $this->cleanup($user_id);

        $this
            ->db
            ->table(self::TABLE)
            ->insert(array(
                'user_id' => $user_id,
                'ip' => $ip,
                'user_agent' => $user_agent,
                'token' => $token,
                'sequence' => $sequence,
                'expiration' => $expiration,
                'date_creation' => time(),
            ));

        return array(
            'token' => $token,
            'sequence' => $sequence,
            'expiration' => $expiration,
        );
    }

    /**
     * Remove old sessions for a given user
     *
     * @access public
     * @param  integer  $user_id  User id
     * @return bool
     */
    public function cleanup($user_id)
    {
        return $this->db
                    ->table(self::TABLE)
                    ->eq('user_id', $user_id)
                    ->lt('expiration', time())
                    ->remove();
    }

    /**
     * Return a new sequence token and update the database
     *
     * @access public
     * @param  string   $token        Session token
     * @return string
     */
    public function update($token)
    {
        $new_sequence = Security::generateToken();

        $this->db
             ->table(self::TABLE)
             ->eq('token', $token)
             ->update(array('sequence' => $new_sequence));

        return $new_sequence;
    }

    /**
     * Encode the cookie
     *
     * @access public
     * @param  string   $token        Session token
     * @param  string   $sequence     Sequence token
     * @return string
     */
    public function encodeCookie($token, $sequence)
    {
        return implode('|', array($token, $sequence));
    }

    /**
     * Decode the value of a cookie
     *
     * @access public
     * @param  string   $value    Raw cookie data
     * @return array
     */
    public function decodeCookie($value)
    {
        list($token, $sequence) = explode('|', $value);

        return array(
            'token' => $token,
            'sequence' => $sequence,
        );
    }

    /**
     * Return true if the current user has a RememberMe cookie
     *
     * @access public
     * @return bool
     */
    public function hasCookie()
    {
        return ! empty($_COOKIE[self::COOKIE_NAME]);
    }

    /**
     * Write and encode the cookie
     *
     * @access public
     * @param  string   $token        Session token
     * @param  string   $sequence     Sequence token
     * @param  string   $expiration   Cookie expiration
     */
    public function writeCookie($token, $sequence, $expiration)
    {
        setcookie(
            self::COOKIE_NAME,
            $this->encodeCookie($token, $sequence),
            $expiration,
            $this->helper->url->dir(),
            null,
            Request::isHTTPS(),
            true
        );
    }

    /**
     * Read and decode the cookie
     *
     * @access public
     * @return mixed
     */
    public function readCookie()
    {
        if (empty($_COOKIE[self::COOKIE_NAME])) {
            return false;
        }

        return $this->decodeCookie($_COOKIE[self::COOKIE_NAME]);
    }

    /**
     * Remove the cookie
     *
     * @access public
     */
    public function deleteCookie()
    {
        setcookie(
            self::COOKIE_NAME,
            '',
            time() - 3600,
            $this->helper->url->dir(),
            null,
            Request::isHTTPS(),
            true
        );
    }
}
