<?php

namespace Ignite\Core\Repositories;

use Ignite\Core\Events\EnrollmentDeleting;
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\Auth;
use Ignite\Core\Entities\User;
use Ignite\Core\Contracts\Repositories\ParticipantRepository as ParticipantRepositoryInterface;
use Ignite\Core\Entities\Participant;
use Ignite\Core\Exceptions\InvalidPasswordException;
use Illuminate\Support\Facades\DB;

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

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

    /**
     * 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 Participant
     */
    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 Participant
     */
    public function findByEmail($email)
    {
        return $this->query()->byEmail($email)->first();
    }

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

    /**
     * Create a new participant.
     *
     * @param  array $data
     * @return Participant
     */
    public function create(array $data)
    {
        $userData = array_get($data, 'user', []);
        $participantData = array_get($data, 'participant', []);

        if (empty($participantData)) {
            $participantData = $data;
        }

        /** @var User $user */
        $user = User::forceCreate(array_merge([
            'username' => $participantData['email'],
            'password' => '',
            'email' => $participantData['email'],
            'first' => $participantData['first'],
            'last' => $participantData['last'],
            'status' => User::STAT_PENDING,
            'internal' => User::TYPE_PARTICIPANT
        ], $userData));

        array_forget($participantData, 'password');

        /** @var Participant $participant */
        $participant = Participant::forceCreate(array_merge([
            'user_id' => $user->getKey(),
            'email' => $participantData['email'],
            'first' => $participantData['first'],
            'last' => $participantData['last'],
            'status' => Participant::STATUS_PENDING,
            'archived' => 0,
            'internal' => 0
        ], $participantData));

        $participant->setRelation('user', $user);

        return $participant;
    }

    /**
     * Update an existing participant.
     *
     * @param  int $id
     * @param  array $data
     * @return Participant
     */
    public function update($id, array $data)
    {
        /** @var Participant $participant */
        $participant = $this->find($id);
        $userData = array_get($data, 'user');
        $participantData = array_get($data, 'participant');

        if (empty($participantData)) {
            $participantData = $data;
        }

        if (! empty($userData)) {
            $password = array_get($userData, 'password', '');
            if (! empty($password)) {
                array_set($userData, 'password', bcrypt($password));
            } else {
                array_forget($userData, 'password');
            }
            $participant->user->update($userData);
        }

        if (! empty($participantData)) {
            array_forget($participantData, 'password');
            $participant->update($participantData);
        }

        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 new InvalidPasswordException(
                'The new password cannot be the same as the old password.'
            );
        }

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

        if (! Auth::attempt(compact('username', 'password'))) {
            throw new InvalidPasswordException(
                'Old password did not match our records.'
            );
        }

        $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;
    }
}
