<?php
/**
 * JournalTasker.php
 * Copyright (C) 2016 thegrumpydictator@gmail.com
 *
 * This software may be modified and distributed under the terms of the
 * Creative Commons Attribution-ShareAlike 4.0 International License.
 *
 * See the LICENSE file for details.
 */

declare(strict_types=1);

namespace FireflyIII\Repositories\Journal;

use DB;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Steam;

/**
 * Class JournalTasker
 *
 * @package FireflyIII\Repositories\Journal
 */
class JournalTasker implements JournalTaskerInterface
{

    /** @var User */
    private $user;


    /**
     * @param TransactionJournal $journal
     *
     * @return Collection
     */
    public function getPiggyBankEvents(TransactionJournal $journal): Collection
    {
        /** @var Collection $set */
        $events = $journal->piggyBankEvents()->get();
        $events->each(
            function (PiggyBankEvent $event) {
                $event->piggyBank = $event->piggyBank()->withTrashed()->first();
            }
        );

        return $events;
    }

    /**
     * Get an overview of the transactions of a journal, tailored to the view
     * that shows a transaction (transaction/show/xx).
     *
     * @param TransactionJournal $journal
     *
     * @return array
     */
    public function getTransactionsOverview(TransactionJournal $journal): array
    {
        // get all transaction data + the opposite site in one list.
        $set = $journal
            ->transactions()// "source"
            ->leftJoin(
                'transactions as destination', function (JoinClause $join) {
                $join
                    ->on('transactions.transaction_journal_id', '=', 'destination.transaction_journal_id')
                    ->where('transactions.amount', '=', DB::raw('destination.amount * -1'))
                    ->where('transactions.identifier', '=', DB::raw('destination.identifier'))
                    ->whereNull('destination.deleted_at');
            }
            )
            ->with(['budgets', 'categories'])
            ->leftJoin('accounts as source_accounts', 'transactions.account_id', '=', 'source_accounts.id')
            ->leftJoin('account_types as source_account_types', 'source_accounts.account_type_id', '=', 'source_account_types.id')
            ->leftJoin('accounts as destination_accounts', 'destination.account_id', '=', 'destination_accounts.id')
            ->leftJoin('account_types as destination_account_types', 'destination_accounts.account_type_id', '=', 'destination_account_types.id')
            ->leftJoin('transaction_currencies as native_currencies', 'transactions.transaction_currency_id', '=', 'native_currencies.id')
            ->leftJoin('transaction_currencies as foreign_currencies', 'transactions.foreign_currency_id', '=', 'foreign_currencies.id')
            ->where('transactions.amount', '<', 0)
            ->whereNull('transactions.deleted_at')
            ->get(
                [
                    'transactions.id',
                    'transactions.account_id',
                    'source_accounts.name as account_name',
                    'source_accounts.encrypted as account_encrypted',
                    'source_account_types.type as account_type',
                    'transactions.amount',
                    'transactions.foreign_amount',
                    'transactions.description',
                    'destination.id as destination_id',
                    'destination.account_id as destination_account_id',
                    'destination_accounts.name as destination_account_name',
                    'destination_accounts.encrypted as destination_account_encrypted',
                    'destination_account_types.type as destination_account_type',
                    'native_currencies.id as transaction_currency_id',
                    'native_currencies.decimal_places as transaction_currency_dp',
                    'native_currencies.code as transaction_currency_code',
                    'native_currencies.symbol as transaction_currency_symbol',

                    'foreign_currencies.id as foreign_currency_id',
                    'foreign_currencies.decimal_places as foreign_currency_dp',
                    'foreign_currencies.code as foreign_currency_code',
                    'foreign_currencies.symbol as foreign_currency_symbol',

                ]
            );

        $transactions = [];

        /** @var Transaction $entry */
        foreach ($set as $entry) {
            $sourceBalance      = $this->getBalance(intval($entry->id));
            $destinationBalance = $this->getBalance(intval($entry->destination_id));
            $budget             = $entry->budgets->first();
            $category           = $entry->categories->first();
            $transaction        = [
                'source_id'                   => $entry->id,
                'source_amount'               => $entry->amount,
                'foreign_source_amount'       => $entry->foreign_amount,
                'description'                 => $entry->description,
                'source_account_id'           => $entry->account_id,
                'source_account_name'         => Steam::decrypt(intval($entry->account_encrypted), $entry->account_name),
                'source_account_type'         => $entry->account_type,
                'source_account_before'       => $sourceBalance,
                'source_account_after'        => bcadd($sourceBalance, $entry->amount),
                'destination_id'              => $entry->destination_id,
                'destination_amount'          => bcmul($entry->amount, '-1'),
                'foreign_destination_amount'  => is_null($entry->foreign_amount) ? null : bcmul($entry->foreign_amount, '-1'),
                'destination_account_id'      => $entry->destination_account_id,
                'destination_account_type'    => $entry->destination_account_type,
                'destination_account_name'    => Steam::decrypt(intval($entry->destination_account_encrypted), $entry->destination_account_name),
                'destination_account_before'  => $destinationBalance,
                'destination_account_after'   => bcadd($destinationBalance, bcmul($entry->amount, '-1')),
                'budget_id'                   => is_null($budget) ? 0 : $budget->id,
                'category'                    => is_null($category) ? '' : $category->name,
                'transaction_currency_id'     => $entry->transaction_currency_id,
                'transaction_currency_code'   => $entry->transaction_currency_code,
                'transaction_currency_symbol' => $entry->transaction_currency_symbol,
                'transaction_currency_dp'     => $entry->transaction_currency_dp,
                'foreign_currency_id'         => $entry->foreign_currency_id,
                'foreign_currency_code'       => $entry->foreign_currency_code,
                'foreign_currency_symbol'     => $entry->foreign_currency_symbol,
                'foreign_currency_dp'         => $entry->foreign_currency_dp,
            ];
            if ($entry->destination_account_type === AccountType::CASH) {
                $transaction['destination_account_name'] = '';
            }

            if ($entry->account_type === AccountType::CASH) {
                $transaction['source_account_name'] = '';
            }


            $transactions[] = $transaction;
        }

        return $transactions;
    }

    /**
     * @param User $user
     */
    public function setUser(User $user)
    {
        $this->user = $user;
    }

    /**
     * Collect the balance of an account before the given transaction has hit. This is tricky, because
     * the balance does not depend on the transaction itself but the journal it's part of. And of course
     * the order of transactions within the journal. So the query is pretty complex:
     *
     * @param int $transactionId
     *
     * @return string
     */
    private function getBalance(int $transactionId): string
    {
        // find the transaction first:
        $transaction = Transaction::find($transactionId);
        $date        = $transaction->transactionJournal->date->format('Y-m-d');
        $order       = intval($transaction->transactionJournal->order);
        $journalId   = intval($transaction->transaction_journal_id);
        $identifier  = intval($transaction->identifier);

        // go!
        $sum = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
                          ->where('account_id', $transaction->account_id)
                          ->whereNull('transactions.deleted_at')
                          ->whereNull('transaction_journals.deleted_at')
                          ->where('transactions.id', '!=', $transactionId)
                          ->where(
                              function (Builder $q1) use ($date, $order, $journalId, $identifier) {
                                  $q1->where('transaction_journals.date', '<', $date); // date
                                  $q1->orWhere(
                                      function (Builder $q2) use ($date, $order) { // function 1
                                          $q2->where('transaction_journals.date', $date);
                                          $q2->where('transaction_journals.order', '>', $order);
                                      }
                                  );
                                  $q1->orWhere(
                                      function (Builder $q3) use ($date, $order, $journalId) { // function 2
                                          $q3->where('transaction_journals.date', $date);
                                          $q3->where('transaction_journals.order', $order);
                                          $q3->where('transaction_journals.id', '<', $journalId);
                                      }
                                  );
                                  $q1->orWhere(
                                      function (Builder $q4) use ($date, $order, $journalId, $identifier) { // function 3
                                          $q4->where('transaction_journals.date', $date);
                                          $q4->where('transaction_journals.order', $order);
                                          $q4->where('transaction_journals.id', $journalId);
                                          $q4->where('transactions.identifier', '>', $identifier);
                                      }
                                  );
                              }
                          )->sum('transactions.amount');

        return strval($sum);
    }
}
