<?php

namespace App\Services;

use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use RuntimeException;

class MpesaService
{
    public function accessToken(): string
    {
        $consumerKey = (string) config('mpesa.consumer_key');
        $consumerSecret = (string) config('mpesa.consumer_secret');

        if ($consumerKey === '' || $consumerSecret === '') {
            throw new RuntimeException(
                'M-PESA Consumer Key and Consumer Secret are not configured.'
            );
        }

        $environment = config('mpesa.environment', 'sandbox');

        return Cache::remember(
            "mpesa.access_token.{$environment}",
            now()->addSeconds(3500),
            function () use ($consumerKey, $consumerSecret, $environment): string {
                $response = Http::withBasicAuth(
                    $consumerKey,
                    $consumerSecret
                )
                    ->acceptJson()
                    ->timeout(config('mpesa.timeout', 30))
                    ->get(
                        $this->baseUrl($environment)
                        . '/oauth/v1/generate?grant_type=client_credentials'
                    );

                $response->throw();

                $token = $response->json('access_token');

                if (! is_string($token) || $token === '') {
                    throw new RuntimeException(
                        'M-PESA authorization did not return an access token.'
                    );
                }

                return $token;
            }
        );
    }

    public function stkPush(
        string $phoneNumber,
        int|float|string $amount,
        string $accountReference,
        string $transactionDescription = 'Chama payment'
    ): array {
        $shortcode = (string) config('mpesa.shortcode');
        $passkey = (string) config('mpesa.passkey');
        $transactionType = (string) config(
            'mpesa.transaction_type',
            'CustomerPayBillOnline'
        );
        $callbackUrl = (string) config('mpesa.callback_url');

        if ($shortcode === '' || $passkey === '' || $callbackUrl === '') {
            throw new RuntimeException(
                'M-PESA shortcode, passkey, and callback URL must be configured.'
            );
        }

        if (config('mpesa.environment', 'sandbox') === 'sandbox') {
            $phoneNumber = (string) config('mpesa.sandbox_phone_number', '254708374149');
        } else {
            $phoneNumber = $this->normalizePhoneNumber($phoneNumber);
        }

        $amount = (int) round((float) $amount);

        if ($amount < 1) {
            throw new RuntimeException(
                'M-PESA payment amount must be at least KES 1.'
            );
        }

        $timestamp = now('Africa/Nairobi')->format('YmdHis');

        $password = base64_encode(
            $shortcode . $passkey . $timestamp
        );

        $payload = [
            'BusinessShortCode' => $shortcode,
            'Password' => $password,
            'Timestamp' => $timestamp,
            'TransactionType' => $transactionType,
            'Amount' => $amount,
            'PartyA' => $phoneNumber,
            'PartyB' => $shortcode,
            'PhoneNumber' => $phoneNumber,
            'CallBackURL' => $callbackUrl,
            'AccountReference' => Str::limit(
                preg_replace('/[^A-Za-z0-9]/', '', $accountReference),
                12,
                ''
            ),
            'TransactionDesc' => Str::limit(
                $transactionDescription,
                13,
                ''
            ),
        ];

        $response = Http::withToken($this->accessToken())
            ->acceptJson()
            ->asJson()
            ->timeout(config('mpesa.timeout', 30))
            ->post(
                $this->baseUrl(config('mpesa.environment', 'sandbox'))
                . '/mpesa/stkpush/v1/processrequest',
                $payload
            );

        $response->throw();

        $data = $response->json();

        if (($data['ResponseCode'] ?? null) !== '0') {
            throw new RuntimeException(
                $data['ResponseDescription']
                ?? $data['errorMessage']
                ?? 'M-PESA STK Push request was not accepted.'
            );
        }

        if (
            empty($data['CheckoutRequestID'])
            || empty($data['MerchantRequestID'])
        ) {
            throw new RuntimeException(
                'M-PESA did not return the required payment request identifiers.'
            );
        }

        return $data;
    }

    public function normalizePhoneNumber(string $phoneNumber): string
    {
        $phoneNumber = preg_replace('/\D+/', '', $phoneNumber) ?? '';

        if (str_starts_with($phoneNumber, '0')) {
            $phoneNumber = '254' . substr($phoneNumber, 1);
        } elseif (
            strlen($phoneNumber) === 9
            && str_starts_with($phoneNumber, '7')
        ) {
            $phoneNumber = '254' . $phoneNumber;
        }

        if (! preg_match('/^254[17]\d{8}$/', $phoneNumber)) {
            throw new RuntimeException(
                'The M-PESA phone number must be a valid Kenyan mobile number.'
            );
        }

        return $phoneNumber;
    }

    private function baseUrl(string $environment): string
    {
        return rtrim(
            (string) config("mpesa.base_urls.{$environment}"),
            '/'
        );
    }
}