<?php

namespace App\Services\Payments;

use App\Enums\PaymentStatus;
use App\Models\Application;
use App\Models\Payment;
use App\Models\PaymentEvent;
use App\Models\User;
use App\Services\Admissions\ReferenceNumberService;
use App\Services\AuditLogger;
use App\Services\Notifications\ApplicantNotifier;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * Application-level payment logic (PRD 19, 73).
 *
 * A payment is only ever settled here, and only from a server-side source:
 * a verified provider response, a validated webhook, or an explicit act by a
 * Finance Officer. Nothing the applicant's browser reports is trusted.
 */
class PaymentService
{
    public function __construct(
        protected PaymentGatewayManager $gateways,
        protected ReferenceNumberService $references,
        protected AuditLogger $audit,
        protected ApplicantNotifier $notifier,
    ) {}

    /**
     * Get the open payment for an application's fee, creating one if needed.
     * Re-uses an existing pending record so retries don't litter the table.
     */
    public function forApplication(Application $application, ?string $provider = null): Payment
    {
        $provider ??= config('kcu.payments.default_provider', 'manual');

        $existing = $application->payments()
            ->where('purpose', 'application_fee')
            ->whereIn('status', [PaymentStatus::Paid, PaymentStatus::Pending, PaymentStatus::Processing])
            ->latest()
            ->first();

        if ($existing?->isPaid()) {
            return $existing;
        }

        if ($existing) {
            if ($existing->provider !== $provider) {
                $existing->update(['provider' => $provider]);
            }

            return $existing;
        }

        $applicant = $application->applicant;

        return $application->payments()->create([
            'reference' => $this->references->paymentReference(),
            'provider' => $provider,
            'purpose' => 'application_fee',
            'amount' => $application->applicationFee(),
            'currency' => setting('default_currency', 'UGX'),
            'status' => PaymentStatus::Pending,
            'payer_name' => $applicant?->fullName(),
            'payer_phone' => $applicant?->phone,
            'payer_email' => $applicant?->user?->email,
        ]);
    }

    public function initialize(Payment $payment, array $context = []): PaymentInitialization
    {
        $result = $this->gateways->driver($payment->provider)->initializePayment($payment, $context);

        if ($result->successful) {
            $payment->update([
                'status' => PaymentStatus::Processing,
                'provider_reference' => $result->providerReference ?? $payment->provider_reference,
                'metadata' => array_merge((array) $payment->metadata, $result->metadata),
            ]);
        }

        return $result;
    }

    /**
     * Ask the provider what really happened and settle accordingly.
     * Safe to call repeatedly — an already-paid payment is left alone.
     */
    public function verify(Payment $payment): PaymentVerification
    {
        if ($payment->isPaid()) {
            return PaymentVerification::paid($payment->provider_reference, (float) $payment->amount);
        }

        $verification = $this->gateways->driver($payment->provider)->verifyPayment($payment);

        if ($verification->isPaid()) {
            $this->markPaid($payment, $verification->providerReference, $verification->raw);
        } elseif ($verification->status === PaymentStatus::Failed) {
            $this->markFailed($payment, $verification->message);
        }

        return $verification;
    }

    /**
     * Record and process an inbound webhook.
     *
     * Every event is persisted first, keyed on (provider, event_id), so a
     * provider retrying the same notification cannot settle a payment twice
     * (PRD 84: duplicate webhook protection).
     */
    public function handleWebhook(string $provider, Request $request): WebhookResult
    {
        $result = $this->gateways->driver($provider)->handleWebhook($request);

        if (! $result->valid) {
            Log::warning('Invalid payment webhook rejected', [
                'provider' => $provider,
                'error' => $result->error,
                'ip' => $request->ip(),
            ]);

            return $result;
        }

        $payment = Payment::query()->where('reference', $result->paymentReference)->first();

        try {
            $event = PaymentEvent::create([
                'payment_id' => $payment?->id,
                'provider' => $provider,
                'event_id' => $result->eventId ?: sha1($request->getContent()),
                'event_type' => $result->eventType,
                'payload' => $result->payload,
                'processed' => false,
            ]);
        } catch (UniqueConstraintViolationException) {
            // Already seen this exact event: acknowledge and do nothing.
            Log::info('Duplicate payment webhook ignored', [
                'provider' => $provider,
                'event_id' => $result->eventId,
            ]);

            return $result;
        }

        if (! $payment) {
            $event->update([
                'processed' => true,
                'processed_at' => now(),
                'processing_notes' => 'No payment matches reference '.$result->paymentReference,
            ]);

            return $result;
        }

        if ($result->status === PaymentStatus::Paid) {
            // Guard against a webhook claiming a smaller amount than billed.
            if ($result->amount !== null && $result->amount + 0.01 < (float) $payment->amount) {
                $event->update([
                    'processed' => true,
                    'processed_at' => now(),
                    'processing_notes' => 'Underpayment: expected '.$payment->amount.', got '.$result->amount,
                ]);

                return $result;
            }

            $this->markPaid($payment, $result->providerReference, $result->payload);
        } elseif ($result->status === PaymentStatus::Failed) {
            $this->markFailed($payment, 'Reported failed by provider webhook.');
        }

        $event->update(['processed' => true, 'processed_at' => now()]);

        return $result;
    }

    /** Settle a payment. Idempotent: a paid payment is never re-settled. */
    public function markPaid(Payment $payment, ?string $providerReference = null, array $raw = []): Payment
    {
        if ($payment->isPaid()) {
            return $payment;
        }

        DB::transaction(function () use ($payment, $providerReference, $raw) {
            $payment->update([
                'status' => PaymentStatus::Paid,
                'provider_reference' => $providerReference ?? $payment->provider_reference,
                'paid_at' => now(),
                'metadata' => array_merge((array) $payment->metadata, ['provider_response' => $raw]),
            ]);
        });

        $this->audit->log(AuditLogger::PAYMENT_RECEIVED, $payment, [], [
            'reference' => $payment->reference,
            'amount' => $payment->amount,
        ]);

        $this->notifier->paymentReceived($payment->fresh('application'));

        return $payment;
    }

    public function markFailed(Payment $payment, ?string $reason = null): Payment
    {
        if (! $payment->status->isOpen()) {
            return $payment;
        }

        $payment->update([
            'status' => PaymentStatus::Failed,
            'failed_at' => now(),
            'failure_reason' => $reason,
        ]);

        return $payment;
    }

    /**
     * Finance Officer confirms an off-platform payment (bank deposit).
     * This is the manual provider's only route to `paid`.
     */
    public function verifyManually(Payment $payment, User $actor, ?string $note = null): Payment
    {
        $payment->forceFill([
            'verified_by' => $actor->id,
            'verified_at' => now(),
            'metadata' => array_merge((array) $payment->metadata, array_filter(['verification_note' => $note])),
        ])->save();

        $this->markPaid($payment, $payment->provider_reference);

        $this->audit->log(AuditLogger::PAYMENT_VERIFIED, $payment, [], [
            'reference' => $payment->reference,
            'verified_by' => $actor->name,
            'note' => $note,
        ]);

        return $payment;
    }
}
