<?php

namespace App\Services;

use App\Models\Expense;
use App\Models\ExpenseReminder;
use App\Models\RecurringExpense;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;

class RecurringExpenseService
{
    public function __construct(private ExpenseService $expenses) {}

    public function process(string $date): array
    {
        $asOf = CarbonImmutable::parse($date)->startOfDay();
        $stats = ['reminders' => 0, 'expenses' => 0, 'schedules' => 0];

        // Active schedules must be considered before their first due date so a
        // configured pre-due reminder can be generated. The per-schedule logic
        // below decides whether the current date has reached the reminder trigger.
        $ids = RecurringExpense::query()
            ->where('status', 'active')
            ->pluck('id');

        foreach ($ids as $id) {
            DB::transaction(function () use ($id, $asOf, &$stats): void {
                $schedule = RecurringExpense::query()->lockForUpdate()->find($id);
                if (! $schedule || $schedule->status !== 'active') {
                    return;
                }
                $schedule->loadMissing('creator','category','financialAccount','branch');
                $stats['schedules']++;

                $due = CarbonImmutable::parse($schedule->next_due_date)->startOfDay();
                if ($schedule->end_date && $due->gt(CarbonImmutable::parse($schedule->end_date)->startOfDay())) {
                    $schedule->update(['status' => 'inactive', 'updated_by' => $schedule->created_by]);
                    return;
                }

                while ($due->lte($asOf)) {
                    $reminder = $this->ensureReminder($schedule, $due, $stats);
                    $expense = Expense::query()
                        ->where('recurring_expense_id', $schedule->id)
                        ->whereDate('occurrence_date', $due->toDateString())
                        ->first();

                    if (! $expense) {
                        $expense = $this->expenses->createPending([
                            'branch_id' => $schedule->branch_id,
                            'expense_category_id' => $schedule->expense_category_id,
                            'financial_account_id' => $schedule->financial_account_id,
                            'description' => $schedule->description,
                            'amount' => (string) $schedule->amount,
                            'expense_date' => $due,
                            'supplier' => $schedule->supplier,
                            'notes' => $schedule->notes,
                        ], $schedule->creator, $schedule, $due);
                        $stats['expenses']++;
                    }

                    if ($reminder->expense_id !== $expense->id || $reminder->status === 'open') {
                        $reminder->update(['expense_id' => $expense->id, 'status' => 'generated']);
                    }

                    $next = $this->advance($due, $schedule);
                    $schedule->update(['next_due_date' => $next->toDateString(), 'updated_by' => $schedule->created_by]);
                    $due = $next;

                    if ($schedule->end_date && $due->gt(CarbonImmutable::parse($schedule->end_date)->startOfDay())) {
                        $schedule->update(['status' => 'inactive']);
                        return;
                    }
                }

                $trigger = $due->subDays((int) $schedule->reminder_days_before);
                if ($asOf->gte($trigger) && $asOf->lt($due)) {
                    $this->ensureReminder($schedule, $due, $stats);
                }
            });
        }

        return $stats;
    }

    private function ensureReminder(RecurringExpense $schedule, CarbonImmutable $due, array &$stats): ExpenseReminder
    {
        $dueDate = $due->toDateString();
        $reminder = ExpenseReminder::query()
            ->where('recurring_expense_id', $schedule->id)
            ->whereDate('due_date', $dueDate)
            ->first();

        if ($reminder) {
            return $reminder;
        }

        $trigger = $due->subDays((int) $schedule->reminder_days_before);
        $reminder = ExpenseReminder::create([
            'recurring_expense_id' => $schedule->id,
            'due_date' => $dueDate,
            'branch_id' => $schedule->branch_id,
            'reminder_date' => $trigger->toDateString(),
            'message' => $schedule->category->name.' expense due '.$due->format('d M Y').': '.$schedule->description.' ('.number_format((float) $schedule->amount, 2).')',
            'status' => 'open',
        ]);
        $stats['reminders']++;

        return $reminder;
    }

    private function advance(CarbonImmutable $due, RecurringExpense $schedule): CarbonImmutable
    {
        return match ($schedule->recurrence) {
            'weekly' => $due->addWeek(),
            'monthly' => $due->addMonthNoOverflow(),
            'quarterly' => $due->addMonthsNoOverflow(3),
            'yearly' => $due->addYearNoOverflow(),
            'custom_days' => $due->addDays(max(1, (int) $schedule->interval_days)),
            default => throw new \InvalidArgumentException('Unsupported recurring expense interval.'),
        };
    }
}