<?php

namespace Ignite\Core\Services\Participant\Strategy;

use Ignite\Core\Contracts\Participant\FactoryStrategy;
use Ignite\Core\Entities\Participant;
use Ignite\Core\Entities\User;
use Ignite\Core\Models\Data\UserParticipant;
use Illuminate\Config\Repository;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Fluent;

class DefaultStrategy implements FactoryStrategy
{
    /**
     * @var Repository
     */
    protected $config;

    /**
     * Flag to determine whether the system should use the participant's email as their login username.
     *
     * @var bool
     */
    protected $usernameIsEmail = true;

    /**
     * DefaultStrategy constructor.
     *
     * @param Repository $config
     */
    public function __construct(Repository $config)
    {
        $this->config = $config;
        $this->usernameIsEmail = $this->config->get('core.use_email_as_username');
    }

    /**
     * @inheritdoc
     */
    public function make(Fluent $data, Model $model = null)
    {
        if ($model && ! $model->relationLoaded('user')) {
            $model->load('user');
        }

        $user = $this->buildUser($this->prepareUserData($data), $model ? $model->getRelation('user') : null);

        $participant = $this->buildParticipant($this->prepareParticipantData($data), $user, $model);

        // The UserParticipant class doesn't know whether the models exist in the database
        // since we created new objects from existing objects, so when a model is passed
        // into this method, we need to update our models to indicate their existence.
        if ($model) {
            $user->exists = $participant->exists = true;
        }

        return new UserParticipant($user, $participant);
    }

    /**
     * Prepare the user data.
     *
     * @param Fluent $data
     *
     * @return Fluent
     */
    protected function prepareUserData(Fluent $data)
    {
        return $this->prepareFluentData($data, 'user');
    }

    /**
     * Prepare the participant data.
     *
     * @param Fluent $data
     *
     * @return Fluent
     */
    protected function prepareParticipantData(Fluent $data)
    {
        return $this->prepareFluentData($data, 'participant');
    }

    /**
     * Prepare the data for the given key.
     *
     * @param Fluent $data
     * @param string $key
     *
     * @return Fluent
     */
    protected function prepareFluentData(Fluent $data, $key)
    {
        if (! $data->offsetExists($key)) {
            return $data;
        }

        $values = $data->get($key, []);

        if (empty($values)) {
            return $data;
        }

        return new Fluent($values);
    }

    /**
     * Build a user model from the given data.
     *
     * @param Fluent $data
     * @param Model|null $model
     *
     * @return User
     */
    protected function buildUser(Fluent $data, ?Model $model = null)
    {
        $data = [
            'user_id' => $this->getUserId($data, null, $model),
            'username' => $this->getUsername($data, $model),
            'password' => $this->getPassword($data, $model),
            'email' => $this->getEmail($data, $model),
            'first' => $this->getFirstName($data, $model),
            'last' => $this->getLastName($data, $model),
            'status' => $this->getUserStatus($data, $model),
            'internal' => $this->getUserType($data, $model),
        ];

        return resolve(User::class, ['attributes' => $data]);
    }

    /**
     * Build a participant model from the given data.
     *
     * @param Fluent $data
     * @param User $user
     * @param Model|null $model
     *
     * @return Participant
     */
    protected function buildParticipant(Fluent $data, User $user, ?Model $model = null)
    {
        $data->offsetUnset('password');
        $data->offsetUnset('password_salt');
        $data->offsetUnset('remember_token');
        $data->offsetUnset('last_login_at');

        $data = array_merge(
            $data->toArray(),
            [
                'user_id' => $this->getUserId($data, $user, $model),
                'email' => $this->getEmail($data, $model),
                'first' => $this->getFirstName($data, $model),
                'last' => $this->getLastName($data, $model),
                'status' => $this->getParticipantStatus($data, $model),
                'internal' => $this->getUserType($data, $model),
                'archived' => $this->getParticipantArchived($data, $model)
            ]
        );

        return resolve(Participant::class, ['attributes' => $data]);
    }

    /**
     * The user's username for login.
     *
     * @param Fluent $data
     * @param Model|null $model
     * @param mixed $default
     *
     * @return string
     */
    protected function getUsername(Fluent $data, ?Model $model = null, $default = null)
    {
        if ($this->usernameIsEmail) {
            return $this->getEmail($data, $model, $default);
        }

        $key = 'username';

        return $data->get($key, $this->getAttributeFromModel($model, $key, $default));
    }

    /**
     * The user's password for login.
     *
     * @param Fluent $data
     * @param Model|null $model
     * @param mixed $default
     *
     * @return string
     */
    protected function getPassword(Fluent $data, ?Model $model = null, $default = null)
    {
        $password = $data->get('password');

        if (empty($password)) {
            return $this->getAttributeFromModel($model, 'password', $default);
        }

        return bcrypt($password);
    }

    /**
     * The user's id.
     *
     * @param Fluent $data
     * @param User|null $user
     * @param Model|null $model
     *
     * @return string
     */
    protected function getUserId(Fluent $data, ?User $user = null, ?Model $model = null)
    {
        $key = 'user_id';

        if ($model) {
            return $this->getAttributeFromModel($model, $key, null);
        }

        if (! $user || ! $user->exists) {
            return $data->get($key, null);
        }

        return $user->getKey();
    }

    /**
     * The email address.
     *
     * @param Fluent $data
     * @param Model|null $model
     * @param mixed $default
     *
     * @return string
     */
    protected function getEmail(Fluent $data, $model = null, $default = null)
    {
        return mb_strtolower($data->get($key = 'email', $this->getAttributeFromModel($model, $key, $default)));
    }

    /**
     * The first name.
     *
     * @param Fluent $data
     * @param Model|null $model
     * @param mixed $default
     *
     * @return string
     */
    protected function getFirstName(Fluent $data, $model = null, $default = null)
    {
        return $data->get($key = 'first', $this->getAttributeFromModel($model, $key, $default));
    }

    /**
     * The last name.
     *
     * @param Fluent $data
     * @param Model|null $model
     * @param mixed $default
     *
     * @return string
     */
    protected function getLastName(Fluent $data, $model = null, $default = null)
    {
        return $data->get($key = 'last', $this->getAttributeFromModel($model, $key, $default));
    }

    /**
     * The user status.
     *
     * @param Fluent $data
     * @param Model|null $model
     * @param mixed $default
     *
     * @return string
     */
    protected function getUserStatus(Fluent $data, $model = null, $default = null)
    {
        if (is_null($default)) {
            $default = config('core.participant.enrollment.default_status', User::STAT_PENDING);
        }

        return $data->get($key = 'status', $this->getAttributeFromModel($model, $key, $default));
    }

    /**
     * The participant status.
     *
     * @param Fluent $data
     * @param Model|null $model
     * @param mixed $default
     *
     * @return string
     */
    protected function getParticipantStatus(Fluent $data, $model = null, $default = null)
    {
        if (is_null($default)) {
            $default = config('core.participant.enrollment.default_status', Participant::STATUS_PENDING);
        }

        return $data->get($key = 'status', $this->getAttributeFromModel($model, $key, $default));
    }

    /**
     * The user type.
     *
     * @param Fluent $data
     * @param Model|null $model
     * @param mixed $default
     *
     * @return string
     */
    protected function getUserType(Fluent $data, $model = null, $default = User::TYPE_PARTICIPANT)
    {
        return $data->get($key = 'internal', $this->getAttributeFromModel($model, $key, $default));
    }

    /**
     * The participant archived flag.
     *
     * @param Fluent $data
     * @param Model|null $model
     * @param mixed $default
     *
     * @return string
     */
    protected function getParticipantArchived(Fluent $data, $model = null, $default = Participant::UNARCHIVED)
    {
        return $data->get($key = 'archived', $this->getAttributeFromModel($model, $key, $default));
    }

    /**
     * Get the attribute value from the user model.
     *
     * @param Model $model
     * @param string $key
     * @param mixed $default
     *
     * @return mixed
     */
    protected function getAttributeFromModel($model, $key, $default = null)
    {
        if (! $model) {
            return $default;
        }

        $value = $model->getAttribute($key);

        if (empty($value)) {
            return $default;
        }

        return $value;
    }
}
