<?php

namespace App\Services;

use App\Models\Branch;
use App\Models\FinancialAccount;
use App\Models\Loan;
use App\Models\RemotePaymentSubmission;
use App\Models\User;
use App\Services\Authorization\FinancialMakerCheckerService;
use App\Services\RemotePayments\PaymentProviderRegistry;
use Carbon\CarbonInterface;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;

class RemotePaymentService
{
    public function __construct(
        private MoneyMath $money,
        private LoanPaymentService $payments,
        private SecureFileService $files,
        private PaymentProviderRegistry $providers,
        private FinancialMakerCheckerService $makerChecker,
    ) {}

    public function submit(
        Loan $loan,
        string $amount,
        string $channel,
        string $transactionReference,
        UploadedFile $proof,
        ?string $notes,
        string $submittedByType,
        ?User $user = null,
    ): RemotePaymentSubmission {
        $amountMinor=$this->money->toMinor($amount);
        if($amountMinor<=0) throw ValidationException::withMessages(['amount'=>'Payment amount must be greater than zero.']);
        if(!in_array($submittedByType,['customer','staff'],true)) throw ValidationException::withMessages(['submission'=>'Invalid submission source.']);
        if($submittedByType==='staff' && !$user) throw ValidationException::withMessages(['submission'=>'Staff submission requires an authenticated employee.']);

        $provider=$this->providers->resolve($channel);
        $reference=trim($transactionReference);
        if($reference==='') throw ValidationException::withMessages(['transaction_reference'=>'Transaction reference is required.']);
        if(RemotePaymentSubmission::query()->where('payment_channel',$channel)->where('transaction_reference',$reference)->exists()) {
            throw ValidationException::withMessages(['transaction_reference'=>'This transaction reference has already been submitted for this payment channel.']);
        }

        $proofPath=$this->files->store($proof,"remote-payments/{$loan->id}/proofs");

        try {
            return DB::transaction(function () use ($loan,$amountMinor,$channel,$reference,$proof,$proofPath,$notes,$submittedByType,$user,$provider): RemotePaymentSubmission {
                $lockedLoan=Loan::query()->with(['branch','customer'])->lockForUpdate()->findOrFail($loan->id);
                if($lockedLoan->status!=='active') throw ValidationException::withMessages(['loan_number'=>'Remote payments can only be submitted for an active loan.']);
                if($user && !$user->canAccessBranch($lockedLoan->branch_id)) throw ValidationException::withMessages(['loan_number'=>'You do not have access to this loan branch.']);
                if($submittedByType==='staff' && $user) Gate::forUser($user)->authorize('submitRemotePayment',$lockedLoan);

                $outstandingMinor=$this->money->toMinor((string)$lockedLoan->total_outstanding);
                if($outstandingMinor<=0) throw ValidationException::withMessages(['amount'=>'This loan has no outstanding amount to collect.']);
                if($amountMinor>$outstandingMinor) throw ValidationException::withMessages(['amount'=>'Submitted amount cannot exceed the current outstanding loan balance of '.$this->money->fromMinor($outstandingMinor).'.']);
                if(RemotePaymentSubmission::query()->where('payment_channel',$channel)->where('transaction_reference',$reference)->exists()) {
                    throw ValidationException::withMessages(['transaction_reference'=>'This transaction reference has already been submitted for this payment channel.']);
                }

                $submission=RemotePaymentSubmission::create([
                    'submission_number'=>$this->nextSubmissionNumber($lockedLoan),
                    'loan_id'=>$lockedLoan->id,
                    'customer_id'=>$lockedLoan->customer_id,
                    'branch_id'=>$lockedLoan->branch_id,
                    'amount'=>$this->money->fromMinor($amountMinor),
                    'payment_channel'=>$channel,
                    'transaction_reference'=>$reference,
                    'proof_path'=>$proofPath,
                    'proof_original_name'=>$proof->getClientOriginalName(),
                    'proof_mime_type'=>$proof->getMimeType(),
                    'proof_file_size'=>$proof->getSize(),
                    'notes'=>$notes,
                    'status'=>'pending_verification',
                    'submitted_by_type'=>$submittedByType,
                    'submitted_by_user_id'=>$user?->id,
                    'submitted_at'=>now(),
                    'provider_code'=>$provider->code(),
                ]);

                $providerResult=$provider->verify($submission);
                $submission->update([
                    'provider_status'=>$providerResult['status'],
                    'provider_message'=>$providerResult['message'] ?? null,
                ]);

                return $submission->fresh(['loan.customer','branch','submitter']);
            });
        } catch (\Throwable $exception) {
            $this->files->delete($proofPath);
            throw $exception;
        }
    }

    public function approve(RemotePaymentSubmission $submission, FinancialAccount $account, CarbonInterface $paymentDate, ?string $verificationNotes, User $user): RemotePaymentSubmission
    {
        return DB::transaction(function () use ($submission,$account,$paymentDate,$verificationNotes,$user): RemotePaymentSubmission {
            $locked=RemotePaymentSubmission::query()->with(['loan','branch.business'])->lockForUpdate()->findOrFail($submission->id);
            $this->assertPendingAndAccessible($locked,$user);
            if($locked->loan_payment_id) throw ValidationException::withMessages(['status'=>'This remote payment has already been posted.']);
            if($account->branch_id!==$locked->branch_id) throw ValidationException::withMessages(['financial_account_id'=>'The receiving account must belong to the remote payment branch.']);

            $payment=$this->payments->postVerifiedRemotePayment(
                $locked,
                $account,
                $paymentDate,
                $this->paymentMethodForChannel($locked->payment_channel),
                $user,
            );

            $locked->update([
                'status'=>'approved',
                'verified_by'=>$user->id,
                'verified_at'=>now(),
                'verification_notes'=>$verificationNotes,
                'financial_account_id'=>$account->id,
                'payment_date'=>$paymentDate,
                'loan_payment_id'=>$payment->id,
            ]);

            return $locked->fresh(['loan.customer','branch','submitter','verifier','financialAccount','loanPayment']);
        });
    }

    public function reject(RemotePaymentSubmission $submission, string $reason, User $user): RemotePaymentSubmission
    {
        return DB::transaction(function () use ($submission,$reason,$user): RemotePaymentSubmission {
            $locked=RemotePaymentSubmission::query()->with('branch.business')->lockForUpdate()->findOrFail($submission->id);
            $this->assertPendingAndAccessible($locked,$user);
            $locked->update([
                'status'=>'rejected',
                'verified_by'=>$user->id,
                'verified_at'=>now(),
                'verification_notes'=>$reason,
            ]);
            return $locked->fresh(['loan.customer','branch','submitter','verifier']);
        });
    }

    private function assertPendingAndAccessible(RemotePaymentSubmission $submission, User $user): void
    {
        if(!$user->canAccessBranch($submission->branch_id)) throw ValidationException::withMessages(['status'=>'You do not have access to this remote payment branch.']);
        Gate::forUser($user)->authorize('verify',$submission);
        $this->makerChecker->assertRemotePaymentVerificationSeparated($submission,$user);
        if($submission->status!=='pending_verification') throw ValidationException::withMessages(['status'=>'Only pending remote payments can be verified.']);
    }

    private function paymentMethodForChannel(string $channel): string
    {
        return match($channel) {
            'mpesa','airtel_money','mixx_yas'=>'mobile_money',
            'bank'=>'bank',
            default=>'other',
        };
    }

    private function nextSubmissionNumber(Loan $loan): string
    {
        $year=(int)now()->format('Y');
        DB::table('number_sequences')->insertOrIgnore(['type'=>'remote_payment','branch_id'=>$loan->branch_id,'year'=>$year,'current_value'=>0,'created_at'=>now(),'updated_at'=>now()]);
        $row=DB::table('number_sequences')->where('type','remote_payment')->where('branch_id',$loan->branch_id)->where('year',$year)->lockForUpdate()->first();
        $sequence=((int)$row->current_value)+1;
        DB::table('number_sequences')->where('id',$row->id)->update(['current_value'=>$sequence,'updated_at'=>now()]);
        $branch=$loan->relationLoaded('branch') ? $loan->branch : Branch::findOrFail($loan->branch_id);
        $tag=strtoupper(preg_replace('/[^A-Z0-9]/','',str_replace('BR-','',$branch->code)) ?: (string)$loan->branch_id);
        return 'RMP-'.$tag.'-'.$year.'-'.str_pad((string)$sequence,6,'0',STR_PAD_LEFT);
    }
}
