<?php

namespace App\Http\Controllers;

use App\Models\Branch;
use App\Models\Collateral;
use App\Models\FinancialAccount;
use App\Models\Guarantor;
use App\Models\Loan;
use App\Models\LoanProduct;
use App\Models\LoanTopUp;
use App\Rules\SameBranchAs;
use App\Services\AuditService;
use App\Services\LoanTopUpService;
use App\Services\MoneyMath;
use App\Services\RepaymentScheduleGenerator;
use Carbon\CarbonImmutable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use InvalidArgumentException;

class LoanTopUpController extends Controller
{
    public function index(Request $request, LoanTopUpService $service): View
    {
        $branchIds = $this->allowedBranchIds($request);
        $search = trim((string) $request->string('search'));

        $active = Loan::query()
            ->with(['customer','branch','product'])
            ->whereIn('branch_id',$branchIds)
            ->where('status','active')
            ->when($search !== '', fn ($query) => $query->where(function ($sub) use ($search): void {
                $sub->where('loan_number','like',"%{$search}%")
                    ->orWhereHas('customer', fn ($customer) => $customer->where('customer_number','like',"%{$search}%")
                        ->orWhere('first_name','like',"%{$search}%")->orWhere('last_name','like',"%{$search}%")->orWhere('phone','like',"%{$search}%"));
            }))
            ->latest('id')->paginate(15,['*'],'active_page')->withQueryString();
        $active->getCollection()->each(fn (Loan $loan) => $loan->setAttribute('top_up_eligibility',$service->eligibility($loan)));

        $pending = Loan::query()
            ->with(['customer','branch','product','topUpSourceLoan'])
            ->whereIn('branch_id',$branchIds)
            ->whereNotNull('top_up_source_loan_id')
            ->whereNotIn('status',['active','rejected','closed_through_top_up','completed'])
            ->latest('id')->paginate(15,['*'],'pending_page')->withQueryString();

        $history = LoanTopUp::query()
            ->with(['previousLoan.customer','newLoan','branch','financialAccount','executor'])
            ->whereIn('branch_id',$branchIds)
            ->when($search !== '', fn ($query) => $query->where(function ($sub) use ($search): void {
                $sub->where('top_up_number','like',"%{$search}%")
                    ->orWhereHas('previousLoan',fn ($loan)=>$loan->where('loan_number','like',"%{$search}%"))
                    ->orWhereHas('newLoan',fn ($loan)=>$loan->where('loan_number','like',"%{$search}%"));
            }))
            ->latest('id')->paginate(15,['*'],'history_page')->withQueryString();

        return view('loan-top-ups.index',compact('active','pending','history'));
    }

    public function create(Request $request, Loan $loan, LoanTopUpService $service): View
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        abort_unless($loan->status === 'active',409,'Only active loans can be topped up.');
        $loan->load(['customer','branch.business','product','collaterals']);
        $businessId = $loan->branch->business_id;

        return view('loan-top-ups.create',[
            'loan'=>$loan,
            'eligibility'=>$service->eligibility($loan),
            'products'=>LoanProduct::where('business_id',$businessId)->where('status','active')->orderBy('name')->get(),
            'collaterals'=>Collateral::with('category')->where('branch_id',$loan->branch_id)->where('customer_id',$loan->customer_id)
                ->whereNotIn('status',['released','sold','returned'])->orderBy('code')->get(),
            'guarantors'=>Guarantor::where('customer_id',$loan->customer_id)->where('status','active')->orderBy('full_name')->get(),
        ]);
    }

    public function preview(Request $request, Loan $loan, LoanTopUpService $service, RepaymentScheduleGenerator $generator, MoneyMath $money): JsonResponse
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        abort_unless($loan->status === 'active',409,'Only active loans can be topped up.');
        $data = $request->validate([
            'loan_product_id'=>['required','integer','exists:loan_products,id'],
            'principal'=>['required','decimal:0,2','min:0.01'],
            'schedule_start_date'=>['required','date'],
            'custom_dates_text'=>['nullable','string','max:5000'],
        ]);
        $product = LoanProduct::findOrFail($data['loan_product_id']);
        abort_unless($product->business_id === $loan->branch->business_id && $product->status === 'active',422,'Loan product is unavailable for this branch.');
        try {
            $schedule = $generator->generate($product,(string)$data['principal'],CarbonImmutable::parse($data['schedule_start_date']),$this->customDates($data['custom_dates_text'] ?? null));
        } catch (InvalidArgumentException $e) {
            return response()->json(['message'=>$e->getMessage()],422);
        }
        $principal = $money->toMinor((string)$data['principal']);
        $outstanding = $money->toMinor((string)$loan->total_outstanding);
        $schedule['top_up'] = [
            'previous_loan'=>$loan->loan_number,
            'previous_outstanding'=>$money->fromMinor($outstanding),
            'cash_given_to_customer'=>$principal > $outstanding ? $money->fromMinor($principal-$outstanding) : '0.00',
            'principal_exceeds_outstanding'=>$principal > $outstanding,
            'eligibility'=>$service->eligibility($loan),
        ];
        return response()->json($schedule);
    }

    public function store(Request $request, Loan $loan, LoanTopUpService $service): RedirectResponse
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        $data = $this->validatedApplication($request);
        $product = $this->assertApplicationReferences($loan,$data);
        $newLoan = $service->initiate($loan,$data,$product,$request->user());
        AuditService::record('loan_top_up.application_created',$newLoan,[],[
            'previous_loan_id'=>$loan->id,'new_loan_id'=>$newLoan->id,
            'eligibility_percentage'=>(string)$newLoan->top_up_paid_percentage_at_application,
            'threshold'=>(string)$newLoan->top_up_eligibility_percentage,
            'override_used'=>(bool)$newLoan->top_up_override_used,
            'override_reason'=>$newLoan->top_up_override_reason,
        ],$loan->branch_id);
        return redirect()->route('loans.show',$newLoan)->with('success','Top-up replacement loan created as a normal loan application. Complete contract and approval before executing the top-up.');
    }

    public function executeForm(Request $request, Loan $loan): View
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        abort_unless($loan->top_up_source_loan_id && $loan->status === 'ready_for_disbursement',409,'Only a top-up replacement loan that is Ready for Disbursement can be executed.');
        $loan->load(['customer','branch.business','product','topUpSourceLoan.customer']);
        abort_unless($loan->topUpSourceLoan?->status === 'active',409,'The previous loan is no longer active.');
        $accounts = FinancialAccount::where('branch_id',$loan->branch_id)->where('status','active')->orderBy('name')->get();
        return view('loan-top-ups.execute',compact('loan','accounts'));
    }

    public function execute(Request $request, Loan $loan, LoanTopUpService $service): RedirectResponse
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        $data = $request->validate([
            'financial_account_id'=>['required','integer','exists:financial_accounts,id'],
            'date'=>['required','date'],
            'reference'=>['nullable','string','max:150'],
            'notes'=>['nullable','string','max:2000'],
        ]);
        $account = FinancialAccount::findOrFail($data['financial_account_id']);
        $this->assertBranchAccess($request,$account->branch_id);
        $request->validate([
            'financial_account_id'=>[
                new SameBranchAs('financial_accounts',(int)$loan->branch_id,'The top-up funding account must belong to the loan branch.'),
            ],
        ]);
        $topUp = $service->execute($loan,$account,CarbonImmutable::parse($data['date']),$data['reference'] ?? null,$data['notes'] ?? null,$request->user());
        AuditService::record('loan_top_up.executed',$topUp,[],[
            'previous_loan_id'=>$topUp->previous_loan_id,'new_loan_id'=>$topUp->new_loan_id,
            'previous_outstanding'=>(string)$topUp->previous_outstanding,'amount_settled_internally'=>(string)$topUp->amount_settled_internally,
            'new_principal'=>(string)$topUp->new_principal,'cash_given_to_customer'=>(string)$topUp->cash_given_to_customer,
            'financial_account_id'=>$topUp->financial_account_id,
        ],$topUp->branch_id);
        return redirect()->route('loan-top-ups.show',$topUp)->with('success','Loan top-up executed successfully. The previous loan is closed through top-up and the replacement loan is active.');
    }

    public function show(Request $request, LoanTopUp $topUp): View
    {
        $this->assertBranchAccess($request,$topUp->branch_id);
        $topUp->load(['previousLoan.customer','previousLoan.product','newLoan.customer','newLoan.product','branch.business','financialAccount','executor','accountTransaction']);
        return view('loan-top-ups.show',compact('topUp'));
    }

    private function validatedApplication(Request $request): array
    {
        $data = $request->validate([
            'loan_product_id'=>['required','integer','exists:loan_products,id'],
            'principal'=>['required','decimal:0,2','min:0.01'],
            'purpose'=>['required','string','max:4000'],
            'schedule_start_date'=>['required','date'],
            'custom_dates_text'=>['nullable','string','max:5000'],
            'collateral_ids'=>['nullable','array'],'collateral_ids.*'=>['integer','exists:collaterals,id'],
            'guarantor_id'=>['nullable','integer','exists:guarantors,id'],
            'override_eligibility'=>['nullable','boolean'],
            'override_reason'=>['nullable','string','max:2000'],
        ]);
        $data['custom_repayment_dates'] = $this->customDates($data['custom_dates_text'] ?? null);
        unset($data['custom_dates_text']);
        return $data;
    }

    private function assertApplicationReferences(Loan $loan, array $data): LoanProduct
    {
        $product = LoanProduct::findOrFail($data['loan_product_id']);
        abort_unless($product->business_id === $loan->branch->business_id && $product->status === 'active',422,'Loan product is unavailable for this branch.');
        $collateralIds = array_map('intval',$data['collateral_ids'] ?? []);
        $valid = Collateral::whereIn('id',$collateralIds)->where('customer_id',$loan->customer_id)->where('branch_id',$loan->branch_id)
            ->whereNotIn('status',['released','sold','returned'])->count();
        abort_unless($valid === count($collateralIds),422,'Selected collateral must belong to this customer and branch and remain available.');
        if ($product->collateral_required && empty($collateralIds)) throw ValidationException::withMessages(['collateral_ids'=>'This loan product requires collateral.']);
        if (! empty($data['guarantor_id'])) {
            $guarantor = Guarantor::findOrFail($data['guarantor_id']);
            abort_unless($guarantor->customer_id === $loan->customer_id && $guarantor->status === 'active',422,'Guarantor must be active and belong to this customer.');
        } elseif ($product->guarantor_required) {
            throw ValidationException::withMessages(['guarantor_id'=>'This loan product requires a guarantor.']);
        }
        return $product;
    }

    /** @return list<string> */
    private function customDates(?string $text): array
    {
        if (blank($text)) return [];
        return collect(preg_split('/\r\n|\r|\n|,/',(string)$text))->map(fn($date)=>trim((string)$date))->filter()->values()->all();
    }

    private function allowedBranchIds(Request $request)
    {
        return collect(app(\App\Services\BranchAccessService::class)->accessibleBranchIds($request->user()));
    }

    private function assertBranchAccess(Request $request, int $branchId): void
    {
        abort_unless($request->user()->canAccessBranch($branchId),403);
    }
}
