<?php

namespace Ignite\Core\Repositories;

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

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
     * @throws \Exception
     */
    public function create(array $data)
    {
        $userData = Arr::get($data, 'user', []);
        $participantData = Arr::get($data, 'participant', []);

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

        DB::beginTransaction();

        try {
            /** @var User $user */
            $user = User::forceCreate(array_merge([
                'username' => config('core.use_email_as_username') ? $participantData['email'] : $participantData['username'],
                'password' => isset($participantData['password']) ? bcrypt($participantData['password']) : null,
                'email' => strtolower($participantData['email']),
                'first' => $participantData['first'],
                'last' => $participantData['last'],
                'status' => isset($participantData['status']) ? $participantData['status'] : User::STAT_PENDING,
                'internal' => isset($participantData['internal']) ? $participantData['internal'] : User::TYPE_PARTICIPANT
            ], $userData));

            Arr::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' => isset($participantData['archived']) ? $participantData['archived'] : Participant::UNARCHIVED,
                'internal' => isset($participantData['internal']) ? $participantData['internal'] : User::TYPE_PARTICIPANT
            ], $participantData));

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

            DB::commit();

            return $participant;
        } catch (\Exception $e) {
            DB::rollBack();
            if ($e instanceof \PDOException) {
                throw new \Exception($e->errorInfo[2], 0, $e);
            }
            throw new \Exception($e->getMessage(), 0, $e);
        }
    }

    /**
     * Update an existing participant.
     *
     * @param  int $id
     * @param  array $data
     * @return Participant
     * @throws \Exception
     */
    public function update($id, array $data)
    {
        DB::beginTransaction();

        try {
            /** @var Participant $participant */
            $participant = $this->find($id);
            $userData = Arr::get($data, 'user');
            $participantData = Arr::get($data, 'participant');

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

            if (empty($userData)) {
                if (config('core.use_email_as_username') ) {
                    $userData['username'] = $participantData['email'] ?? $participant->user->email;
                } else {
                    $userData['username'] = $participantData['username'] ?? $participant->user->username;
                }
                $userData['email'] = $participantData['email'] ?? $participant->user->email;
                $userData['first'] = $participantData['first'] ?? $participant->user->first;
                $userData['last'] = $participantData['last'] ?? $participant->user->last;
                $userData['status'] = $participantData['status'] ?? $participant->user->status ?? Participant::STATUS_PENDING;
                $userData['internal'] = $participantData['internal'] ?? $participant->user->internal ?? User::TYPE_PARTICIPANT;
            }

            // Handle the password if it exists.
            if (isset($userData['password']) || isset($data['password'])) {
                $password = Arr::get($userData, 'password', Arr::get($data, 'password', ''));
                if (empty($password)) {
                    Arr::forget($userData, 'password');
                } else {
                    Arr::set($userData, 'password', bcrypt($password));
                }
            }

            $participant->user->update($userData);

            if (! empty($participantData)) {
                Arr::forget($participantData, ['password']);
                $participant->update($participantData);
            }

            DB::commit();

            return $participant;
        } catch (\Exception $e) {
            DB::rollBack();
            if ($e instanceof \PDOException) {
                throw new \Exception($e->errorInfo[2], 0, $e);
            }
            throw new \Exception($e->getMessage(), 0, $e);
        }
    }

    /**
     * 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;
    }
}
