<?php

namespace App\Services;

use App\Models\BillPayment;
use App\Models\Expense;
use App\Models\Income;
use App\Models\InvestmentAccount;
use App\Models\InvestmentTransaction;
use App\Models\Loan;
use App\Models\LoanPayment;
use App\Models\LoanRepaymentSchedule;
use App\Models\SavingsAccount;
use App\Models\SavingsTransaction;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

class TransactionCancellationService
{
    public function __construct(
        private readonly AccountingService $accounting,
        private readonly LoanCalculator $loanCalculator,
    ) {
    }

    public function cancelSavings(SavingsTransaction $transaction, string $reason, int $userId): SavingsTransaction
    {
        return DB::transaction(function () use ($transaction, $reason, $userId): SavingsTransaction {
            $lockedTransaction = SavingsTransaction::whereKey($transaction->id)->lockForUpdate()->firstOrFail();
            $this->guardPostedStatus($lockedTransaction->status, 'Only posted savings transactions can be canceled.');

            $account = SavingsAccount::whereKey($lockedTransaction->savings_account_id)->lockForUpdate()->firstOrFail();
            $currentBalance = (float) $account->balance;
            $grossAmount = (float) $lockedTransaction->amount;
            $netAmount = (float) ($lockedTransaction->net_amount ?? 0);
            $processingFee = (float) ($lockedTransaction->processing_fee_amount ?? 0);

            if ($netAmount <= 0 && $processingFee < $grossAmount) {
                $netAmount = round($grossAmount - $processingFee, 2);
            }

            if ($lockedTransaction->type === 'deposit') {
                if ($currentBalance < $netAmount) {
                    throw new InvalidArgumentException('This deposit cannot be canceled because the current savings balance is lower than the amount to reverse.');
                }

                $newBalance = round($currentBalance - $netAmount, 2);
            } else {
                $newBalance = round($currentBalance + $grossAmount, 2);
            }

            $account->forceFill(['balance' => $newBalance])->saveQuietly();

            if ($lockedTransaction->type === 'deposit' && $account->status === 'closed') {
                $completionAmount = $account->completionAmount();

                if ($completionAmount <= 0 || $newBalance < $completionAmount) {
                    $account->forceFill(['status' => 'active'])->saveQuietly();
                }
            }

            $lockedTransaction->update($this->cancelPayload($reason, $userId));
            $this->accounting->reverseSource($lockedTransaction->fresh(), $reason);

            ActivityLogger::log('savings_transaction_canceled', 'Savings transaction '.$lockedTransaction->reference.' canceled.', $lockedTransaction);

            return $lockedTransaction->fresh(['customer', 'account', 'creator', 'approver', 'canceler']);
        });
    }

    public function cancelLoanPayment(LoanPayment $payment, string $reason, int $userId): LoanPayment
    {
        return DB::transaction(function () use ($payment, $reason, $userId): LoanPayment {
            $lockedPayment = LoanPayment::whereKey($payment->id)->lockForUpdate()->firstOrFail();
            $this->guardPostedStatus($lockedPayment->status ?? LoanPayment::STATUS_POSTED, 'Only posted loan repayments can be canceled.');

            $loan = Loan::whereKey($lockedPayment->loan_id)->lockForUpdate()->firstOrFail();

            if ($lockedPayment->allocations()->exists()) {
                $this->reverseLoanPaymentAllocations($lockedPayment);
            } else {
                $this->reverseLoanPaymentFromCurrentSchedules($loan, (float) $lockedPayment->amount);
            }

            $lockedPayment->update($this->cancelPayload($reason, $userId));
            $this->syncLoanAfterPaymentCancellation($loan);
            $this->accounting->reverseSource($lockedPayment->fresh(), $reason);

            ActivityLogger::log('loan_repayment_canceled', 'Loan repayment '.$lockedPayment->reference.' canceled.', $lockedPayment);

            return $lockedPayment->fresh(['loan', 'customer', 'receiver', 'canceler']);
        });
    }

    public function cancelInvestment(InvestmentTransaction $transaction, string $reason, int $userId): InvestmentTransaction
    {
        return DB::transaction(function () use ($transaction, $reason, $userId): InvestmentTransaction {
            $lockedTransaction = InvestmentTransaction::whereKey($transaction->id)->lockForUpdate()->firstOrFail();
            $this->guardPostedStatus($lockedTransaction->status, 'Only posted investment transactions can be canceled.');

            $account = InvestmentAccount::with('product')->whereKey($lockedTransaction->investment_account_id)->lockForUpdate()->firstOrFail();
            $amount = (float) $lockedTransaction->amount;
            $balance = (float) $account->balance;
            $updates = [];

            if ($lockedTransaction->type === 'deposit') {
                if ($balance < $amount) {
                    throw new InvalidArgumentException('This investment deposit cannot be canceled because the current balance is lower than the amount to reverse.');
                }

                $updates['balance'] = round($balance - $amount, 2);
                $updates['amount'] = max(0, round((float) $account->amount - $amount, 2));
                $updates['expected_return'] = max(0, round((float) $account->expected_return - $this->expectedInvestmentReturn($account, $amount), 2));
            } elseif ($lockedTransaction->type === 'return') {
                if ($balance < $amount) {
                    throw new InvalidArgumentException('This investment return cannot be canceled because the current balance is lower than the amount to reverse.');
                }

                $updates['balance'] = round($balance - $amount, 2);
                $updates['return_paid'] = max(0, round((float) $account->return_paid - $amount, 2));
            } else {
                $updates['balance'] = round($balance + $amount, 2);

                if (in_array($account->status, ['closed', 'matured'], true)) {
                    $updates['status'] = 'active';
                    $updates['closed_at'] = null;
                }
            }

            $account->forceFill($updates)->saveQuietly();
            $lockedTransaction->update($this->cancelPayload($reason, $userId));
            $this->accounting->reverseSource($lockedTransaction->fresh(), $reason);

            ActivityLogger::log('investment_transaction_canceled', 'Investment transaction '.$lockedTransaction->reference.' canceled.', $lockedTransaction);

            return $lockedTransaction->fresh(['customer', 'account', 'creator', 'approver', 'canceler']);
        });
    }

    public function cancelExpense(Expense $expense, string $reason, int $userId): Expense
    {
        return DB::transaction(function () use ($expense, $reason, $userId): Expense {
            $lockedExpense = Expense::whereKey($expense->id)->lockForUpdate()->firstOrFail();

            if ($lockedExpense->status !== Expense::STATUS_APPROVED) {
                throw new InvalidArgumentException('Only approved expenses can be canceled. Pending expenses should be rejected instead.');
            }

            $history = $lockedExpense->approval_history ?: [];
            $history[] = [
                'action' => 'canceled',
                'user_id' => $userId,
                'user_name' => auth()->user()?->name,
                'canceled_at' => now()->toDateTimeString(),
                'reason' => $reason,
            ];

            $lockedExpense->update(array_merge($this->cancelPayload($reason, $userId), [
                'status' => Expense::STATUS_CANCELED,
                'approval_history' => $history,
            ]));

            $this->accounting->reverseSource($lockedExpense->fresh(), $reason);

            ActivityLogger::log('expense_canceled', 'Expense '.$lockedExpense->reference.' canceled.', $lockedExpense);

            return $lockedExpense->fresh(['recordedBy', 'approvedBy', 'canceledBy']);
        });
    }

    public function cancelBillPayment(BillPayment $billPayment, string $reason, int $userId): BillPayment
    {
        return DB::transaction(function () use ($billPayment, $reason, $userId): BillPayment {
            $lockedPayment = BillPayment::whereKey($billPayment->id)->lockForUpdate()->firstOrFail();

            if ($lockedPayment->status === BillPayment::STATUS_REVERSED) {
                throw new InvalidArgumentException('This bill payment has already been reversed.');
            }

            $lockedPayment->update(array_merge($this->cancelPayload($reason, $userId), [
                'status' => BillPayment::STATUS_REVERSED,
                'processed_at' => now(),
            ]));

            ActivityLogger::log('bill_payment_canceled', 'Bill payment '.$lockedPayment->reference.' canceled.', $lockedPayment);

            return $lockedPayment->fresh(['customer', 'recordedBy', 'canceledBy']);
        });
    }

    public function cancelIncome(Income $income, string $reason, int $userId): Income
    {
        return DB::transaction(function () use ($income, $reason, $userId): Income {
            $lockedIncome = Income::whereKey($income->id)->lockForUpdate()->firstOrFail();
            $this->guardPostedStatus($lockedIncome->status ?? Income::STATUS_POSTED, 'Only posted income records can be canceled.');

            $lockedIncome->update($this->cancelPayload($reason, $userId));
            $this->accounting->reverseSource($lockedIncome->fresh(), $reason);

            ActivityLogger::log('income_canceled', 'Income '.$lockedIncome->reference.' canceled.', $lockedIncome);

            return $lockedIncome->fresh(['recordedBy', 'canceledBy']);
        });
    }

    private function reverseLoanPaymentAllocations(LoanPayment $payment): void
    {
        $allocations = $payment->allocations()->with('schedule')->get();

        foreach ($allocations as $allocation) {
            $schedule = LoanRepaymentSchedule::withoutGlobalScope('current_schedule')
                ->whereKey($allocation->loan_repayment_schedule_id)
                ->lockForUpdate()
                ->first();

            if (! $schedule) {
                continue;
            }

            $this->reduceSchedulePaidAmount($schedule, (float) $allocation->total_amount);
        }
    }

    private function reverseLoanPaymentFromCurrentSchedules(Loan $loan, float $amount): void
    {
        $remaining = $amount;
        $schedules = $loan->schedules()
            ->where('paid_amount', '>', 0)
            ->orderByDesc('installment_no')
            ->lockForUpdate()
            ->get();

        foreach ($schedules as $schedule) {
            if ($remaining <= 0) {
                break;
            }

            $reversal = min($remaining, (float) $schedule->paid_amount);
            $this->reduceSchedulePaidAmount($schedule, $reversal);
            $remaining = round($remaining - $reversal, 2);
        }

        if ($remaining > 0.01) {
            throw new InvalidArgumentException('This older repayment cannot be canceled automatically because its detailed schedule allocation was not recorded.');
        }
    }

    private function reduceSchedulePaidAmount(LoanRepaymentSchedule $schedule, float $amount): void
    {
        $newPaid = round(max(0, (float) $schedule->paid_amount - $amount), 2);
        $totalDue = (float) $schedule->total_due;

        $status = match (true) {
            $newPaid >= $totalDue && $totalDue > 0 => 'paid',
            $newPaid > 0 => 'partial',
            $schedule->due_date && $schedule->due_date->lt(today()) => 'overdue',
            default => 'pending',
        };

        $schedule->forceFill([
            'paid_amount' => $newPaid,
            'status' => $status,
        ])->saveQuietly();
    }

    private function syncLoanAfterPaymentCancellation(Loan $loan): void
    {
        $loan = $loan->fresh();
        $balance = $this->loanCalculator->outstandingBalance($loan);
        $status = $loan->status === 'completed' && $balance > 0 ? 'active' : $loan->status;

        $loan->forceFill([
            'balance' => $balance,
            'status' => $status,
        ])->saveQuietly();
    }

    private function cancelPayload(string $reason, int $userId): array
    {
        return [
            'status' => 'canceled',
            'canceled_by' => $userId,
            'canceled_at' => now(),
            'canceled_reason' => $reason,
        ];
    }

    private function guardPostedStatus(?string $status, string $message): void
    {
        if (($status ?: 'posted') !== 'posted') {
            throw new InvalidArgumentException($message);
        }
    }

    private function expectedInvestmentReturn(InvestmentAccount $account, float $amount): float
    {
        $rate = (float) $account->interest_rate;

        if ($rate <= 0 || $amount <= 0) {
            return 0.0;
        }

        $years = match ($account->term_unit) {
            'days' => (int) $account->term_value / 365,
            'weeks' => ((int) $account->term_value * 7) / 365,
            'years' => (int) $account->term_value,
            default => (int) $account->term_value / 12,
        };

        if (($account->product?->return_type ?? 'fixed') === 'compound') {
            return round($amount * ((1 + ($rate / 100)) ** max($years, 0)) - $amount, 2);
        }

        return round($amount * ($rate / 100) * max($years, 0), 2);
    }
}
