<?php

namespace App\Services;

use App\Enums\ServicePackageStatusEnum;
use App\Enums\NotificationTypeEnum;
use App\Enums\PaymentSplitStatusEnum;
use App\Enums\PaymentStatusEnum;
use App\Exceptions\PaymentFailedException;
use App\Exceptions\PaymentException;
use App\Jobs\StartScheduleJob;
use App\Models\ServicePackage;
use App\Models\ClientPaymentMethod;
use App\Models\Payment;
use App\Models\PaymentSplit;
use App\Models\Schedule;
use App\Services\Pagarme\PagarmePaymentService;
use Carbon\Carbon;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class PaymentService
{
    public function __construct(
        private readonly PagarmePaymentService $pagarmePaymentService,
    ) {}

    public function getAll(): Collection
    {
        return Payment::query()
            ->with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule'])
            ->orderBy('created_at', 'desc')
            ->get();
    }

    public function findById(int $id): ?Payment
    {
        return Payment::query()
            ->with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule'])
            ->find($id);
    }

    public function create(array $data): Payment
    {
        return Payment::create($data);
    }

    public function update(int $id, array $data): ?Payment
    {
        $model = $this->findById($id);

        if (! $model) {
            return null;
        }

        $model->update($data);

        return $model->fresh();
    }

    public function delete(int $id): bool
    {
        $model = $this->findById($id);

        if (! $model) {
            return false;
        }

        return $model->delete();
    }

    //

    public function platformFees(): array
    {
        return $this->pagarmePaymentService->platformFeeRates();
    }

    public function payServicePackage(
        int    $servicePackageId,
        string $paymentMethod,
        ?int   $clientPaymentMethodId = null,
        array  $options               = [],
    ): Payment {
        $userId = (int) Auth::id();

        if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
            throw new PaymentException;
        }

        $paymentData = DB::transaction(function () use (
            $servicePackageId,
            $userId,
            $paymentMethod,
            $clientPaymentMethodId,
            $options,
        ): array {
            $servicePackage = ServicePackage::query()
                ->lockForUpdate()
                ->with(['client', 'provider', 'items.schedule.client', 'items.schedule.provider', 'items.schedule.customSchedule.serviceType'])
                ->findOrFail($servicePackageId);

            if ($servicePackage->client?->user_id !== $userId) {
                throw new AuthorizationException;
            }

            $schedules = $this->activePackageSchedules($servicePackage);

            $this->validateServicePackageForPayment($servicePackage, $schedules);

            $existingPayment = Payment::query()
                ->where('service_package_id', $servicePackage->id)
                ->whereIn('status', [
                    PaymentStatusEnum::PENDING->value,
                    PaymentStatusEnum::PROCESSING->value,
                    PaymentStatusEnum::AUTHORIZED->value,
                    PaymentStatusEnum::PAID->value,
                ])
                ->latest('id')
                ->first();

            if ($existingPayment) {
                if ($this->isExpiredPixPayment($existingPayment)) {
                    $existingPayment->forceFill([
                        'status'          => PaymentStatusEnum::FAILED,
                        'failed_at'       => now(),
                        'failure_message' => 'Pagamento Pix expirado.',
                    ])->save();

                    PaymentSplit::query()
                        ->where('payment_id', $existingPayment->id)
                        ->update(['status' => PaymentSplitStatusEnum::FAILED]);
                } elseif ($this->isStaleIncompleteGatewayPayment($existingPayment)) {
                    if ($existingPayment->payment_method !== $paymentMethod) {
                        throw new PaymentException;
                    }

                    [, $cardId] = $this->resolveCard(
                        clientId:              $servicePackage->client_id,
                        paymentMethod:         $paymentMethod,
                        clientPaymentMethodId: $clientPaymentMethodId ?? $existingPayment->client_payment_method_id,
                        cardId:                data_get($options, 'card_id'),
                    );

                    $this->servicePackagePaymentTotals($schedules, $paymentMethod);

                    return [
                        'payment'   => $existingPayment,
                        'schedules' => $schedules,
                        'cardId'    => $cardId,
                    ];
                } else {
                    if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
                        throw new PaymentException;
                    }

                    return ['existing' => $existingPayment];
                }
            }

            if ($servicePackage->status !== ServicePackageStatusEnum::OPEN) {
                throw new PaymentException;
            }

            [$clientPaymentMethod, $cardId] = $this->resolveCard(
                clientId:              $servicePackage->client_id,
                paymentMethod:         $paymentMethod,
                clientPaymentMethodId: $clientPaymentMethodId,
                cardId:                data_get($options, 'card_id'),
            );

            $totals = $this->servicePackagePaymentTotals($schedules, $paymentMethod);

            $payment = Payment::create([
                'schedule_id'              => null,
                'service_package_id'       => $servicePackage->id,
                'client_id'                => $servicePackage->client_id,
                'provider_id'              => $servicePackage->provider_id,
                'client_payment_method_id' => $paymentMethod === 'credit_card' ? $clientPaymentMethod?->id : null,
                'gateway_provider'         => 'pagarme',
                'gateway_code'             => 'payment-'.(string) Str::uuid(),
                'payment_method'           => $paymentMethod,
                'status'                   => PaymentStatusEnum::PENDING,
                'gross_amount'             => data_get($totals, 'gross_amount'),
                'gateway_fee_amount'       => 0,
                'platform_fee_amount'      => data_get($totals, 'platform_fee_amount'),
                'net_amount'               => data_get($totals, 'gross_amount'),
                'currency'                 => 'BRL',
                'installments'             => 1,
                'expires_at'               => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,

                'metadata' => [
                    'service_package_id' => (string) $servicePackage->id,
                    'schedule_ids'       => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
                    'service_amount'     => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
                    'platform_fee'       => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
                ],
            ]);

            PaymentSplit::create([
                'payment_id'                        => $payment->id,
                'provider_id'                       => $servicePackage->provider_id,
                'gateway_provider'                  => 'pagarme',
                'gateway_transfer_target_reference' => $servicePackage->provider->recipient_id,
                'gateway_transfer_target_label'     => 'recipient',
                'status'                            => PaymentSplitStatusEnum::PENDING,
                'gross_amount'                      => data_get($totals, 'service_amount'),
                'gateway_fee_amount'                => 0,
                'net_amount'                        => data_get($totals, 'service_amount'),

                'metadata' => [
                    'service_package_id' => (string) $servicePackage->id,
                    'schedule_ids'       => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
                ],
            ]);

            return compact('payment', 'schedules', 'cardId');
        });

        if (data_get($paymentData, 'existing')) {
            $existingPayment = data_get($paymentData, 'existing');

            $this->syncPaymentTargets($existingPayment);

            return $existingPayment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
        }

        /** @var Payment $payment */
        $payment = data_get($paymentData, 'payment');

        /** @var SupportCollection $schedules */
        $schedules = data_get($paymentData, 'schedules');

        try {
            $schedules->first()->ensureCustomerPhone(data_get($options, 'phone'));

            $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
                payment:       $payment,
                schedules:     $schedules,
                paymentMethod: $paymentMethod,
                cardId:        data_get($paymentData, 'cardId'),
                options:       $options,
            );
        } catch (\Throwable $e) {
            $this->failPayment($payment, $e->getMessage());

            throw $e;
        }

        $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);

        $this->syncPaymentTargets($payment);

        if ($payment->status === PaymentStatusEnum::FAILED) {
            throw new PaymentFailedException;
        }

        return $payment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
    }

    //

    public function getOrCreateServicePackagePixPayment(ServicePackage $servicePackage): Payment
    {
        $userId = (int) Auth::id();

        if ($servicePackage->client?->user_id !== $userId) {
            throw new AuthorizationException;
        }

        $existingPayment = Payment::query()
            ->where('service_package_id', $servicePackage->id)
            ->where('payment_method', 'pix')
            ->whereIn('status', [
                PaymentStatusEnum::PENDING->value,
                PaymentStatusEnum::PROCESSING->value,
                PaymentStatusEnum::AUTHORIZED->value,
                PaymentStatusEnum::PAID->value,
            ])
            ->latest('id')
            ->first();

        if ($existingPayment && $this->isExpiredPixPayment($existingPayment)) {
            $existingPayment->forceFill([
                'status'          => PaymentStatusEnum::FAILED,
                'failed_at'       => Carbon::now(),
                'failure_message' => 'Pagamento Pix expirado.',
            ])->save();

            PaymentSplit::query()
                ->where('payment_id', $existingPayment->id)
                ->update(['status' => PaymentSplitStatusEnum::FAILED]);

            $existingPayment = null;
        }

        if ($existingPayment) {
            if ($this->isIncompleteGatewayPayment($existingPayment)) {
                $existingPayment->forceFill([
                    'status'          => PaymentStatusEnum::FAILED,
                    'failed_at'       => Carbon::now(),
                    'failure_message' => 'Pagamento pendente sem retorno do gateway.',
                ])->save();

                PaymentSplit::query()
                    ->where('payment_id', $existingPayment->id)
                    ->update(['status' => PaymentSplitStatusEnum::FAILED]);
            } else {
                $this->syncPaymentTargets($existingPayment);

                return $existingPayment;
            }
        }

        $paymentData = DB::transaction(function () use ($servicePackage, $userId): array {
            $servicePackage = ServicePackage::query()
                ->lockForUpdate()
                ->with(['client', 'provider', 'items.schedule.client', 'items.schedule.provider', 'items.schedule.customSchedule.serviceType'])
                ->findOrFail($servicePackage->id);

            if ($servicePackage->client?->user_id !== $userId) {
                throw new AuthorizationException;
            }

            $schedules = $this->activePackageSchedules($servicePackage);

            $this->validateServicePackageForPayment($servicePackage, $schedules);

            if ($servicePackage->status !== ServicePackageStatusEnum::OPEN) {
                throw new PaymentException;
            }

            $totals = $this->servicePackagePaymentTotals($schedules, 'pix');

            $payment = Payment::create([
                'schedule_id'              => null,
                'service_package_id'       => $servicePackage->id,
                'client_id'                => $servicePackage->client_id,
                'provider_id'              => $servicePackage->provider_id,
                'client_payment_method_id' => null,
                'gateway_provider'         => 'pagarme',
                'gateway_code'             => 'payment-'.(string) Str::uuid(),
                'payment_method'           => 'pix',
                'status'                   => PaymentStatusEnum::PENDING,
                'gross_amount'             => data_get($totals, 'gross_amount'),
                'gateway_fee_amount'       => 0,
                'platform_fee_amount'      => data_get($totals, 'platform_fee_amount'),
                'net_amount'               => data_get($totals, 'gross_amount'),
                'currency'                 => 'BRL',
                'installments'             => 1,
                'expires_at'               => Carbon::now()->addMinutes(30),

                'metadata' => [
                    'service_package_id' => (string) $servicePackage->id,
                    'schedule_ids'       => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
                    'service_amount'     => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
                    'platform_fee'       => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
                ],
            ]);

            PaymentSplit::create([
                'payment_id'                        => $payment->id,
                'provider_id'                       => $servicePackage->provider_id,
                'gateway_provider'                  => 'pagarme',
                'gateway_transfer_target_reference' => $servicePackage->provider->recipient_id,
                'gateway_transfer_target_label'     => 'recipient',
                'status'                            => PaymentSplitStatusEnum::PENDING,
                'gross_amount'                      => data_get($totals, 'service_amount'),
                'gateway_fee_amount'                => 0,
                'net_amount'                        => data_get($totals, 'service_amount'),

                'metadata' => [
                    'service_package_id' => (string) $servicePackage->id,
                    'schedule_ids'       => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
                ],
            ]);

            return compact('payment', 'schedules');
        });

        /** @var Payment $payment */
        $payment = data_get($paymentData, 'payment');

        /** @var SupportCollection $schedules */
        $schedules = data_get($paymentData, 'schedules');

        try {
            $schedules->first()->ensureCustomerPhone();

            $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
                payment:       $payment,
                schedules:     $schedules,
                paymentMethod: 'pix',
                cardId:        null,
                options:       [],
            );
        } catch (\Throwable $e) {
            $this->failPayment($payment, $e->getMessage());

            throw $e;
        }

        $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);

        $this->syncPaymentTargets($payment);

        return $payment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
    }

    //

    private function isExpiredPixPayment(Payment $payment): bool
    {
        if ($payment->payment_method !== 'pix') {
            return false;
        }

        if ($payment->status === PaymentStatusEnum::PAID) {
            return false;
        }

        return $payment->expires_at !== null
            && $payment->expires_at->isPast();
    }

    private function isIncompleteGatewayPayment(Payment $payment): bool
    {
        return $payment->status === PaymentStatusEnum::PENDING
            && empty($payment->gateway_entity_reference)
            && empty($payment->gateway_operation_reference)
            && empty($payment->gateway_payload);
    }

    private function isStaleIncompleteGatewayPayment(Payment $payment): bool
    {
        return $this->isIncompleteGatewayPayment($payment)
            && ($payment->created_at?->lte(now()->subMinutes(5)) ?? false);
    }

    public function syncScheduleStatusAfterPayment(Schedule $schedule, Payment $payment): void
    {
        if ($payment->status !== PaymentStatusEnum::PAID) {
            return;
        }

        if ($schedule->status !== 'paid') {
            $schedule->update(['status' => 'paid']);
        }

        $this->syncServicePackagesForSchedule($schedule);
    }

    public function syncPaymentTargets(Payment $payment): void
    {
        if ($payment->status !== PaymentStatusEnum::PAID) {
            return;
        }

        if ($payment->service_package_id) {
            $paidSchedules = DB::transaction(function () use ($payment): SupportCollection {
                $servicePackage = ServicePackage::query()->lockForUpdate()->with('items')->find($payment->service_package_id);

                if (! $servicePackage) {
                    return collect();
                }

                $scheduleIds = $servicePackage->items->pluck('schedule_id')->filter()->unique();

                $schedulesToPay = Schedule::query()
                    ->with('provider')
                    ->whereIn('id', $scheduleIds)
                    ->where('status', 'accepted')
                    ->get();

                Schedule::query()
                    ->whereIn('id', $schedulesToPay->pluck('id'))
                    ->where('status', 'accepted')
                    ->update(['status' => 'paid']);

                if ($servicePackage->status !== ServicePackageStatusEnum::PAID) {
                    $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]);
                }

                return $schedulesToPay->toBase();
            });

            $paidSchedules->each(fn (Schedule $schedule) => $this->notifyProviderAndScheduleStart($schedule));

            return;
        }

        $payment->loadMissing('schedule');

        if ($payment->schedule) {
            $this->syncScheduleStatusAfterPayment($payment->schedule, $payment);
        }
    }

    private function notifyProviderAndScheduleStart(Schedule $schedule): void
    {
        if ($schedule->provider_id && $schedule->provider) {
            app(NotificationService::class)->create([
                'title'       => __('notifications.payment_confirmed_title'),
                'description' => __('notifications.payment_confirmed_description'),
                'origin'      => 'schedule',
                'origin_id'   => $schedule->id,
                'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value,
                'user_id'     => $schedule->provider->user_id,
            ]);
        }

        $dateCleaned = Carbon::parse($schedule->date)->format('Y-m-d');

        StartScheduleJob::dispatch($schedule->id)
            ->delay(Carbon::parse($dateCleaned . ' ' . $schedule->start_time)->subHour());
    }

    private function syncServicePackagesForSchedule(Schedule $schedule): void
    {
        ServicePackage::query()
            ->whereHas('items', fn ($query) => $query->where('schedule_id', $schedule->id))
            ->with('items')
            ->get()
            ->each(fn (ServicePackage $servicePackage) => $this->syncServicePackageStatusAfterPayments($servicePackage));
    }

    private function syncServicePackageStatusAfterPayments(ServicePackage $servicePackage): void
    {
        $servicePackage->loadMissing('items');

        $scheduleIds = $servicePackage->items
            ->pluck('schedule_id')
            ->filter()
            ->unique()
            ->values();

        if ($scheduleIds->isEmpty()) {
            return;
        }

        $paidSchedulesCount = Schedule::query()
            ->whereIn('id', $scheduleIds)
            ->where('status', 'paid')
            ->count();

        if ($paidSchedulesCount !== $scheduleIds->count()) {
            return;
        }

        if ($servicePackage->status !== ServicePackageStatusEnum::PAID) {
            $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]);
        }
    }

    private function activePackageSchedules(ServicePackage $servicePackage): SupportCollection
    {
        return $servicePackage->items
            ->pluck('schedule')
            ->filter()
            ->reject(fn (Schedule $schedule) => in_array($schedule->status, ['cancelled', 'rejected'], true))
            ->values();
    }

    private function validateServicePackageForPayment(ServicePackage $servicePackage, SupportCollection $schedules): void
    {
        if (! in_array($servicePackage->status, [ServicePackageStatusEnum::OPEN, ServicePackageStatusEnum::PAID], true)) {
            throw new PaymentException;
        }

        if ($schedules->isEmpty()) {
            throw new PaymentException;
        }

        $providerIds = $schedules->pluck('provider_id')->filter()->unique()->values();
        $clientIds   = $schedules->pluck('client_id')->unique()->values();

        if (! $servicePackage->provider_id || $providerIds->count() !== 1 || (int) $providerIds->first() !== $servicePackage->provider_id) {
            throw new PaymentException;
        }

        if ($clientIds->count() !== 1 || (int) $clientIds->first() !== $servicePackage->client_id) {
            throw new PaymentException;
        }

        if (empty($servicePackage->provider?->recipient_id)) {
            throw new PaymentException;
        }

        foreach ($schedules as $schedule) {
            $expectedStatus = $servicePackage->status === ServicePackageStatusEnum::PAID ? 'paid' : 'accepted';

            if ($schedule->status !== $expectedStatus) {
                throw new PaymentException;
            }

            if ((float) $schedule->total_amount <= 0) {
                throw new PaymentException;
            }
        }
    }

    private function resolveCard(
        int $clientId,
        string $paymentMethod,
        ?int $clientPaymentMethodId,
        ?string $cardId,
    ): array {
        if ($paymentMethod !== 'credit_card') {
            return [null, null];
        }

        if (! $clientPaymentMethodId && empty($cardId)) {
            throw new PaymentException;
        }

        $clientPaymentMethod = $clientPaymentMethodId
            ? ClientPaymentMethod::query()
                ->where('client_id', $clientId)
                ->where('id', $clientPaymentMethodId)
                ->where('is_active', true)
                ->first()
            : null;

        if ($clientPaymentMethodId && ! $clientPaymentMethod) {
            throw new PaymentException;
        }

        $cardId = $cardId ?: $clientPaymentMethod?->gateway_card_id;

        if (empty($cardId)) {
            throw new PaymentException;
        }

        return [$clientPaymentMethod, $cardId];
    }

    private function servicePackagePaymentTotals(SupportCollection $schedules, string $paymentMethod): array
    {
        return $schedules->reduce(function (array $totals, Schedule $schedule) use ($paymentMethod): array {
            $amounts = $this->pagarmePaymentService->calculatePaymentAmounts(
                serviceAmount: (float) $schedule->total_amount,
                paymentMethod: $paymentMethod,
                schedule:      $schedule,
            );

            $schedule->setAttribute('payment_gross_amount', data_get($amounts, 'gross_amount'));

            return [
                'service_amount'      => round(data_get($totals, 'service_amount') + data_get($amounts, 'service_amount'), 2),
                'platform_fee_amount' => round(data_get($totals, 'platform_fee_amount') + data_get($amounts, 'platform_fee_amount'), 2),
                'gross_amount'        => round(data_get($totals, 'gross_amount') + data_get($amounts, 'gross_amount'), 2),
            ];
        }, [
            'service_amount'      => 0.0,
            'platform_fee_amount' => 0.0,
            'gross_amount'        => 0.0,
        ]);
    }

    private function failPayment(Payment $payment, string $message): void
    {
        $payment->forceFill([
            'status'          => PaymentStatusEnum::FAILED,
            'failed_at'       => now(),
            'failure_message' => $message,
        ])->save();

        PaymentSplit::query()
            ->where('payment_id', $payment->id)
            ->update(['status' => PaymentSplitStatusEnum::FAILED]);
    }
}
