<?php

namespace Ignite\Core\Auth;

use Ignite\Core\Events\ImpersonationStarted;
use Ignite\Core\Events\ImpersonationStopped;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Foundation\Application;
use Ignite\Core\Entities\User;

class Impersonation
{
    public const SESSION_KEY = 'impersonator_id';
    public const PERMISSION_KEY = 'core.user.impersonate';

    /**
     * @var Application
     */
    private $app;

    /**
     * Impersonation constructor.
     *
     * @param Application $application
     */
    public function __construct(Application $application)
    {
        $this->app = $application;
    }

    /**
     * Find a user by id.
     *
     * @param  int $id
     * @return User
     */
    public function findUserById($id)
    {
        return User::query()->where('user_id', $id)->firstOrFail();
    }

    /**
     * Determine whether the logged-in user is currently impersonating another user.
     *
     * @return bool
     */
    public function isImpersonating()
    {
        return $this->app['session.store']->has(static::SESSION_KEY);
    }

    /**
     * Determine whether the logged-in can impersonate the target user.
     *
     * @param  User $source
     * @param  User $target
     * @return bool
     */
    public function canImpersonate($source, $target)
    {
        if ($this->isImpersonating()) {
            return false;
        }

        if ($source->cannot(static::PERMISSION_KEY)) {
            return false;
        }

        $sourceMin = (int) $source->groups->min('level');
        $targetMin = (int) $target->groups->min('level');

        return $sourceMin <= $targetMin;
    }

    /**
     * Determine whether the logged-in cannot impersonate the target user.
     *
     * @param  User $source
     * @param  User $target
     * @return bool
     */
    public function cannotImpersonate($source, $target)
    {
        return ! $this->canImpersonate($source, $target);
    }

    /**
     * Start impersonating another user.
     *
     * @param  User $impersonator
     * @param  User $impersonated
     * @return User
     * @throws AuthenticationException
     */
    public function startImpersonating($impersonator, $impersonated)
    {
        try {
            $this->app['session.store']->put(static::SESSION_KEY, $impersonator->getKey());
            $this->app['auth']->quietLogout();
            $this->app['auth']->quietLogin($impersonated);
            $this->app['authorization']->forgetPermissions()->registerPermissions();
            $this->app['events']->dispatch(new ImpersonationStarted($impersonator, $impersonated));

            return $impersonated;
        } catch (\Exception $e) {
            throw new AuthenticationException('Unable to start impersonating. Error: ' . $e->getMessage());
        }
    }

    /**
     * Stop impersonating another user.
     *
     * @return User
     * @throws AuthenticationException
     */
    public function stopImpersonating()
    {
        try {
            $impersonator = $this->findUserById($this->getImpersonatorId());

            $this->app['auth']->quietLogout();
            $this->app['auth']->quietLogin($impersonator);
            $this->app['session.store']->forget(static::SESSION_KEY);
            $this->app['authorization']->forgetPermissions()->registerPermissions();
            $this->app['events']->dispatch(new ImpersonationStopped($impersonator, $this->getImpersonated()));

            return $impersonator;
        } catch (\Exception $e) {
            throw new AuthenticationException('Unable to stop impersonating. Error: ' . $e->getMessage());
        }
    }

    /**
     * The ID of the user being impersonated.
     *
     * @return User
     */
    public function getImpersonated()
    {
        return $this->app['auth']->user();
    }

    /**
     * The ID of the user being impersonated.
     *
     * @return int
     */
    public function getImpersonatedId()
    {
        return $this->getImpersonated()->getKey();
    }

    /**
     * The ID of the currently user impersonating.
     *
     * @return int
     */
    public function getImpersonatorId()
    {
        return $this->app['session.store']->get(static::SESSION_KEY);
    }
}
