<?php

/*
 * RecurringEnrichment.php
 * Copyright (c) 2025 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\Support\JsonApi\Enrichments;

use Carbon\Carbon;
use FireflyIII\Enums\RecurrenceRepetitionWeekend;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\CategoryFactory;
use FireflyIII\Models\Account;
use FireflyIII\Models\Bill;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Category;
use FireflyIII\Models\Note;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\Preference;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\RecurrenceRepetition;
use FireflyIII\Models\RecurrenceTransaction;
use FireflyIII\Models\RecurrenceTransactionMeta;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;

use function Safe\json_decode;

class RecurringEnrichment implements EnrichmentInterface
{
    private array $accounts                = [];
    private Collection $collection;
    //    private array               $transactionTypeIds    = [];
    // private array               $transactionTypes      = [];
    private readonly bool $convertToPrimary;
    private array  $currencies             = [];
    private array  $currencyIds            = [];
    private array  $destinationAccountIds  = [];
    private array  $foreignCurrencyIds     = [];
    private array  $ids                    = [];
    private string $language               = 'en_US';
    private array  $notes                  = [];
    private readonly TransactionCurrency $primaryCurrency;
    private array $recurrenceByTransaction = [];
    private array $repetitions             = [];
    private array $sourceAccountIds        = [];
    private array $transactions            = [];
    private User $user;
    private UserGroup $userGroup;

    public function __construct()
    {
        $this->primaryCurrency  = Amount::getPrimaryCurrency();
        $this->convertToPrimary = Amount::convertToPrimary();
    }

    public function enrich(Collection $collection): Collection
    {
        Log::debug(__METHOD__);
        $this->collection = $collection;
        $this->collectIds();
        $this->collectRepetitions();
        $this->collectTransactions();
        $this->collectCurrencies();
        $this->collectNotes();
        $this->collectAccounts();
        $this->collectTransactionMetaData();

        $this->appendCollectedData();

        return $this->collection;
    }

    public function enrichSingle(array|Model $model): array|Model
    {
        Log::debug(__METHOD__);
        $collection = new Collection()->push($model);
        $collection = $this->enrich($collection);

        return $collection->first();
    }

    /**
     * Parse the repetition in a string that is user readable.
     * TODO duplicate with repository.
     */
    public function getRepetitionDescription(RecurrenceRepetition $repetition): string
    {
        if ('daily' === $repetition->repetition_type) {
            return (string) trans('firefly.recurring_daily', [], $this->language);
        }
        if ('weekly' === $repetition->repetition_type) {
            $dayOfWeek = trans(sprintf('config.dow_%s', $repetition->repetition_moment), [], $this->language);
            if ($repetition->repetition_skip > 0) {
                return (string) trans('firefly.recurring_weekly_skip', ['weekday' => $dayOfWeek, 'skip' => $repetition->repetition_skip + 1], $this->language);
            }

            return (string) trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek], $this->language);
        }
        if ('monthly' === $repetition->repetition_type) {
            if ($repetition->repetition_skip > 0) {
                return (string) trans(
                    'firefly.recurring_monthly_skip',
                    ['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip + 1],
                    $this->language
                );
            }

            return (string) trans(
                'firefly.recurring_monthly',
                ['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip - 1],
                $this->language
            );
        }
        if ('ndom' === $repetition->repetition_type) {
            $parts     = explode(',', $repetition->repetition_moment);
            // first part is number of week, second is weekday.
            $dayOfWeek = trans(sprintf('config.dow_%s', $parts[1]), [], $this->language);
            if ($repetition->repetition_skip > 0) {
                return (string) trans(
                    'firefly.recurring_ndom_skip',
                    ['skip' => $repetition->repetition_skip, 'weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]],
                    $this->language
                );
            }

            return (string) trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $this->language);
        }
        if ('yearly' === $repetition->repetition_type) {
            $today   = today(config('app.timezone'))->endOfYear();
            $repDate = Carbon::createFromFormat('Y-m-d', $repetition->repetition_moment);
            if (!$repDate instanceof Carbon) {
                $repDate = clone $today;
            }
            // $diffInYears = (int)$today->diffInYears($repDate, true);
            // $repDate->addYears($diffInYears); // technically not necessary.
            $string  = $repDate->isoFormat((string) trans('config.month_and_day_no_year_js'));

            return (string) trans('firefly.recurring_yearly', ['date' => $string], $this->language);
        }

        return '';
    }

    public function setUser(User $user): void
    {
        $this->user = $user;
        $this->setUserGroup($user->userGroup);
        $this->getLanguage();
    }

    public function setUserGroup(UserGroup $userGroup): void
    {
        $this->userGroup = $userGroup;
    }

    private function appendCollectedData(): void
    {
        $this->collection = $this->collection->map(function (Recurrence $item): Recurrence {
            $id         = (int) $item->id;
            $meta       = [
                'notes'        => $this->notes[$id] ?? null,
                'repetitions'  => array_values($this->repetitions[$id] ?? []),
                'transactions' => $this->processTransactions(array_values($this->transactions[$id] ?? [])),
            ];

            $item->meta = $meta;

            return $item;
        });
    }

    private function collectAccounts(): void
    {
        $all      = array_merge(array_unique($this->sourceAccountIds), array_unique($this->destinationAccountIds));
        $accounts = Account::query()->with(['accountType'])->whereIn('id', array_unique($all))->get();

        /** @var Account $account */
        foreach ($accounts as $account) {
            $id                  = (int) $account->id;
            $this->accounts[$id] = $account;
            Log::debug(sprintf('Collected account #%d', $id));
        }
    }

    private function collectBillInfo(array $billIds): void
    {
        if (0 === count($billIds)) {
            return;
        }
        $ids    = Arr::pluck($billIds, 'bill_id');
        $bills  = Bill::query()->whereIn('id', $ids)->get();
        $mapped = [];
        foreach ($bills as $bill) {
            $mapped[(int) $bill->id] = $bill;
        }
        foreach ($billIds as $info) {
            $recurrenceId                                                           = $info['recurrence_id'];
            $transactionId                                                          = $info['transaction_id'];
            $this->transactions[$recurrenceId][$transactionId]['subscription_name'] = $mapped[$info['bill_id']]->name ?? '';
        }
    }

    private function collectBudgetInfo(array $budgetIds): void
    {
        if (0 === count($budgetIds)) {
            return;
        }
        $ids        = Arr::pluck($budgetIds, 'budget_id');
        $categories = Budget::query()->whereIn('id', $ids)->get();
        $mapped     = [];
        foreach ($categories as $category) {
            $mapped[(int) $category->id] = $category;
        }
        foreach ($budgetIds as $info) {
            $recurrenceId                                                     = $info['recurrence_id'];
            $transactionId                                                    = $info['transaction_id'];
            $this->transactions[$recurrenceId][$transactionId]['budget_name'] = $mapped[$info['budget_id']]->name ?? '';
        }
    }

    private function collectCategoryIdInfo(array $categoryIds): void
    {
        if (0 === count($categoryIds)) {
            return;
        }
        $ids        = Arr::pluck($categoryIds, 'category_id');
        $categories = Category::query()->whereIn('id', $ids)->get();
        $mapped     = [];
        foreach ($categories as $category) {
            $mapped[(int) $category->id] = $category;
        }
        foreach ($categoryIds as $info) {
            $recurrenceId                                                       = $info['recurrence_id'];
            $transactionId                                                      = $info['transaction_id'];
            $this->transactions[$recurrenceId][$transactionId]['category_name'] = $mapped[$info['category_id']]->name ?? '';
        }
    }

    /**
     * TODO This method does look-up in a loop.
     */
    private function collectCategoryNameInfo(array $categoryNames): void
    {
        if (0 === count($categoryNames)) {
            return;
        }
        $factory = app(CategoryFactory::class);
        $factory->setUser($this->user);
        foreach ($categoryNames as $info) {
            $recurrenceId  = $info['recurrence_id'];
            $transactionId = $info['transaction_id'];
            $category      = $factory->findOrCreate(null, $info['category_name']);
            if (null !== $category) {
                $this->transactions[$recurrenceId][$transactionId]['category_id']   = (string) $category->id;
                $this->transactions[$recurrenceId][$transactionId]['category_name'] = $category->name;
            }
        }
    }

    private function collectCurrencies(): void
    {
        $all        = array_merge(array_unique($this->currencyIds), array_unique($this->foreignCurrencyIds));
        $currencies = TransactionCurrency::query()->whereIn('id', array_unique($all))->get();
        foreach ($currencies as $currency) {
            $id                    = (int) $currency->id;
            $this->currencies[$id] = $currency;
            Log::debug(sprintf('Collected currency #%d', $id));
        }
    }

    private function collectIds(): void
    {
        /** @var Recurrence $recurrence */
        foreach ($this->collection as $recurrence) {
            $id          = (int) $recurrence->id;
            $this->ids[] = $id;
            Log::debug(sprintf('Collected recurrence id #%d', $id));
        }
        $this->ids = array_unique($this->ids);
        Log::debug('Final array:', $this->ids);
    }

    private function collectNotes(): void
    {
        $notes = Note::query()
            ->whereIn('noteable_id', $this->ids)
            ->whereNotNull('notes.text')
            ->where('notes.text', '!=', '')
            ->where('noteable_type', Recurrence::class)
            ->get(['notes.id', 'notes.noteable_id', 'notes.text'])
            ->toArray()
        ;
        foreach ($notes as $note) {
            $notableId               = (int) $note['noteable_id'];
            $this->notes[$notableId] = (string) $note['text'];
            Log::debug(sprintf('Collected note #%d for recurrence #%d', $note['id'], $notableId));
        }

        //        Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
    }

    private function collectPiggyBankInfo(array $piggyBankIds): void
    {
        if (0 === count($piggyBankIds)) {
            return;
        }
        $ids        = Arr::pluck($piggyBankIds, 'piggy_bank_id');
        $piggyBanks = PiggyBank::query()->whereIn('id', $ids)->get();
        $mapped     = [];
        foreach ($piggyBanks as $piggyBank) {
            $mapped[(int) $piggyBank->id] = $piggyBank;
        }
        foreach ($piggyBankIds as $info) {
            $recurrenceId                                                         = $info['recurrence_id'];
            $transactionId                                                        = $info['transaction_id'];
            $this->transactions[$recurrenceId][$transactionId]['piggy_bank_name'] = $mapped[$info['piggy_bank_id']]->name ?? '';
        }
    }

    private function collectRepetitions(): void
    {
        Log::debug('Start of enrichment: collectRepetitions()');
        $repository = app(RecurringRepositoryInterface::class);
        $repository->setUserGroup($this->userGroup);
        $set        = RecurrenceRepetition::query()->whereIn('recurrence_id', $this->ids)->get();

        /** @var RecurrenceRepetition $repetition */
        foreach ($set as $repetition) {
            $recurrence                               = $this->collection->filter(static fn (Recurrence $item): bool => (int) $item->id === (int) $repetition->recurrence_id)->first();
            $fromDate                                 = clone ($recurrence->latest_date ?? $recurrence->first_date);
            $recurrenceId                             = (int) $repetition->recurrence_id;
            $repId                                    = (int) $repetition->id;
            $this->repetitions[$recurrenceId] ??= [];
            Log::debug(sprintf('Collected repetition #%d of recurrence #%d.', $repId, $recurrenceId));

            // get the (future) occurrences for this specific type of repetition:
            $amount                                   = 'daily' === $repetition->repetition_type ? 9 : 5;
            $set                                      = $repository->getXOccurrencesSince($repetition, $fromDate, now(config('app.timezone')), $amount);
            $occurrences                              = [];

            /** @var Carbon $carbon */
            foreach ($set as $carbon) {
                $occurrences[] = $carbon->toAtomString();
            }
            $this->repetitions[$recurrenceId][$repId] = [
                'id'          => (string) $repId,
                'created_at'  => $repetition->created_at->toAtomString(),
                'updated_at'  => $repetition->updated_at->toAtomString(),
                'type'        => $repetition->repetition_type,
                'moment'      => (string) $repetition->repetition_moment,
                'skip'        => (int) $repetition->repetition_skip,
                'weekend'     => RecurrenceRepetitionWeekend::from((int) $repetition->weekend)->value,
                'description' => $this->getRepetitionDescription($repetition),
                'occurrences' => $occurrences,
            ];
        }
        Log::debug('End of enrichment: collectRepetitions()');
    }

    private function collectTransactionMetaData(): void
    {
        $rtIds         = [];
        foreach ($this->ids as $recurrenceId) {
            $rtIds = array_merge($rtIds, array_keys($this->transactions[$recurrenceId]));
        }
        $meta          = RecurrenceTransactionMeta::query()->whereNull('deleted_at')->whereIn('rt_id', $rtIds)->get();
        // other meta-data to be collected:
        $billIds       = [];
        $piggyBankIds  = [];
        $categoryIds   = [];
        $categoryNames = [];
        $budgetIds     = [];
        foreach ($meta as $entry) {
            $id            = (int) $entry->id;
            $transactionId = (int) $entry->rt_id;

            // this should refer to another array, were rtIds can be used to find the recurrence.
            $recurrenceId  = $this->recurrenceByTransaction[$transactionId] ?? 0;
            Log::debug(sprintf('Collecting meta data entry #%d for recurrence transaction #%d, for recurrence #%d ', $id, $transactionId, $recurrenceId));
            $name          = (string) ($entry->name ?? '');
            if (0 === $recurrenceId) {
                Log::error(sprintf('Could not find recurrence ID for recurrence transaction ID #%d', $transactionId));

                continue;
            }

            switch ($name) {
                default:
                    throw new FireflyException(sprintf('Recurrence transformer cant handle field "%s"', $name));

                case 'bill_id':
                    if ((int) $entry->value > 0) {
                        $this->transactions[$recurrenceId][$transactionId]['subscription_id'] = $entry->value;
                        if (!array_key_exists($id, $billIds)) {
                            $billIds[$id] = ['recurrence_id' => $recurrenceId, 'transaction_id' => $transactionId, 'bill_id' => (int) $entry->value];
                        }
                    }

                    break;

                case 'tags':
                    $this->transactions[$recurrenceId][$transactionId]['tags'] = json_decode((string) $entry->value);

                    break;

                case 'piggy_bank_id':
                    if ((int) $entry->value > 0) {
                        $this->transactions[$recurrenceId][$transactionId]['piggy_bank_id'] = (string) $entry->value;
                        if (!array_key_exists($id, $piggyBankIds)) {
                            $piggyBankIds[$id] = [
                                'recurrence_id'  => $recurrenceId,
                                'transaction_id' => $transactionId,
                                'piggy_bank_id'  => (int) $entry->value,
                            ];
                        }
                    }

                    break;

                case 'category_id':
                    if ((int) $entry->value > 0) {
                        $this->transactions[$recurrenceId][$transactionId]['category_id'] = (string) $entry->value;
                        if (!array_key_exists($id, $categoryIds)) {
                            $categoryIds[$id] = [
                                'recurrence_id'  => $recurrenceId,
                                'transaction_id' => $transactionId,
                                'category_id'    => (int) $entry->value,
                            ];
                        }
                    }

                    break;

                case 'category_name':
                    if ('' !== (string) $entry->value) {
                        $this->transactions[$recurrenceId][$transactionId]['category_name'] = (string) $entry->value;
                        if (!array_key_exists($id, $categoryIds)) {
                            $categoryNames[$id] = ['recurrence_id' => $recurrenceId, 'transaction_id' => $transactionId, 'category_name' => $entry->value];
                        }
                    }

                    break;

                case 'budget_id':
                    if ((int) $entry->value > 0) {
                        $this->transactions[$recurrenceId][$transactionId]['budget_id'] = (string) $entry->value;
                        if (!array_key_exists($id, $budgetIds)) {
                            $budgetIds[$id] = ['recurrence_id' => $recurrenceId, 'transaction_id' => $transactionId, 'budget_id' => (int) $entry->value];
                        }
                    }

                    break;
            }
        }
        $this->collectBillInfo($billIds);
        $this->collectPiggyBankInfo($piggyBankIds);
        $this->collectCategoryIdInfo($categoryIds);
        $this->collectCategoryNameInfo($categoryNames);
        $this->collectBudgetInfo($budgetIds);
    }

    private function collectTransactions(): void
    {
        $set = RecurrenceTransaction::query()->whereIn('recurrence_id', $this->ids)->get();

        /** @var RecurrenceTransaction $transaction */
        foreach ($set as $transaction) {
            $recurrenceId                                      = (int) $transaction->recurrence_id;
            $transactionId                                     = (int) $transaction->id;
            $this->recurrenceByTransaction[$transactionId]     = $recurrenceId;
            $this->transactions[$recurrenceId] ??= [];
            $amount                                            = $transaction->amount;
            $foreignAmount                                     = $transaction->foreign_amount;
            Log::debug(sprintf('Collected transaction #%d of recurrence #%d', $transactionId, $recurrenceId));

            $this->transactions[$recurrenceId][$transactionId] = [
                'id'                          => (string) $transactionId,
                'transaction_currency_id'     => (int) $transaction->transaction_currency_id,
                'foreign_currency_id'         => null === $transaction->foreign_currency_id ? null : (int) $transaction->foreign_currency_id,
                'source_id'                   => (int) $transaction->source_id,
                'object_has_currency_setting' => true,
                'destination_id'              => (int) $transaction->destination_id,
                'amount'                      => $amount,
                'foreign_amount'              => $foreignAmount,
                'pc_amount'                   => null,
                'pc_foreign_amount'           => null,
                'description'                 => $transaction->description,
                'tags'                        => [],
                'category_id'                 => null,
                'category_name'               => null,
                'budget_id'                   => null,
                'budget_name'                 => null,
                'piggy_bank_id'               => null,
                'piggy_bank_name'             => null,
                'subscription_id'             => null,
                'subscription_name'           => null,
            ];
            // collect all kinds of meta-data to be collected later.
            $this->currencyIds[$transactionId]                 = (int) $transaction->transaction_currency_id;
            $this->sourceAccountIds[$transactionId]            = (int) $transaction->source_id;
            $this->destinationAccountIds[$transactionId]       = (int) $transaction->destination_id;
            if (null !== $transaction->foreign_currency_id) {
                $this->foreignCurrencyIds[$transactionId] = (int) $transaction->foreign_currency_id;
            }
        }
    }

    private function getLanguage(): void
    {
        /** @var Preference $preference */
        $preference     = Preferences::getForUser($this->user, 'language', config('firefly.default_language', 'en_US'));
        $language       = $preference->data;
        if (is_array($language)) {
            $language = 'en_US';
        }
        $language       = (string) $language;
        $this->language = $language;
    }

    private function processTransactions(array $transactions): array
    {
        $return    = [];
        $converter = new ExchangeRateConverter();
        foreach ($transactions as $transaction) {
            $currencyId                                     = $transaction['transaction_currency_id'];
            $pcAmount                                       = null;
            $pcForeignAmount                                = null;
            // set the same amount in the primary currency, if both are the same anyway.
            if ($this->convertToPrimary && $currencyId === (int) $this->primaryCurrency->id) {
                $pcAmount = $transaction['amount'];
            }
            // convert the amount to the primary currency, if it is not the same.
            if ($this->convertToPrimary && $currencyId !== (int) $this->primaryCurrency->id) {
                $pcAmount = $converter->convert($this->currencies[$currencyId], $this->primaryCurrency, today(), $transaction['amount']);
            }
            if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) {
                $foreignCurrencyId = $transaction['foreign_currency_id'];
                if ($foreignCurrencyId !== $this->primaryCurrency->id) {
                    $pcForeignAmount = $converter->convert(
                        $this->currencies[$foreignCurrencyId],
                        $this->primaryCurrency,
                        today(),
                        $transaction['foreign_amount']
                    );
                }
            }

            $transaction['pc_amount']                       = $pcAmount;
            $transaction['pc_foreign_amount']               = $pcForeignAmount;

            $sourceId                                       = $transaction['source_id'];
            $transaction['source_name']                     = $this->accounts[$sourceId]->name;
            $transaction['source_iban']                     = $this->accounts[$sourceId]->iban;
            $transaction['source_type']                     = $this->accounts[$sourceId]->accountType->type;
            $transaction['source_id']                       = (string) $transaction['source_id'];

            $destId                                         = $transaction['destination_id'];
            $transaction['destination_name']                = $this->accounts[$destId]->name;
            $transaction['destination_iban']                = $this->accounts[$destId]->iban;
            $transaction['destination_type']                = $this->accounts[$destId]->accountType->type;
            $transaction['destination_id']                  = (string) $transaction['destination_id'];

            $transaction['currency_id']                     = (string) $currencyId;
            $transaction['currency_name']                   = $this->currencies[$currencyId]->name;
            $transaction['currency_code']                   = $this->currencies[$currencyId]->code;
            $transaction['currency_symbol']                 = $this->currencies[$currencyId]->symbol;
            $transaction['currency_decimal_places']         = $this->currencies[$currencyId]->decimal_places;

            $transaction['primary_currency_id']             = (string) $this->primaryCurrency->id;
            $transaction['primary_currency_name']           = $this->primaryCurrency->name;
            $transaction['primary_currency_code']           = $this->primaryCurrency->code;
            $transaction['primary_currency_symbol']         = $this->primaryCurrency->symbol;
            $transaction['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places;

            // $transaction['foreign_currency_id'] = null;
            $transaction['foreign_currency_name']           = null;
            $transaction['foreign_currency_code']           = null;
            $transaction['foreign_currency_symbol']         = null;
            $transaction['foreign_currency_decimal_places'] = null;
            if (null !== $transaction['foreign_currency_id']) {
                $currencyId                                     = $transaction['foreign_currency_id'];
                $transaction['foreign_currency_id']             = (string) $currencyId;
                $transaction['foreign_currency_name']           = $this->currencies[$currencyId]->name;
                $transaction['foreign_currency_code']           = $this->currencies[$currencyId]->code;
                $transaction['foreign_currency_symbol']         = $this->currencies[$currencyId]->symbol;
                $transaction['foreign_currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places;
            }
            unset($transaction['transaction_currency_id']);
            $return[]                                       = $transaction;
        }

        return $return;
    }
}
