<?php

/**
 * MigrateToGroups.php
 * Copyright (c) 2020 james@firefly-iii.org
 *
 * This file is part of Firefly III (https://github.com/firefly-iii).
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

declare(strict_types=1);

namespace FireflyIII\Console\Commands\Upgrade;

use Carbon\Carbon;
use Exception;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Factory\TransactionGroupFactory;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Category;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalCLIRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Services\Internal\Destroy\JournalDestroyService;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class UpgradesToGroups extends Command
{
    use ShowsFriendlyMessages;

    public const string CONFIG_NAME = '480_migrated_to_groups';

    protected $description          = 'Migrates a pre-4.7.8 transaction structure to the 4.7.8+ transaction structure.';
    protected $signature            = 'upgrade:480-migrate-to-groups {--F|force : Force the migration, even if it fired before.}';
    private JournalCLIRepositoryInterface $cliRepository;
    private int $count;
    private TransactionGroupFactory $groupFactory;
    private JournalRepositoryInterface $journalRepository;
    private JournalDestroyService $service;

    /**
     * Execute the console command.
     */
    public function handle(): int
    {
        $this->stupidLaravel();

        if ($this->isMigrated() && true !== $this->option('force')) {
            $this->friendlyInfo('Database is already migrated.');

            return 0;
        }

        if (true === $this->option('force')) {
            $this->friendlyWarning('Forcing the migration.');
        }

        $this->makeGroupsFromSplitJournals();
        $this->makeGroupsFromAll();

        if (0 !== $this->count) {
            $this->friendlyInfo(sprintf('Migrated %d transaction journal(s).', $this->count));
        }
        $this->markAsMigrated();

        return 0;
    }

    private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction
    {
        $set = $journal->transactions->filter(static function (Transaction $subject) use ($transaction): bool {
            $amount     = ((float) $transaction->amount * -1) === (float) $subject->amount; // intentional float
            $identifier = $transaction->identifier === $subject->identifier;
            Log::debug(sprintf('Amount the same? %s', var_export($amount, true)));
            Log::debug(sprintf('ID the same?     %s', var_export($identifier, true)));

            return $amount && $identifier;
        });

        return $set->first();
    }

    /**
     * @SuppressWarnings("PHPMD.ExcessiveMethodLength")
     */
    private function generateTransaction(TransactionJournal $journal, Transaction $transaction): array
    {
        Log::debug(sprintf('Now going to add transaction #%d to the array.', $transaction->id));
        $opposingTr     = $this->findOpposingTransaction($journal, $transaction);

        if (!$opposingTr instanceof Transaction) {
            $this->friendlyError(sprintf(
                'Journal #%d has no opposing transaction for transaction #%d. Cannot upgrade this entry.',
                $journal->id,
                $transaction->id
            ));

            return [];
        }

        $budgetId       = $this->cliRepository->getJournalBudgetId($journal);
        $categoryId     = $this->cliRepository->getJournalCategoryId($journal);
        $notes          = $this->cliRepository->getNoteText($journal);
        $tags           = $this->cliRepository->getTags($journal);
        $internalRef    = $this->cliRepository->getMetaField($journal, 'internal-reference');
        $sepaCC         = $this->cliRepository->getMetaField($journal, 'sepa_cc');
        $sepaCtOp       = $this->cliRepository->getMetaField($journal, 'sepa_ct_op');
        $sepaCtId       = $this->cliRepository->getMetaField($journal, 'sepa_ct_id');
        $sepaDb         = $this->cliRepository->getMetaField($journal, 'sepa_db');
        $sepaCountry    = $this->cliRepository->getMetaField($journal, 'sepa_country');
        $sepaEp         = $this->cliRepository->getMetaField($journal, 'sepa_ep');
        $sepaCi         = $this->cliRepository->getMetaField($journal, 'sepa_ci');
        $sepaBatchId    = $this->cliRepository->getMetaField($journal, 'sepa_batch_id');
        $externalId     = $this->cliRepository->getMetaField($journal, 'external-id');
        $originalSource = $this->cliRepository->getMetaField($journal, 'original-source');
        $recurrenceId   = $this->cliRepository->getMetaField($journal, 'recurrence_id');
        $bunq           = $this->cliRepository->getMetaField($journal, 'bunq_payment_id');
        $hash           = $this->cliRepository->getMetaField($journal, 'import_hash');
        $hashTwo        = $this->cliRepository->getMetaField($journal, 'import_hash_v2');
        $interestDate   = $this->cliRepository->getMetaDate($journal, 'interest_date');
        $bookDate       = $this->cliRepository->getMetaDate($journal, 'book_date');
        $processDate    = $this->cliRepository->getMetaDate($journal, 'process_date');
        $dueDate        = $this->cliRepository->getMetaDate($journal, 'due_date');
        $paymentDate    = $this->cliRepository->getMetaDate($journal, 'payment_date');
        $invoiceDate    = $this->cliRepository->getMetaDate($journal, 'invoice_date');

        // overrule journal category with transaction category.
        $budgetId       = $this->getTransactionBudget($transaction, $opposingTr) ?? $budgetId;
        $categoryId     = $this->getTransactionCategory($transaction, $opposingTr) ?? $categoryId;

        return [
            'type'                => strtolower((string) $journal->transactionType->type),
            'date'                => $journal->date,
            'user'                => $journal->user,
            'user_group'          => $journal->user->userGroup,
            'currency_id'         => $transaction->transaction_currency_id,
            'foreign_currency_id' => $transaction->foreign_currency_id,
            'amount'              => $transaction->amount,
            'foreign_amount'      => $transaction->foreign_amount,
            'description'         => $transaction->description ?? $journal->description,
            'source_id'           => $opposingTr->account_id,
            'destination_id'      => $transaction->account_id,
            'budget_id'           => $budgetId,
            'category_id'         => $categoryId,
            'bill_id'             => $journal->bill_id,
            'notes'               => $notes,
            'tags'                => $tags,
            'internal_reference'  => $internalRef,
            'sepa_cc'             => $sepaCC,
            'sepa_ct_op'          => $sepaCtOp,
            'sepa_ct_id'          => $sepaCtId,
            'sepa_db'             => $sepaDb,
            'sepa_country'        => $sepaCountry,
            'sepa_ep'             => $sepaEp,
            'sepa_ci'             => $sepaCi,
            'sepa_batch_id'       => $sepaBatchId,
            'external_id'         => $externalId,
            'original-source'     => $originalSource,
            'recurrence_id'       => $recurrenceId,
            'bunq_payment_id'     => $bunq,
            'import_hash'         => $hash,
            'import_hash_v2'      => $hashTwo,
            'interest_date'       => $interestDate,
            'book_date'           => $bookDate,
            'process_date'        => $processDate,
            'due_date'            => $dueDate,
            'payment_date'        => $paymentDate,
            'invoice_date'        => $invoiceDate,
        ];
    }

    private function getDestinationTransactions(TransactionJournal $journal): Collection
    {
        return $journal->transactions->filter(static fn (Transaction $transaction): bool => $transaction->amount > 0);
    }

    private function getTransactionBudget(Transaction $left, Transaction $right): ?int
    {
        Log::debug('Now in getTransactionBudget()');

        // try to get a budget ID from the left transaction:
        /** @var null|Budget $budget */
        $budget = $left->budgets()->first();
        if (null !== $budget) {
            Log::debug(sprintf('Return budget #%d, from transaction #%d', $budget->id, $left->id));

            return $budget->id;
        }

        // try to get a budget ID from the right transaction:
        /** @var null|Budget $budget */
        $budget = $right->budgets()->first();
        if (null !== $budget) {
            Log::debug(sprintf('Return budget #%d, from transaction #%d', $budget->id, $right->id));

            return $budget->id;
        }
        Log::debug('Neither left or right have a budget, return NULL');

        // if all fails, return NULL.
        return null;
    }

    private function getTransactionCategory(Transaction $left, Transaction $right): ?int
    {
        Log::debug('Now in getTransactionCategory()');

        // try to get a category ID from the left transaction:
        /** @var null|Category $category */
        $category = $left->categories()->first();
        if (null !== $category) {
            Log::debug(sprintf('Return category #%d, from transaction #%d', $category->id, $left->id));

            return $category->id;
        }

        // try to get a category ID from the left transaction:
        /** @var null|Category $category */
        $category = $right->categories()->first();
        if (null !== $category) {
            Log::debug(sprintf('Return category #%d, from transaction #%d', $category->id, $category->id));

            return $category->id;
        }
        Log::debug('Neither left or right have a category, return NULL');

        // if all fails, return NULL.
        return null;
    }

    private function giveGroup(array $array): void
    {
        $groupId = DB::table('transaction_groups')->insertGetId([
            'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
            'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
            'title'      => null,
            'user_id'    => $array['user_id'],
        ]);
        DB::table('transaction_journals')->where('id', $array['id'])->update(['transaction_group_id' => $groupId]);
        ++$this->count;
    }

    private function isMigrated(): bool
    {
        $configVar = FireflyConfig::get(self::CONFIG_NAME, false);

        return (bool) $configVar?->data;
    }

    /**
     * Gives all journals without a group a group.
     */
    private function makeGroupsFromAll(): void
    {
        $orphanedJournals = $this->cliRepository->getJournalsWithoutGroup();
        $total            = count($orphanedJournals);
        if ($total > 0) {
            Log::debug(sprintf('Going to convert %d transaction journals. Please hold..', $total));
            $this->friendlyInfo(sprintf('Going to convert %d transaction journals. Please hold..', $total));

            /** @var array $array */
            foreach ($orphanedJournals as $array) {
                $this->giveGroup($array);
            }
        }
    }

    /**
     * @throws Exception
     */
    private function makeGroupsFromSplitJournals(): void
    {
        $splitJournals = $this->cliRepository->getSplitJournals();
        if ($splitJournals->count() > 0) {
            $this->friendlyLine(sprintf('Going to convert %d split transaction(s). Please hold..', $splitJournals->count()));

            /** @var TransactionJournal $journal */
            foreach ($splitJournals as $journal) {
                $this->makeMultiGroup($journal);
            }
        }
    }

    /**
     * @throws Exception
     */
    private function makeMultiGroup(TransactionJournal $journal): void
    {
        // double check transaction count.
        if ($journal->transactions->count() <= 2) {
            Log::debug(sprintf('Will not try to convert journal #%d because it has 2 or fewer transactions.', $journal->id));

            return;
        }
        Log::debug(sprintf('Will now try to convert journal #%d', $journal->id));

        $this->journalRepository->setUser($journal->user);
        $this->groupFactory->setUser($journal->user);
        $this->cliRepository->setUser($journal->user);

        $data             = [
            // mandatory fields.
            'group_title'  => $journal->description,
            'transactions' => [],
        ];
        $destTransactions = $this->getDestinationTransactions($journal);

        Log::debug(sprintf('Will use %d positive transactions to create a new group.', $destTransactions->count()));

        /** @var Transaction $transaction */
        foreach ($destTransactions as $transaction) {
            $data['transactions'][] = $this->generateTransaction($journal, $transaction);
        }
        Log::debug(sprintf('Now calling transaction journal factory (%d transactions in array)', count($data['transactions'])));
        $group            = $this->groupFactory->create($data);
        Log::debug('Done calling transaction journal factory');

        // delete the old transaction journal.
        $this->service->destroy($journal);

        ++$this->count;

        // report on result:
        Log::debug(sprintf(
            'Migrated journal #%d into group #%d with these journals: #%s',
            $journal->id,
            $group->id,
            implode(', #', $group->transactionJournals->pluck('id')->toArray())
        ));
        $this->friendlyInfo(sprintf(
            'Migrated journal #%d into group #%d with these journals: #%s',
            $journal->id,
            $group->id,
            implode(', #', $group->transactionJournals->pluck('id')->toArray())
        ));
    }

    private function markAsMigrated(): void
    {
        FireflyConfig::set(self::CONFIG_NAME, true);
    }

    /**
     * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
     * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
     * be called from the handle method instead of using the constructor to initialize the command.
     */
    private function stupidLaravel(): void
    {
        $this->count             = 0;
        $this->journalRepository = app(JournalRepositoryInterface::class);
        $this->service           = app(JournalDestroyService::class);
        $this->groupFactory      = app(TransactionGroupFactory::class);
        $this->cliRepository     = app(JournalCLIRepositoryInterface::class);
    }
}
