<?php

namespace Ignite\Core\Repositories;

use DomainException;
use Exception;
use Ignite\Core\Events\EnrollmentDeleting;
use Ignite\Core\Events\ParticipantCreated;
use Ignite\Core\Events\ParticipantCreating;
use Ignite\Core\Events\ParticipantUpdated;
use Ignite\Core\Events\ParticipantUpdating;
use Ignite\Core\Services\Participant\Factory;
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\Auth;
use Ignite\Core\Contracts\Entities\Participant as ParticipantContract;
use Ignite\Core\Contracts\Repositories\ParticipantRepository as ParticipantRepositoryContract;
use Ignite\Core\Entities\Participant;
use Ignite\Core\Exceptions\InvalidPasswordException;
use Illuminate\Support\Fluent;
use Throwable;

class ParticipantRepository implements ParticipantRepositoryContract
{
    /**
     * @var bool
     */
    protected $emailForUsername;

    /**
     * @var Factory
     */
    private $participantFactory;

    /**
     * ParticipantRepository constructor.
     *
     * @param Factory $participantFactory
     */
    public function __construct(Factory $participantFactory)
    {
        $this->emailForUsername = config('core.use_email_as_username');
        $this->participantFactory = $participantFactory;
    }

    /**
     * The default query.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function query()
    {
        return Participant::query()->with('user');
    }

    /**
     * The participant types.
     *
     * @return array
     */
    public function getTypes()
    {
        return config('core.participant.types', []);
    }

    /**
     * The participant statuses.
     *
     * @return array
     */
    public function getStatuses()
    {
        return Participant::getStatusList();
    }

    /**
     * Find a participant by id.
     *
     * @param  int $id
     * @param  bool $includeTrashed
     * @return ParticipantContract
     */
    public function find($id, $includeTrashed = false)
    {
        $query = $this->query()->where('user_id', $id);

        if ($includeTrashed) {
            $query->withTrashed();
        }

        return $query->firstOrFail();
    }

    /**
     * Find a participant by email.
     *
     * @param  string $email
     * @return ParticipantContract
     */
    public function findByEmail($email)
    {
        return $this->query()->byEmail($email)->first();
    }

    /**
     * Find the currently authenticated participant.
     *
     * @return ParticipantContract
     */
    public function findAuthenticated()
    {
        return $this->find(auth()->user()->getKey());
    }

    /**
     * Create a new participant.
     *
     * @param  array $data
     * @return ParticipantContract
     * @throws Exception
     */
    public function create(array $data)
    {
        $data = new Fluent($data);

        event(new ParticipantCreating($data));

        $userParticipant = $this->participantFactory->make($data)->save();

        $participant = $userParticipant->participantWithUser();

        event(new ParticipantCreated($participant));

        return $participant;
    }

    /**
     * Update an existing participant.
     *
     * @param  int $id
     * @param  array $data
     * @return ParticipantContract
     * @throws Exception
     */
    public function update($id, array $data)
    {
        $data = new Fluent($data);
        $model = $this->find($id);

        event(new ParticipantUpdating($data, $model));

        $userParticipant = $this->participantFactory->make($data, $model)->save();

        $participant = $userParticipant->participantWithUser();

        event(new ParticipantUpdated($participant));

        return $participant;
    }

    /**
     * Change a participant's password.
     *
     * @param int $id
     * @param string $old
     * @param string $new
     * @return Participant
     * @throws InvalidPasswordException
     */
    public function changePassword($id, $old, $new)
    {
        if ($new == $old) {
            throw InvalidPasswordException::reusedPassword();
        }

        $participant = $this->find($id);
        $username = $this->emailForUsername ? $participant->user->email : $participant->user->username;
        $password = $old;

        if (! Auth::attempt(compact('username', 'password'))) {
            throw InvalidPasswordException::incorrectPreviousPassword();
        }

        $participant->user->password = bcrypt($new);
        $participant->user->password_salt = '';
        $participant->user->save();

        return $participant;
    }

    /**
     * Trash a participant.
     *
     * @param  int $id
     * @return bool
     * @throws Exception
     */
    public function trash($id)
    {
        return (bool) $this->find($id)->delete();
    }

    /**
     * Delete a participant.
     *
     * @param  int $id
     * @return bool
     * @throws DomainException
     * @throws Throwable
     */
    public function delete($id)
    {
        $participant = $this->find($id, true);

        if ($participant->transactions()->count() > 0) {
            throw new DomainException('You cannot delete a participant with existing transactions.');
        }

        $tables = [
            'core_audit' => 'user_id',
            'core_note' => 'by_user_id',
            'core_user_group' => 'user_id',
            'core_participant' => 'user_id',
            'core_user' => 'user_id'
        ];

        try {
            $participant->getConnection()->transaction(
                function (Connection $connection) use ($participant, $tables) {
                    event(new EnrollmentDeleting($participant));
                    foreach ($tables as $table => $column) {
                        $connection->table($table)->where($column, $participant->getKey())->delete();
                    }
                }
            );
        } catch (Throwable $e) {
            throw new DomainException(sprintf(
                'Unable to delete participant with ID: %s. Error: %s',
                $participant->getKey(),
                $e->getMessage()
            ), 0, $e);
        }

        return true;
    }
}
