<?php

namespace App\Services\Pagarme;

use App\Data\Pagarme\Customer\CustomerRequestData;
use App\Data\Pagarme\Customer\Parts\Request\AddressData;
use App\Data\Pagarme\Customer\Parts\Request\PhoneData;
use App\Data\Pagarme\Customer\Parts\Request\PhonesData;
use App\Data\Pagarme\Order\OrderRequestData;
use App\Data\Pagarme\Order\OrderResponseData;
use App\Data\Pagarme\Order\Parts\Request\CreditCardData;
use App\Data\Pagarme\Order\Parts\Request\ItemData;
use App\Data\Pagarme\Order\Parts\Request\PaymentData;
use App\Data\Pagarme\Order\Parts\Request\PixAdditionalInformationData;
use App\Data\Pagarme\Order\Parts\Request\PixData;
use App\Data\Pagarme\Order\Parts\Request\SplitData;
use App\Data\Pagarme\Order\Parts\Request\SplitOptionsData;
use App\Enums\PaymentSplitStatusEnum;
use App\Enums\PaymentStatusEnum;
use App\Exceptions\PaymentException;
use App\Models\Address;
use App\Models\ServicePackage;
use App\Models\Client;
use App\Models\Payment;
use App\Models\PaymentSplit;
use App\Models\Schedule;
use App\Services\Pagarme\Concerns\FormatsPagarmeData;
use App\Services\Pagarme\Concerns\MocksPagarmeRequests;
use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

class PagarmePaymentService
{
    use FormatsPagarmeData;
    use MocksPagarmeRequests;
    use SendsPagarmeRequests;

    public function calculatePaymentAmounts(float $serviceAmount, string $paymentMethod, ?Schedule $schedule = null): array
    {
        if ($serviceAmount <= 0) {
            throw new PaymentException;
        }

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

        $platformFeeRate = $this->platformFeeRate($paymentMethod, $schedule);

        $platformFee = round($serviceAmount * $platformFeeRate, 2);
        $grossAmount = round($serviceAmount + $platformFee, 2);

        if ($platformFee > 0 && empty(config('services.pagarme.platform_recipient_id'))) {
            throw new \InvalidArgumentException('PAGARME_PLATFORM_RECIPIENT_ID precisa estar configurado para receber a taxa da plataforma no split.');
        }

        return [
            'service_amount'      => round($serviceAmount, 2),
            'platform_fee_amount' => $platformFee,
            'gross_amount'        => $grossAmount,
        ];
    }

    private function platformFeeRate(string $paymentMethod, ?Schedule $schedule = null): float
    {
        if ($schedule
            && $schedule->schedule_type !== 'custom'
            && $this->scheduleBelongsToServicePackageWithAtLeastThreeItems($schedule)) {
            return $paymentMethod === 'credit_card'
                ? $this->configuredPlatformFeeRate('platform_service_package_min_3_schedules_credit_card_fee_rate')
                : $this->configuredPlatformFeeRate('platform_service_package_min_3_schedules_pix_fee_rate');
        }

        return $paymentMethod === 'credit_card'
            ? $this->configuredPlatformFeeRate('platform_credit_card_fee_rate')
            : $this->configuredPlatformFeeRate('platform_pix_fee_rate');
    }

    public function platformFeeRates(): array
    {
        return [
            'pix'         => $this->configuredPlatformFeeRate('platform_pix_fee_rate'),
            'credit_card' => $this->configuredPlatformFeeRate('platform_credit_card_fee_rate'),

            'service_package_min_3_schedules_credit_card' => $this->configuredPlatformFeeRate('platform_service_package_min_3_schedules_credit_card_fee_rate'),
            'service_package_min_3_schedules_pix'         => $this->configuredPlatformFeeRate('platform_service_package_min_3_schedules_pix_fee_rate'),
        ];
    }

    private function configuredPlatformFeeRate(string $key): float
    {
        $rate = config("services.pagarme.{$key}", 0);

        if (is_string($rate)) {
            $rate = str_replace(',', '.', trim($rate));
        }

        $rate = (float) $rate;

        return $rate > 1 ? $rate / 100 : $rate;
    }

    public function processPayment(
        Payment  $payment,
        Schedule $schedule,
        string   $paymentMethod,
        ?string  $cardId  = null,
        array    $options = [],
    ): array {
        $grossAmount = (float) $payment->gross_amount;

        $items    = $this->buildOrderItems($schedule, $grossAmount);
        $customer = $this->buildCustomer($schedule, $options);
        $split    = $this->buildSplit($payment, $options);

        $pixOptions = config('services.pagarme.pix_disable_split')
            ? []
            : ['split' => $split];

        $orderOptions = array_merge(['split' => $split], $pixOptions);

        if ($paymentMethod === 'credit_card') {
            $creditCard = new CreditCardData(
                cardId:              $cardId,
                installments:        $payment->installments,
                statementDescriptor: Str::limit((string) config('app.name', 'SOFTPAR'), 13, ''),
                operationType:       'auth_and_capture',
            );

            $result = $this->createOrderWithCreditCard(
                payment:    $payment,
                items:      $items,
                customer:   $customer,
                creditCard: $creditCard,
                options:    $orderOptions,
            );

            $this->logCreditCardOrderResult($payment, $result, 'create_order');

            $orderStatus = OrderResponseData::fromArray($result)->paymentStatus();

            if (! in_array($orderStatus, [PaymentStatusEnum::PAID, PaymentStatusEnum::AUTHORIZED], true)) {
                return $result;
            }

            return $result;
        }

        $pixData = new PixData(
            expiresIn: 1800,

            additionalInformation: [
                new PixAdditionalInformationData(
                    name:  'Agendamento',
                    value: (string) $schedule->id,
                ),
            ],
        );

        return $this->createOrderWithPix(
            payment:  $payment,
            items:    $items,
            customer: $customer,
            pix:      $pixData,
            options:  $pixOptions,
        );
    }

    public function processServicePackagePayment(
        Payment    $payment,
        Collection $schedules,
        string     $paymentMethod,
        ?string    $cardId  = null,
        array      $options = [],
    ): array {
        $firstSchedule = $schedules->first();

        if (! $firstSchedule) {
            throw new PaymentException;
        }

        $items = $schedules
            ->map(fn (Schedule $schedule) => $this->buildOrderItem(
                $schedule,
                (float) $schedule->getAttribute('payment_gross_amount'),
            ))
            ->all();

        $customer = $this->buildCustomer($firstSchedule, $options);
        $split    = $this->buildSplit($payment, $options);

        $paymentOptions = config('services.pagarme.pix_disable_split')
            ? []
            : ['split' => $split];

        $metadata = [
            'service_package_id' => (string) $payment->service_package_id,
            'schedule_ids'       => $schedules->pluck('id')->implode(','),
        ];

        if ($paymentMethod === 'credit_card') {
            $creditCard = new CreditCardData(
                cardId:              $cardId,
                installments:        $payment->installments,
                statementDescriptor: Str::limit((string) config('app.name', 'SOFTPAR'), 13, ''),
                operationType:       'auth_and_capture',
            );

            $result = $this->createOrderWithCreditCard(
                payment:    $payment,
                items:      $items,
                customer:   $customer,
                creditCard: $creditCard,
                options:    ['split' => $split, 'metadata' => $metadata],
            );

            $this->logCreditCardOrderResult($payment, $result, 'create_service_package_order');

            return $result;
        }

        $pixData = new PixData(
            expiresIn: 1800,
            additionalInformation: [
                new PixAdditionalInformationData(
                    name:  'Agendamentos',
                    value: $schedules->pluck('id')->implode(','),
                ),
            ],
        );

        return $this->createOrderWithPix(
            payment:  $payment,
            items:    $items,
            customer: $customer,
            pix:      $pixData,
            options:  [...$paymentOptions, 'metadata' => $metadata],
        );
    }

    //

    public function createOrderWithCreditCard(
        Payment             $payment,
        array               $items,
        CustomerRequestData $customer,
        CreditCardData      $creditCard,
        array               $options = []
    ): array {
        return $this->createOrder(
            payment:  $payment,
            items:    $items,
            customer: $customer,

            paymentMethod: OrderRequestData::creditCardPaymentMethod(
                creditCard: $creditCard,
                split:      is_array(data_get($options, 'split')) ? data_get($options, 'split') : null,
            ),

            options: $options,
        );
    }

    public function createOrderWithPix(
        Payment             $payment,
        array               $items,
        CustomerRequestData $customer,
        PixData             $pix,
        array               $options = []
    ): array {
        return $this->createOrder(
            payment:  $payment,
            items:    $items,
            customer: $customer,

            paymentMethod: OrderRequestData::pixPaymentMethod(
                pix:   $pix,
                split: is_array(data_get($options, 'split')) ? data_get($options, 'split') : null,
            ),

            options: $options,
        );
    }

    public function createOrder(
        Payment             $payment,
        array               $items,
        CustomerRequestData $customer,
        PaymentData         $paymentMethod,
        array               $options = []
    ): array {
        $metadata = array_filter([
            'payment_id'         => (string) $payment->id,
            'schedule_id'        => $payment->schedule_id ? (string) $payment->schedule_id : null,
            'service_package_id' => $payment->service_package_id ? (string) $payment->service_package_id : null,
            'client_id'          => (string) $payment->client_id,
            'provider_id'        => $payment->provider_id ? (string) $payment->provider_id : null,
        ], fn ($value) => $value !== null);

        $metadata = array_merge($metadata, data_get($options, 'metadata', []));

        $requestData = new OrderRequestData(
            code:       $payment->ensureGatewayCode(),
            items:      $items,
            payments:   [$paymentMethod],
            metadata:   $metadata,
            customer:   $customer,
            customerId: data_get($options, 'customer_id'),
            closed:     data_get($options, 'closed', true),
            channel:    data_get($options, 'channel'),
        );

        if ($this->shouldMockPagarmeRequest()) {
            $order = OrderResponseData::fromArray(
                $this->mockOrderResponse($payment, $requestData, $paymentMethod),
            );

            $order->requireId();

            $this->saveExternalCustomerId($payment, $order);

            return $order->toArray();
        }

        $order = OrderResponseData::fromArray($this->pagarmeRequest(
            method:         'POST',
            path:           '/orders',
            payload:        $requestData,
            idempotencyKey: $this->idempotencyKey($payment),
            errorMessage:   'Erro ao criar pedido de pagamento no Pagar.me.',
        ));

        $order->requireId();

        $this->saveExternalCustomerId($payment, $order);

        return $order->toArray();
    }

    //

    public function applyGatewayResponseToPayment(Payment $payment, array $orderResponse): Payment
    {
        $order = OrderResponseData::fromArray($orderResponse);

        $newStatus = $order->paymentStatus();

        $failureCode    = null;
        $failureMessage = null;

        if ($newStatus === PaymentStatusEnum::FAILED) {
            $failureCode    = $order->failureCode();
            $failureMessage = $order->failureMessage();

            $this->logCreditCardOrderResult($payment, $orderResponse, 'webhook_failed');
        }

        $gatewayFeeCents = $order->lastTransaction()?->cost ?? 0;

        $gatewayFee = $gatewayFeeCents > 0 ? round($gatewayFeeCents / 100, 2) : 0;

        $gatewayPayload = $newStatus === PaymentStatusEnum::FAILED
            ? $this->normalizeFailedGatewayPayload($orderResponse, $failureCode, $failureMessage)
            : $orderResponse;

        $payment->forceFill([
            'gateway_provider'            => 'pagarme',
            'gateway_entity_reference'    => $order->gatewayEntityReference(),
            'gateway_entity_label'        => $order->gatewayEntityLabel(),
            'gateway_operation_reference' => $order->gatewayOperationReference(),
            'gateway_operation_label'     => $order->gatewayOperationLabel(),
            'status'                      => $newStatus,
            'paid_at'                     => $order->paidAt(),
            'authorized_at'               => $order->authorizedAt(),
            'gateway_payload'             => $gatewayPayload,
            'gateway_fee_amount'          => $gatewayFee,
            'failure_code'                => $failureCode,
            'failure_message'             => $failureMessage,
        ])->save();

        $splitStatus = match ($newStatus) {
            PaymentStatusEnum::PAID       => PaymentSplitStatusEnum::TRANSFERRED,
            PaymentStatusEnum::FAILED     => PaymentSplitStatusEnum::FAILED,
            PaymentStatusEnum::CANCELLED  => PaymentSplitStatusEnum::CANCELLED,
            PaymentStatusEnum::AUTHORIZED => PaymentSplitStatusEnum::PROCESSING,
            default                       => PaymentSplitStatusEnum::PENDING,
        };

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

        return $payment->fresh();
    }

    //

    private function scheduleBelongsToServicePackageWithAtLeastThreeItems(Schedule $schedule): bool
    {
        return ServicePackage::query()
            ->whereHas('items', fn ($query) => $query->where('schedule_id', $schedule->id))
            ->whereHas(
                'items.schedule',
                fn ($query) => $query
                    ->whereNotIn('status', ['cancelled', 'rejected'])
                    ->where('schedule_type', 'default'),
                '>=',
                3,
            )
            ->exists();
    }

    //

    private function mockCustomerResponse(OrderRequestData $requestData): array
    {
        $customer = $requestData->customer?->toArray() ?? [];

        return array_merge($customer, [
            'id'         => $requestData->customerId ?: $this->mockPagarmeId('cus', data_get($customer, 'code', $requestData->code)),
            'delinquent' => false,
        ]);
    }

    private function mockOrderResponse(
        Payment $payment, OrderRequestData $requestData, PaymentData $paymentMethod,
    ): array {
        $payload = $requestData->toArray();

        $now = now()->toISOString();

        $orderId       = $this->mockPagarmeId('or', $payment->id);
        $chargeId      = $this->mockPagarmeId('ch', $payment->id);
        $transactionId = $this->mockPagarmeId('tran', $payment->id);

        $amount = array_sum(array_map(
            static fn (array $item): int => ((int) data_get($item, 'amount', 0)) * ((int) data_get($item, 'quantity', 1)),
            data_get($payload, 'items', []),
        ));

        $isCreditCard = $paymentMethod->paymentMethod === 'credit_card';

        $chargeStatus      = $isCreditCard ? 'paid'     : 'pending';
        $transactionStatus = $isCreditCard ? 'captured' : 'waiting_payment';

        $transaction = [
            'id'         => $transactionId,
            'status'     => $transactionStatus,
            'amount'     => $amount,
            'cost'       => 0,
            'created_at' => $now,

            'acquirer_message' => $isCreditCard ? 'Transacao mockada aprovada.' : null,

            'gateway_response' => [
                'code'    => $isCreditCard ? '00' : 'mock_waiting_payment',
                'message' => 'Pagar.me mock local.',
            ],
        ];

        if (! $isCreditCard) {
            $transaction['qr_code']     = '00020101021226880014br.gov.bcb.pix2566mock.local/pix/'.$payment->id;
            $transaction['qr_code_url'] = url("/mock/pix/{$payment->id}");
            $transaction['expires_at']  = now()->addMinutes(30)->toISOString();
        }

        return [
            'id'       => $orderId,
            'code'     => $requestData->code,
            'amount'   => $amount,
            'currency' => 'BRL',
            'closed'   => true,
            'status'   => $chargeStatus,
            'items'    => data_get($payload, 'items', []),
            'customer' => $this->mockCustomerResponse($requestData),

            'charges' => [[
                'id'         => $chargeId,
                'status'     => $chargeStatus,
                'amount'     => $amount,
                'currency'   => 'BRL',
                'paid_at'    => $isCreditCard ? $now : null,
                'created_at' => $now,
                'expires_at' => $isCreditCard ? null : now()->addMinutes(30)->toISOString(),

                'last_transaction' => $transaction,
            ]],

            'checkouts'  => [],
            'metadata'   => array_merge($requestData->metadata, ['mocked' => true]),
            'created_at' => $now,
            'updated_at' => $now,
            'closed_at'  => $now,
        ];
    }

    //

    private function buildCustomer(Schedule $schedule, array $options = []): CustomerRequestData
    {
        $client  = $schedule->client;
        $user    = $client->user()->first(['id', 'name', 'email', 'phone']);
        $address = Address::with(['city.state', 'state'])->find($schedule->address_id);

        foreach ([
            'nome'      => $user?->name,
            'email'     => $user?->email,
            'documento' => $client->document,
        ] as $field => $value) {
            if ($value === null || $value === '') {
                throw new PaymentException;
            }
        }

        if (! $address) {
            throw new PaymentException;
        }

        $document     = $this->customerDocument($client->document);
        $documentType = $this->customerDocumentType($document);

        $phone = $this->buildPhonePayload($user->phone)
            ?: $this->buildPhonePayload(data_get($options, 'phone'));

        $state   = $address->state?->code ?? $address->city?->state?->code;
        $city    = $address->city?->name;
        $zipCode = $this->digits($address->zip_code);

        $line1 = implode(', ', array_filter([
            $address->number ?: 'S/N',
            $address->address,
            $address->district,
        ]));

        foreach ([
            'documento' => $document,
            'estado'    => $state,
            'cidade'    => $city,
            'cep'       => $zipCode,
            'endereco'  => $line1,
            'telefone'  => $phone,
        ] as $field => $value) {
            if ($value === null || $value === '' || $value === []) {
                throw new PaymentException;
            }
        }

        $customerAddress = new AddressData(
            line1:   $line1,
            line2:   $address->complement ?: $address->instructions,
            zipCode: $zipCode,
            city:    $city,
            state:   $state,
            country: 'BR',
        );

        $customerPhones = null;

        if ($phone) {
            $customerPhones = new PhonesData(
                mobilePhone: new PhoneData(
                    countryCode: data_get($phone, 'country_code'),
                    areaCode:    data_get($phone, 'area_code'),
                    number:      data_get($phone, 'number'),
                ),
            );
        }

        return new CustomerRequestData(
            name:         $user->name,
            email:        $user->email,
            document:     $document,
            type:         $documentType === 'CNPJ' ? 'company' : 'individual',
            documentType: $documentType,
            code:         $client->ensureGatewayCode(),
            address:      $customerAddress,
            phones:       $customerPhones,
        );
    }

    private function buildOrderItems(Schedule $schedule, float $grossAmount): array
    {
        return [$this->buildOrderItem($schedule, $grossAmount)];
    }

    private function buildOrderItem(Schedule $schedule, float $grossAmount): ItemData
    {
        $description = $schedule->customSchedule?->serviceType?->description
            ?? "Servico {$schedule->id}";

        return new ItemData(
            code:        "schedule-{$schedule->id}",
            amount:      OrderRequestData::amountInCents($grossAmount),
            quantity:    1,
            description: $description,
        );
    }

    private function buildPhonePayload(?string $phone): ?array
    {
        $digits = $this->digits($phone);

        if (strlen($digits) < 10) {
            return null;
        }

        if (str_starts_with($digits, '55')) {
            $digits = substr($digits, 2);
        }

        return [
            'country_code' => '55',
            'area_code'    => substr($digits, 0, 2),
            'number'       => substr($digits, 2),
        ];
    }

    private function buildSplit(Payment $payment, array $options): array
    {
        $transfers = PaymentSplit::query()
            ->where('payment_id', $payment->id)
            ->get();

        $split = OrderRequestData::splitFromTransfers($transfers);

        $platformRecipientId = config('services.pagarme.platform_recipient_id');

        if (empty($platformRecipientId)) {
            return $split;
        }

        $orderAmountCents = OrderRequestData::amountInCents((float) $payment->gross_amount);

        $providerTotalCents = array_sum(array_map(
            static fn (SplitData $s) => $s->amount,
            $split,
        ));

        $platformAmountCents = $orderAmountCents - $providerTotalCents;

        if ($platformAmountCents > 0) {
            $split[] = new SplitData(
                amount:      $platformAmountCents,
                recipientId: $platformRecipientId,
                type:        'flat',

                options: new SplitOptionsData(
                    chargeProcessingFee: true,
                    chargeRemainderFee:  true,
                    liable:              true,
                ),
            );
        }

        return $split;
    }

    //

    private function isMisleadingGatewayCode(mixed $code): bool
    {
        if ($code === null || $code === '') {
            return false;
        }

        $code = mb_strtolower((string) $code);

        return preg_match('/^[1-2]\d{2}$/', $code) === 1
            || in_array($code, ['00', '0', 'approved', 'success'], true);
    }

    private function logCreditCardOrderResult(Payment $payment, array $orderResponse, string $source): void
    {
        $order       = OrderResponseData::fromArray($orderResponse);
        $charge      = $order->firstCharge();
        $transaction = $order->lastTransaction();
        $failureCode = $order->failureCode();

        Log::channel('pagarme')->info('Resultado do pedido com cartão de crédito no Pagar.me', [
            'source'             => $source,
            'payment_id'         => $payment->id,
            'provider_id'        => $payment->provider_id,
            'order_id'           => $order->id,
            'order_status'       => $order->status,
            'charge_id'          => $charge?->id,
            'charge_status'      => $charge?->status,
            'transaction_id'     => $transaction?->id,
            'transaction_status' => $transaction?->status,
            'failure_code'       => $failureCode,
            'failure_message'    => $order->failureMessage(),
            'acquirer_message'   => $transaction?->acquirerMessage,

            'gateway_response' => $this->normalizeGatewayResponseForFailure(
                $transaction?->gatewayResponse ?? [],
                $failureCode,
            ),
        ]);
    }

    private function normalizeFailedGatewayPayload(array $payload, ?string $failureCode, ?string $failureMessage): array
    {
        $payload['failure_code']    = $failureCode;
        $payload['failure_message'] = $failureMessage;

        $gatewayResponse = data_get($payload, 'charges.0.last_transaction.gateway_response');

        if (is_array($gatewayResponse)) {
            data_set($payload, 'charges.0.last_transaction.gateway_response', $this->normalizeGatewayResponseForFailure(
                $gatewayResponse,
                $failureCode,
            ));
        }

        return $payload;
    }

    private function normalizeGatewayResponseForFailure(array $gatewayResponse, ?string $failureCode): array
    {
        $code = data_get($gatewayResponse, 'code');

        if (! $failureCode || ! $this->isMisleadingGatewayCode($code)) {
            return $gatewayResponse;
        }

        $gatewayResponse['raw_code'] = $code;
        $gatewayResponse['code']     = $failureCode;

        return $gatewayResponse;
    }

    private function roundMoneyUp(float $amount): float
    {
        return ceil($amount * 100) / 100;
    }

    // evita criacao duplicada de payment

    // formato: order-{data}-{gateway-code-prestador}-{gateway-code-cliente} (tudo minusculo e sem acento)

    private function idempotencyKey(Payment $payment): string
    {
        if (! empty($payment->idempotency_key)) {
            return $payment->idempotency_key;
        }

        $payment->loadMissing(['client', 'provider']);

        $date = $payment->created_at?->format('Y-m-d') ?: now()->format('Y-m-d');

        $providerCode = $payment->provider?->ensureGatewayCode() ?: "provider-{$payment->provider_id}";
        $clientCode   = $payment->client?->ensureGatewayCode() ?: "client-{$payment->client_id}";

        $key = $this->pagarmeIdempotencyKey('order', [
            $date,
            $providerCode,
            $clientCode,
        ]);

        $payment->forceFill(['idempotency_key' => $key])->save();

        return $key;
    }

    // salva o gateway_customer_id do Pagar.me no Client apos criacao de ordem

    private function saveExternalCustomerId(Payment $payment, OrderResponseData $order): void
    {
        $customerId   = $order->customer?->id;
        $customerCode = $order->customer?->code;

        if (! $customerId && ! $customerCode) {
            return;
        }

        $client = Client::find($payment->client_id);

        if (! $client) {
            return;
        }

        $updated = false;

        if (! $client->gateway_customer_id && $customerId) {
            $client->gateway_customer_id = $customerId;

            $updated = true;
        }

        if (! $client->gateway_customer_code && $customerCode) {
            $client->gateway_customer_code = $customerCode;

            $updated = true;
        }

        if ($updated) {
            $client->save();
        }
    }
}
