<?php

namespace Ignite\Core\Repositories;

use Exception;
use Ignite\Core\Entities\TransactionResource;
use Ignite\Core\Models\Import\Hashers\TransactionHasher;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Ignite\Core\Entities\User;
use Ignite\Core\Entities\Transaction;
use Ignite\Core\Contracts\Repositories\TransactionRepository as TransactionRepositoryInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class TransactionRepository implements TransactionRepositoryInterface
{
    /**
     * @var TransactionHasher
     */
    private $hasher;

    /**
     * TransactionRepository constructor.
     *
     * @param TransactionHasher $hasher
     */
    public function __construct(TransactionHasher $hasher)
    {
        $this->hasher = $hasher;
    }

    /**
     * The allowed transaction types with matching keys and values.
     *
     * @return array
     */
    public function getAllowedTypes()
    {
        $types = array_merge(
            Transaction::query()->newModelInstance()->getTypes(),
            config('core.transaction.types', [])
        );

        return array_combine(array_values($types), array_values($types));
    }

    /**
     * Check if the given value is an allowed transaction type.
     *
     * @param string $type
     *
     * @return bool
     */
    public function isAllowedType($type)
    {
        return in_array($type, $this->getAllowedTypes());
    }

    /**
     * The current user's balance.
     *
     * @param  User|null $user
     * @return mixed
     */
    public function getBalance($user = null)
    {
        $query = $user ? Transaction::byUser($user) : Transaction::byCurrentUser();

        return $query->sum('value');
    }

    /**
     * Find transactions by user.
     *
     * @param  int|User $user
     *
     * @return \Illuminate\Database\Eloquent\Collection|Transaction[]
     */
    public function findByUser($user)
    {
        return Transaction::query()
            ->where('user_id', ($user instanceof User) ? $user->getKey() : $user)
            ->orderBy('created_at', 'desc')
            ->orderBy('id', 'desc')
            ->get();
    }

    /**
     * Find transactions for the authenticated user.
     *
     * @return Transaction[]|\Illuminate\Database\Eloquent\Collection
     */
    public function findAuthenticated()
    {
        return $this->findByUser(auth()->user());
    }

    /**
     * Create a transaction.
     *
     * @param array $data
     *
     * @return Transaction|Model
     * @throws Exception
     */
    public function create(array $data)
    {
        $now = Carbon::now();

        if (! isset($data['value']) || ! is_numeric($data['value'])) {
            throw new Exception("The `value` was missing or non-numeric.");
        }

        if (! isset($data['type']) || empty($data['type'])) {
            throw new Exception("The `type` was missing.");
        }

        if (! $this->isAllowedType($data['type'])) {
            throw new Exception("The `type` value '{$data['type']}' is not allowed.");
        }

        if (! isset($data['description']) || empty($data['description'])) {
            throw new Exception("The `description` was missing.");
        }

        $data['user_id'] = isset($data['user_id'])
            ? $data['user_id']
            : auth()->id();

        if (! isset($data['user_id']) || empty($data['user_id'])) {
            throw new Exception("The `user_id` was missing.");
        }

        $data['related_id'] = isset($data['related_id'])
            ? $data['related_id']
            : 0;

        $data['related_type'] = isset($data['related_type'])
            ? $data['related_type']
            : null;

        $data['related_name'] = isset($data['related_name'])
            ? $data['related_name']
            : null;

        $data['tax_date'] = isset($data['tax_date'])
            ? Carbon::parse($data['tax_date'])
            : $now;

        $data['transaction_date'] = isset($data['transaction_date'])
            ? Carbon::parse($data['transaction_date'])
            : $now;

        $data['hash'] = $this->hasher->hash([
            'identifier' => $data['user_id'],
            'value' => $data['value'],
            'description' => $data['description'],
            'type' => $data['type'],
        ]);

        return Transaction::query()->forceCreate($data);
    }

    /**
     * Create a transaction given a related model.
     *
     * @param array $data
     * @param Model $model
     *
     * @return Transaction
     */
    public function createUsingRelatedModel(array $data, Model $model)
    {
        return $this->create(array_merge([
            'related_id' => $model->getKey(),
            'related_type' => get_class($model),
            'related_name' => Str::of(class_basename($model))->snake()->upper()
        ], $data));
    }

    /**
     * Create a transaction for the given type.
     *
     * @param string $type
     * @param array $data
     * @param Model|null $model
     *
     * @return Transaction|Model
     */
    public function createTransactionOfType($type, array $data, Model $model = null)
    {
        $data = array_merge(compact('type'), $data);

        if (is_null($model)) {
            return $this->create($data);
        }

        return $this->createUsingRelatedModel($data, $model);
    }

    /**
     * Create a transaction of type EARNED.
     *
     * @param array $data
     * @param Model|null $model
     *
     * @return Transaction|Model
     */
    public function createEarnedTransaction(array $data, Model $model = null)
    {
        return $this->createTransactionOfType(Transaction::EARNED, $data, $model);
    }

    /**
     * Create a transaction of type REDEEMED.
     *
     * @param array $data
     * @param Model|null $model
     *
     * @return Transaction|Model
     */
    public function createRedeemTransaction(array $data, Model $model = null)
    {
        return $this->createTransactionOfType(Transaction::REDEEMED, $data, $model);
    }

    /**
     * Create a transaction of type CANCELLED.
     *
     * @param array $data
     * @param Model|null $model
     *
     * @return Transaction|Model
     */
    public function createCancelledTransaction(array $data, Model $model = null)
    {
        return $this->createTransactionOfType(Transaction::CANCELLED, $data, $model);
    }

    /**
     * Create a transaction of type EXPIRED.
     *
     * @param array $data
     * @param Model|null $model
     *
     * @return Transaction|Model
     */
    public function createExpiredTransaction(array $data, Model $model = null)
    {
        return $this->createTransactionOfType(Transaction::EXPIRED, $data, $model);
    }

    /**
     * Delete the records with the given IDs.
     *
     * @param  string|array $ids
     * @return bool
     */
    public function delete($ids)
    {
        DB::beginTransaction();

        try {
            $records = TransactionResource::byIds($ids)->get();

            /** @var TransactionResource $record */
            foreach ($records as $record) {
                $record->delete();
            }
        } catch (\Exception $e) {
            DB::rollBack();
            throw new \DomainException("Unable to delete records with IDs: {$ids} - {$e->getMessage()}");
        }

        DB::commit();

        return true;
    }
}
