<?php

/**
 * IndexController.php
 * Copyright (c) 2019 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\Http\Controllers\Budget;

use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\AvailableBudget;
use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Budget\AvailableBudgetRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Support\Facades\Navigation;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Http\Controllers\DateCalculation;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Spatie\Period\Boundaries;
use Spatie\Period\Period;
use Spatie\Period\Precision;

/**
 * Class IndexController
 */
final class IndexController extends Controller
{
    use DateCalculation;

    private AvailableBudgetRepositoryInterface $abRepository;
    private BudgetLimitRepositoryInterface $blRepository;
    private CurrencyRepositoryInterface $currencyRepository;
    private OperationsRepositoryInterface $opsRepository;
    private BudgetRepositoryInterface $repository;

    /**
     * IndexController constructor.
     */
    public function __construct()
    {
        parent::__construct();

        $this->middleware(function ($request, $next) {
            app('view')->share('title', (string) trans('firefly.budgets'));
            app('view')->share('mainTitleIcon', 'bi-pie-chart');
            $this->repository         = app(BudgetRepositoryInterface::class);
            $this->opsRepository      = app(OperationsRepositoryInterface::class);
            $this->abRepository       = app(AvailableBudgetRepositoryInterface::class);
            $this->currencyRepository = app(CurrencyRepositoryInterface::class);
            $this->blRepository       = app(BudgetLimitRepositoryInterface::class);
            $this->repository->cleanupBudgets();

            return $next($request);
        });
    }

    /**
     * Show all budgets.
     *
     * @return Factory|View
     *
     * @throws FireflyException
     *                                              */
    public function index(?Carbon $start = null, ?Carbon $end = null): Factory|\Illuminate\Contracts\View\View
    {
        $this->abRepository->cleanup();
        Log::debug(sprintf('Start of IndexController::index("%s", "%s")', $start?->format('Y-m-d'), $end?->format('Y-m-d')));

        // collect some basic vars:
        $range            = Navigation::getViewRange(true);
        $isCustomRange    = session('is_custom_range', false);
        if (false === $isCustomRange) {
            $start ??= session('start', today(config('app.timezone'))->startOfMonth());
            $end   ??= Navigation::endOfPeriod($start, $range);
        }

        // overrule start and end if necessary:
        if (true === $isCustomRange) {
            $start ??= session('start', today(config('app.timezone'))->startOfMonth());
            $end   ??= session('end', today(config('app.timezone'))->endOfMonth());
        }

        $currencies       = $this->currencyRepository->get();
        $budgeted         = '0';
        $spent            = '0';

        // new period stuff:
        $periodTitle      = Navigation::periodShow($start, $range);
        $prevLoop         = $this->getPreviousPeriods($start, $range);
        $nextLoop         = $this->getNextPeriods($start, $range);

        // number of days for consistent budgeting.
        $activeDaysPassed = $this->activeDaysPassed($start, $end); // see method description.
        $activeDaysLeft   = $this->activeDaysLeft($start, $end); // see method description.

        // get all available budgets:
        $availableBudgets = $this->getAllAvailableBudgets($start, $end);
        // get all active budgets:
        $budgets          = $this->getAllBudgets($start, $end, $currencies, $this->primaryCurrency);

        //        echo '<pre>';
        //        var_dump($budgets[0]);exit;
        $sums             = $this->getSums($budgets);

        // get budgeted for default currency:
        if (0 === count($availableBudgets)) {
            $budgeted = $this->blRepository->budgeted($start, $end, $this->primaryCurrency);
            $spentArr = $this->opsRepository->sumExpenses($start, $end, null, null, $this->primaryCurrency);
            $spent    = $spentArr[$this->primaryCurrency->id]['sum'] ?? '0';
            unset($spentArr);
        }

        // get all inactive budgets, and simply list them:
        $inactive         = $this->repository->getInactiveBudgets();

        return view('budgets.index', [
            'availableBudgets' => $availableBudgets,
            'budgeted'         => $budgeted,
            'spent'            => $spent,
            'prevLoop'         => $prevLoop,
            'nextLoop'         => $nextLoop,
            'currencies'       => $currencies,
            'periodTitle'      => $periodTitle,
            'activeDaysPassed' => $activeDaysPassed,
            'activeDaysLeft'   => $activeDaysLeft,
            'inactive'         => $inactive,
            'budgets'          => $budgets,
            'start'            => $start,
            'end'              => $end,
            'sums'             => $sums,
        ]);
    }

    public function reorder(Request $request, BudgetRepositoryInterface $repository): JsonResponse
    {
        $this->abRepository->cleanup();
        $budgetIds = $request->input('budgetIds');

        foreach ($budgetIds as $index => $budgetId) {
            $budgetId = (int) $budgetId;
            $budget   = $repository->find($budgetId);
            if ($budget instanceof Budget) {
                Log::debug(sprintf('Set budget #%d ("%s") to position %d', $budget->id, $budget->name, $index + 1));
                $repository->setBudgetOrder($budget, $index + 1);
            }
        }
        Preferences::mark();

        return response()->json(['OK']);
    }

    private function getAllAvailableBudgets(Carbon $start, Carbon $end): array
    {
        Log::debug(sprintf('Start of getAllAvailableBudgets("%s", "%s")', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')));
        $converter        = new ExchangeRateConverter();
        // get all available budgets.
        $ab               = $this->abRepository->get($start, $end);
        $availableBudgets = [];

        // for each, complement with spent amount:
        /** @var AvailableBudget $entry */
        foreach ($ab as $entry) {
            $array                = $entry->toArray();
            $array['start_date']  = $entry->start_date;
            $array['end_date']    = $entry->end_date;

            // spent in period:
            $spentArr             = $this->opsRepository->sumExpenses($entry->start_date, $entry->end_date, null, null, $entry->transactionCurrency);
            $array['spent']       = $spentArr[$entry->transaction_currency_id]['sum'] ?? '0';
            $array['pc_spent']    = $this->convertToPrimary && $entry->transaction_currency_id !== $this->primaryCurrency->id
                ? $converter->convert($entry->transactionCurrency, $this->primaryCurrency, $entry->start_date, $array['spent'])
                : null;
            // budgeted in period:
            $budgeted             = $this->blRepository->budgeted($entry->start_date, $entry->end_date, $entry->transactionCurrency);
            $array['budgeted']    = $budgeted;
            $array['pc_budgeted'] = $this->convertToPrimary && $entry->transaction_currency_id !== $this->primaryCurrency->id
                ? $converter->convert($entry->transactionCurrency, $this->primaryCurrency, $entry->start_date, $budgeted)
                : null;
            // this time, because of complex sums, use the currency converter.

            $availableBudgets[]   = $array;
            unset($spentArr);
        }

        return $availableBudgets;
    }

    private function getAllBudgets(Carbon $start, Carbon $end, Collection $currencies, TransactionCurrency $primaryCurrency): array
    {
        // get all budgets, and paginate them into $budgets.
        $collection = $this->repository->getActiveBudgets();
        $budgets    = [];
        Log::debug(sprintf('(getAllBudgets) Start is "%s", end is "%s"', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')));

        // complement budget with budget limits in range, and expenses in currency X in range.
        /** @var Budget $budget */
        foreach ($collection as $budget) {
            Log::debug(sprintf('Working on budget #%d ("%s")', $budget->id, $budget->name));
            $array                = $budget->toArray();
            $array['spent']       = [];
            $array['budgeted']    = [];
            $array['attachments'] = $this->repository->getAttachments($budget);
            $array['auto_budget'] = $this->repository->getAutoBudget($budget);
            $budgetLimits         = $this->blRepository->getBudgetLimits($budget, $start, $end);
            $spentInLimits        = [];

            /** @var BudgetLimit $limit */
            foreach ($budgetLimits as $limit) {
                Log::debug(sprintf('Working on budget limit #%d', $limit->id));
                // number of days for consistent budgeting.
                $activeDaysPassed             = $this->activeDaysPassed($limit->start_date, $limit->end_date); // see method description.
                $activeDaysLeft               = $this->activeDaysLeft($limit->start_date, $limit->end_date); // see method description.
                $limitPeriod                  = Period::make($limit->start_date, $limit->end_date, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
                $inPast                       = $limitPeriod->startsBefore(now()) && $limitPeriod->endsBefore(now());
                $currency                     = $limit->transactionCurrency ?? $primaryCurrency;
                $amount                       = Steam::bcround($limit->amount, $currency->decimal_places);
                $spent                        = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency);

                $spentAmount                  = $spent[$currency->id]['sum'] ?? '0';
                $array['budgeted'][]          = [
                    'id'                      => $limit->id,
                    'amount'                  => $amount,
                    'notes'                   => $this->blRepository->getNoteText($limit),
                    'start_date'              => $limit->start_date->isoFormat($this->monthAndDayFormat),
                    'end_date'                => $limit->end_date->isoFormat($this->monthAndDayFormat),
                    'in_range'                => $limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end),
                    'in_past'                 => $inPast,
                    'total_days'              => $limit->start_date->diffInDays($limit->end_date) + 1,
                    'currency_id'             => $currency->id,
                    'currency_symbol'         => $currency->symbol,
                    'currency_name'           => $currency->name,
                    'currency_decimal_places' => $currency->decimal_places,
                    'spent'                   => $spentAmount,
                    'left'                    => bcadd($amount, $spentAmount),
                    'active_days_passed'      => $activeDaysPassed,
                    'active_days_left'        => $activeDaysLeft,
                ];
                $spentInLimits[$currency->id] = array_key_exists($currency->id, $spentInLimits)
                    ? bcadd($spentInLimits[$currency->id], $spentAmount)
                    : $spentAmount;
                Log::debug(sprintf('The amount budgeted for budget limit #%d is %s %s', $limit->id, $currency->code, $amount));
                Log::debug(sprintf('spentInLimits[%s] is now %s', $currency->code, $spentInLimits[$currency->id]));
            }
            // #10463
            Log::debug('Looping currencies');

            /** @var TransactionCurrency $currency */
            foreach ($currencies as $currency) {
                $spentInLimits[$currency->id] = array_key_exists($currency->id, $spentInLimits) ? $spentInLimits[$currency->id] : '0';
                $spentArr                     = $this->opsRepository->sumExpenses($start, $end, null, new Collection()->push($budget), $currency);

                Log::debug(sprintf('Working on currency %s, spentInLimits is %s', $currency->code, $spentInLimits[$currency->id]));

                if (array_key_exists($currency->id, $spentArr) && array_key_exists('sum', $spentArr[$currency->id])) {
                    $array['spent'][$currency->id]['spent']                   = $spentArr[$currency->id]['sum'];
                    $array['spent'][$currency->id]['spent_outside']           = Steam::negative(bcsub($spentInLimits[$currency->id], $spentArr[$currency->id]['sum']));
                    $array['spent'][$currency->id]['currency_id']             = $currency->id;
                    $array['spent'][$currency->id]['currency_symbol']         = $currency->symbol;
                    $array['spent'][$currency->id]['currency_decimal_places'] = $currency->decimal_places;
                }
            }
            $budgets[]            = $array;
        }

        return $budgets;
    }

    private function getSums(array $budgets): array
    {
        $sums = ['budgeted' => [], 'spent' => [], 'left' => []];

        /** @var array $budget */
        foreach ($budgets as $budget) {
            Log::debug(sprintf('Now working on budget #%d ("%s")', $budget['id'], $budget['name']));

            /** @var array $spent */
            foreach ($budget['spent'] as $spent) {
                $currencyId                           = $spent['currency_id'];
                $sums['spent'][$currencyId] ??= [
                    'amount'                  => '0',
                    'currency_id'             => $spent['currency_id'],
                    'currency_symbol'         => $spent['currency_symbol'],
                    'currency_decimal_places' => $spent['currency_decimal_places'],
                ];
                $sums['spent'][$currencyId]['amount'] = bcadd($sums['spent'][$currencyId]['amount'], (string) $spent['spent']);
            }

            /** @var array $budgeted */
            foreach ($budget['budgeted'] as $budgeted) {
                $currencyId                              = $budgeted['currency_id'];
                $sums['budgeted'][$currencyId] ??= [
                    'amount'                  => '0',
                    'currency_id'             => $budgeted['currency_id'],
                    'currency_symbol'         => $budgeted['currency_symbol'],
                    'currency_decimal_places' => $budgeted['currency_decimal_places'],
                ];
                $sums['budgeted'][$currencyId]['amount'] = bcadd($sums['budgeted'][$currencyId]['amount'], (string) $budgeted['amount']);

                // also calculate how much left from budgeted:
                $sums['left'][$currencyId]     ??= [
                    'amount'                  => '0',
                    'currency_id'             => $budgeted['currency_id'],
                    'currency_symbol'         => $budgeted['currency_symbol'],
                    'currency_decimal_places' => $budgeted['currency_decimal_places'],
                ];
            }
        }

        // final calculation for 'left':
        /**
         * @var int $currencyId
         */
        foreach (array_keys($sums['budgeted']) as $currencyId) {
            $spent                               = $sums['spent'][$currencyId]['amount'] ?? '0';
            $budgeted                            = $sums['budgeted'][$currencyId]['amount'] ?? '0';
            $sums['left'][$currencyId]['amount'] = bcadd($spent, $budgeted);
        }

        return $sums;
    }
}
