<?php

namespace Ignite\Core\Services\Importers;

use DomainException;
use Ignite\Core\Contracts\Importable;
use Ignite\Core\Entities\Transaction;
use Ignite\Core\Entities\TransactionResource;
use Ignite\Core\Models\Import\Importer;
use Ignite\Core\Models\Import\Hashers\TransactionHasher;
use Ignite\Core\Repositories\TransactionRepository;
use Illuminate\Support\Arr;
use Illuminate\Validation\Validator;
use League\Csv\Exception;

class Transactions extends Importer implements Importable
{
    const USER_COLUMN = 'user_id';
    const TYPE_COLUMN = 'type';
    const VALUE_COLUMN = 'value';

    /**
     * The required data that must be available in the file.
     *
     * @var array
     */
    protected $validation = [
        'user_id'          => 'required',
        'type'             => 'required',
        'description'      => 'required',
        'value'            => 'required|numeric',
        'transaction_date' => 'date',
        'tax_date'         => 'date',
    ];

    /**
     * The list of "known" fields.
     *
     * @var array
     */
    protected $fields = [
        'user_id' => ['email', 'account', 'user', 'employee_id'],
        'type' => [],
        'description' => [],
        'value' => ['amount', 'balance', ''],
        'transaction_date' => ['date', 'earned_at', 'submitted at'],
        'tax_date' => ['tax', 'taxed_at'],
        'related_id' => [],
        'related_name' => [],
        'related_type' => [],
        'notes' => [],
        'created_at' => [],
        'updated_at' => [],
        'hash' => []
    ];

    /**
     * @var TransactionRepository
     */
    protected $transactionRepository;

    /**
     * Construct an instance of the Transaction importer.
     *
     * @param TransactionRepository $transactionRepository
     */
    public function __construct(TransactionRepository $transactionRepository)
    {
        $this->transactionRepository = $transactionRepository;
    }

    /**
     * Return the column header in the csv file to use in order to identify the user.
     *
     * @return string
     */
    protected function getUserIdentifier()
    {
        return 'email';
    }

    /**
     * Prepare any dependencies for each iteration of the import process.
     *
     * @return void
     * @throws Exception
     */
    public function prepare()
    {
        $this->cacheUsers(static::USER_COLUMN, $this->getUserIdentifier());
        $this->record->deleteLog();
    }

    /**
     * Validate a single line.
     *
     * @param  array $data
     * @return Validator
     */
    public function validate(array $data) : Validator
    {
        return $this->createValidatorInstance($data, $this->validation);
    }

    /**
     * Transform the data for the given line.
     *
     * @param  array $line
     * @return array
     */
    public function transform(array $line) : array
    {
        $line[$this->getUserIdentifier()] = $line[static::USER_COLUMN] ?? null;
        $line[static::USER_COLUMN] = $this->transformUser($line, static::USER_COLUMN);
        $line[static::TYPE_COLUMN] = $this->transformType($line);
        $line[static::VALUE_COLUMN] = $this->transformValue($line);
        $line['transaction_date'] = $this->transformDate($line, 'transaction_date');
        $line['tax_date'] = $this->transformDate($line, 'tax_date');

        return $line;
    }

    /**
     * Add a success message to the log file.
     *
     * @param  int   $line
     * @param  array $data
     * @return string
     */
    public function formatImportMessage(int $line, array $data) : string
    {
        if (! isset($data[static::USER_COLUMN]) || empty($data[static::USER_COLUMN])) {
            return sprintf(
                'Rejected line `%s`: Unable to import transaction for an un-enrolled participant `%s`. ' .
                'The record has been saved to an intermediate table waiting for the participant to enroll.',
                $line,
                $data[$this->getUserIdentifier()]
            );
        }

        return sprintf(
            'Imported line `%s` with %s `%s` for participant identified by %s `%s` and %s `%s` with transaction %s `%s`',
            $line,
            static::TYPE_COLUMN,
            Arr::get($data, static::TYPE_COLUMN, 'Missing'),
            static::USER_COLUMN,
            Arr::get($data, static::USER_COLUMN, 'Missing'),
            $this->getUserIdentifier(),
            Arr::get($data, $this->getUserIdentifier(), 'Missing'),
            static::VALUE_COLUMN,
            Arr::get($data, static::VALUE_COLUMN, 'Missing')
        );
    }

    /**
     * Add an error message to the log file.
     *
     * @param  int        $line
     * @param  array      $data
     * @param  \Throwable $error
     * @return string
     */
    public function formatRejectMessage(int $line, array $data, \Throwable $error) : string
    {
        return sprintf(
            'Rejected line `%s` with %s `%s` for participant identified by %s `%s` with transaction %s `%s` - Error: %s',
            $line,
            static::TYPE_COLUMN,
            Arr::get($data, static::TYPE_COLUMN, 'Missing'),
            $this->getUserIdentifier(),
            Arr::get($data, static::USER_COLUMN, 'Missing'),
            static::VALUE_COLUMN,
            Arr::get($data, static::VALUE_COLUMN, 'Missing'),
            $error->getMessage()
        );
    }

    /**
     * Process the data for a single line.
     *
     * @param  array $data
     * @return bool
     */
    public function save(array $data) : bool
    {
        $data['related_id'] = 0;
        $data['related_type'] = '';

        $hash = $this->generateHash($data);

        // Save to the resource table when we don't have a user
        if (! isset($data[static::USER_COLUMN]) || empty($data[static::USER_COLUMN])) {
            return $this->saveWaitingTransaction($hash, $data);
        }

        return $this->saveTransaction($hash, $data);
    }

    /**
     * Save the waiting transaction.
     *
     * @param string $hash
     * @param array $data
     *
     * @return bool
     */
    protected function saveWaitingTransaction($hash, $data)
    {
        $attributes = array_merge(
            ['identifier' => $data[$this->getUserIdentifier()]],
            Arr::except(
                Arr::only($data, array_keys($this->fields)),
                [static::USER_COLUMN, $this->getUserIdentifier()]
            )
        );

        /** @var TransactionResource $resource */
        $resource = TransactionResource::query()->firstOrNew(compact('hash'), $attributes);

        if ($resource->exists) {
            $this->setResultColumn(static::RESULT_DUPLICATE . ": {$resource->getKey()} - resource");
            return $resource->exists;
        }

        $this->setResultColumn(static::RESULT_WAITING);
        $resource->save();

        return $resource->exists;
    }

    /**
     * Save a regular transaction.
     *
     * @param string $hash
     * @param array $data
     *
     * @return bool
     */
    protected function saveTransaction($hash, $data)
    {
        $attributes = Arr::except(
            Arr::only($data, array_keys($this->fields)),
            $this->getUserIdentifier()
        );

        /** @var Transaction $transaction */
        $transaction = Transaction::query()->firstOrNew(compact('hash'), $attributes);

        if ($transaction->exists) {
            $this->setResultColumn(static::RESULT_DUPLICATE . ": {$transaction->getKey()} - transaction");
            return $transaction->exists;
        }

        $this->setResultColumn(static::RESULT_IMPORTED);
        $transaction->save();

        return $transaction->exists;
    }

    /**
     * Generate a unique hash from the available unique data.
     *
     * @param array $data
     *
     * @return string
     */
    protected function generateHash(array $data)
    {
        return app(TransactionHasher::class)->hash([
            'identifier' => $data[$this->getUserIdentifier()],
            'value' => $data['value'],
            'description' => $data['description'],
            'type' => $data['type']
        ]);
    }

    /**
     * Transform the user identifier to ID but throw an exception with an unknown user.
     *
     * @param  array  $line
     * @param  string $key
     * @return int|null
     */
    protected function transformUser($line, $key)
    {
        if (! array_key_exists($key, $line)) {
            throw new DomainException("Unable to locate the header with name: $key");
        }

        $users = $this->cache['users'] ?? [];
        if (! array_key_exists($line[$key], $users)) {
            return null;
        }

        return $users[$line[$key]];
    }

    /**
     * Transform the Transaction type but throw an Exception with Unknown type.
     *
     * @param  array $line
     * @return string
     */
    protected function transformType($line)
    {
        $types = $this->transactionRepository->getAllowedTypes();

        if (! isset($line[static::TYPE_COLUMN]) || ! in_array($line[static::TYPE_COLUMN], $types)) {
            return 'EARNED';
        }

        return $types[$line[static::TYPE_COLUMN]];
    }

    /**
     * Transform the Transaction value but throw an Exception with Unknown value.
     *
     * @param  array $line
     * @return int
     */
    protected function transformValue($line)
    {
        if (! isset($line[static::VALUE_COLUMN]) || empty($line[static::VALUE_COLUMN])) {
            throw new DomainException('Missing value');
        }

        if (! is_numeric($line[static::VALUE_COLUMN])) {
            throw new DomainException('Value must be numeric');
        }

        return (float) $line[static::VALUE_COLUMN];
    }
}
