<?php

namespace App\Services;

use App\Models\Loan;
use App\Models\LoanPayment;
use App\Models\LoanRepaymentSchedule;
use App\Models\SavingsAccount;
use App\Models\SavingsTransaction;
use App\Models\Tenant;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Throwable;

class AutoDebitService
{
    public function __construct(
        private readonly LoanPaymentService $loanPayments,
        private readonly AccountingService $accounting,
    ) {
    }

    /**
     * @return array{loan_attempts:int, loan_successful:int, savings_attempts:int, savings_successful:int}
     */
    public function run(?Tenant $tenant = null, ?Carbon $date = null): array
    {
        $date ??= today();

        return [
            ...$this->processLoanDebits($tenant, $date),
            ...$this->processSavingsDebits($tenant, $date),
        ];
    }

    /**
     * @return array{loan_attempts:int, loan_successful:int}
     */
    public function processLoanDebits(?Tenant $tenant = null, ?Carbon $date = null): array
    {
        $date ??= today();
        $attempts = 0;
        $successful = 0;

        $query = LoanRepaymentSchedule::withoutGlobalScopes()
            ->with(['loan.autoDebitAccount', 'loan.customer'])
            ->whereIn('status', ['pending', 'partial', 'overdue'])
            ->whereDate('due_date', '<=', $date->toDateString())
            ->whereHas('loan', function ($loanQuery) use ($tenant): void {
                $loanQuery->whereIn('status', ['active', 'disbursed'])
                    ->whereIn('auto_debit_mode', [Loan::AUTO_DEBIT_AUTOMATIC, Loan::AUTO_DEBIT_MANUAL_ON_DEFAULT])
                    ->whereNotNull('auto_debit_savings_account_id')
                    ->when($tenant, fn ($query) => $query->where('tenant_id', $tenant->id));
            })
            ->orderBy('due_date')
            ->orderBy('installment_no');

        $query->chunkById(100, function ($schedules) use ($date, &$attempts, &$successful): void {
            foreach ($schedules as $schedule) {
                $loan = $schedule->loan;

                if (! $loan) {
                    continue;
                }

                if ($loan->auto_debit_mode === Loan::AUTO_DEBIT_MANUAL_ON_DEFAULT && $schedule->due_date?->isSameDay($date)) {
                    continue;
                }

                $attempts++;
                $amount = max(0, (float) $schedule->total_due - (float) $schedule->paid_amount);

                if ($amount <= 0) {
                    continue;
                }

                try {
                    $this->debitLoan($loan, $amount);
                    $successful++;
                } catch (Throwable $exception) {
                    $loan->forceFill([
                        'auto_debit_last_attempt_at' => now(),
                        'auto_debit_failure_reason' => $exception->getMessage(),
                    ])->save();
                }
            }
        });

        return ['loan_attempts' => $attempts, 'loan_successful' => $successful];
    }

    /**
     * @return array{savings_attempts:int, savings_successful:int}
     */
    public function processSavingsDebits(?Tenant $tenant = null, ?Carbon $date = null): array
    {
        $date ??= today();
        $attempts = 0;
        $successful = 0;

        SavingsAccount::with(['autoDebitAccount', 'customer'])
            ->where('status', 'active')
            ->where('contribution_amount', '>', 0)
            ->whereNotNull('auto_debit_savings_account_id')
            ->whereIn('auto_debit_mode', [SavingsAccount::AUTO_DEBIT_AUTOMATIC, SavingsAccount::AUTO_DEBIT_MANUAL_ON_DEFAULT])
            ->when($tenant, fn ($query) => $query->where('tenant_id', $tenant->id))
            ->chunkById(100, function ($accounts) use ($date, &$attempts, &$successful): void {
                foreach ($accounts as $account) {
                    $amount = $this->savingsContributionDue($account, $date);

                    if ($amount <= 0) {
                        continue;
                    }

                    if ($account->auto_debit_mode === SavingsAccount::AUTO_DEBIT_MANUAL_ON_DEFAULT && $this->isSavingsDueToday($account, $date)) {
                        continue;
                    }

                    $attempts++;

                    try {
                        $this->debitSavingsContribution($account, $amount);
                        $successful++;
                    } catch (Throwable $exception) {
                        $account->forceFill([
                            'auto_debit_last_attempt_at' => now(),
                            'auto_debit_failure_reason' => $exception->getMessage(),
                        ])->save();
                    }
                }
            });

        return ['savings_attempts' => $attempts, 'savings_successful' => $successful];
    }

    private function debitLoan(Loan $loan, float $amount): void
    {
        DB::transaction(function () use ($loan, $amount): void {
            $source = SavingsAccount::whereKey($loan->auto_debit_savings_account_id)->lockForUpdate()->firstOrFail();
            $this->createSavingsDebit($source, $amount, 'loan_auto_debit', 'Automatic loan repayment debit for '.$loan->loan_number);

            $payment = $this->loanPayments->record($loan->fresh(), $amount, 'auto_debit', null, 'Automatic debit from '.$source->account_number);

            if ($payment->status === LoanPayment::STATUS_PENDING) {
                $this->loanPayments->approve($payment, null);
            }

            $loan->forceFill([
                'auto_debit_last_attempt_at' => now(),
                'auto_debit_failure_reason' => null,
            ])->save();
        });
    }

    private function debitSavingsContribution(SavingsAccount $target, float $amount): void
    {
        DB::transaction(function () use ($target, $amount): void {
            $source = SavingsAccount::whereKey($target->auto_debit_savings_account_id)->lockForUpdate()->firstOrFail();
            $target = SavingsAccount::whereKey($target->id)->lockForUpdate()->firstOrFail();

            if ((int) $source->id === (int) $target->id) {
                throw new \RuntimeException('Source and destination savings account cannot be the same.');
            }

            $this->createSavingsDebit($source, $amount, 'savings_auto_debit', 'Automatic savings contribution to '.$target->account_number);
            $this->createSavingsCredit($target, $amount, 'savings_auto_debit', 'Automatic savings contribution from '.$source->account_number);

            $target->forceFill([
                'auto_debit_last_attempt_at' => now(),
                'auto_debit_failure_reason' => null,
            ])->save();
        });
    }

    private function createSavingsDebit(SavingsAccount $account, float $amount, string $method, string $description): SavingsTransaction
    {
        if ($account->status !== 'active') {
            throw new \RuntimeException('Auto debit source account is not active.');
        }

        $before = (float) $account->balance;

        if ($amount > $before) {
            throw new \RuntimeException('Insufficient source savings balance.');
        }

        $after = round($before - $amount, 2);
        $transaction = SavingsTransaction::create([
            'tenant_id' => $account->tenant_id,
            'savings_account_id' => $account->id,
            'customer_id' => $account->customer_id,
            'type' => 'withdrawal',
            'amount' => $amount,
            'processing_fee_amount' => 0,
            'net_amount' => $amount,
            'balance_before' => $before,
            'balance_after' => $after,
            'reference' => NumberGenerator::reference('AUTODR'),
            'method' => $method,
            'status' => 'posted',
            'description' => $description,
            'transacted_at' => now(),
            'approved_at' => now(),
        ]);

        $account->update(['balance' => $after]);
        $this->accounting->recordSavings($transaction->fresh());

        return $transaction;
    }

    private function createSavingsCredit(SavingsAccount $account, float $amount, string $method, string $description): SavingsTransaction
    {
        $before = (float) $account->balance;
        $after = round($before + $amount, 2);
        $transaction = SavingsTransaction::create([
            'tenant_id' => $account->tenant_id,
            'savings_account_id' => $account->id,
            'customer_id' => $account->customer_id,
            'type' => 'deposit',
            'amount' => $amount,
            'processing_fee_amount' => 0,
            'net_amount' => $amount,
            'balance_before' => $before,
            'balance_after' => $after,
            'reference' => NumberGenerator::reference('AUTOCR'),
            'method' => $method,
            'status' => 'posted',
            'description' => $description,
            'transacted_at' => now(),
            'approved_at' => now(),
        ]);

        $account->updateBalanceAndCloseIfCompleted($after);
        $this->accounting->recordSavings($transaction->fresh());

        return $transaction;
    }

    private function savingsContributionDue(SavingsAccount $account, Carbon $date): float
    {
        if (! $account->start_date || (float) $account->contribution_amount <= 0) {
            return 0.0;
        }

        $periodsDue = $this->periodsDue($account, $date);
        if ($periodsDue <= 0) {
            return 0.0;
        }

        if ($account->contribution_periods) {
            $periodsDue = min($periodsDue, (int) $account->contribution_periods);
        }

        $expected = round($periodsDue * (float) $account->contribution_amount, 2);
        $postedDeposits = (float) $account->transactions()
            ->whereIn('type', ['deposit', 'transfer_in'])
            ->where('status', 'posted')
            ->sum('net_amount');

        $due = round($expected - $postedDeposits, 2);

        return $due > 0 ? min($due, (float) $account->contribution_amount) : 0.0;
    }

    private function isSavingsDueToday(SavingsAccount $account, Carbon $date): bool
    {
        if (! $account->start_date) {
            return false;
        }

        return $this->periodsDue($account, $date) > $this->periodsDue($account, $date->copy()->subDay());
    }

    private function periodsDue(SavingsAccount $account, Carbon $date): int
    {
        $start = $account->start_date?->copy()->startOfDay();
        if (! $start || $date->lt($start)) {
            return 0;
        }

        $days = (int) max(0, $start->diffInDays($date->copy()->startOfDay()));
        $months = (int) max(0, $start->diffInMonths($date));
        $years = (int) max(0, $start->diffInYears($date));

        return match ($account->contribution_frequency) {
            'daily' => $days + 1,
            'weekly' => intdiv($days, 7) + 1,
            'biweekly' => intdiv($days, 14) + 1,
            'quarterly' => max(1, intdiv($months, 3) + 1),
            'yearly' => $years + 1,
            default => $months + 1,
        };
    }
}
