<?php

namespace Botble\Paystack\Services;

use Botble\Paystack\Exceptions\IsNullException;
use Botble\Paystack\Exceptions\PaymentVerificationFailedException;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Config;

class Paystack
{
    /**
     * Transaction Verification Successful
     */
    public const VS = 'Verification successful';

    /**
     *  Invalid Transaction reference
     */
    public const ITF = 'Invalid transaction reference';

    /**
     * Issue Secret Key from your Paystack Dashboard
     * @var string
     */
    protected $secretKey;

    /**
     * Instance of Client
     * @var Client
     */
    protected $client;

    /**
     *  Response from requests made to Paystack
     * @var mixed
     */
    protected $response;

    /**
     * Paystack API base Url
     * @var string
     */
    protected $baseUrl;

    /**
     * Authorization Url - Paystack payment page
     * @var string
     */
    protected $authorizationUrl;

    public function __construct()
    {
        $this->setKey();
        $this->setBaseUrl();
        $this->setRequestOptions();
    }

    /**
     * Get Base Url from Paystack config file
     */
    public function setBaseUrl()
    {
        $this->baseUrl = Config::get('paystack.paymentUrl');
    }

    /**
     * Get secret key from Paystack config file
     */
    public function setKey()
    {
        $this->secretKey = Config::get('paystack.secretKey');
    }

    /**
     * Set options for making the Client request
     */
    private function setRequestOptions()
    {
        $authBearer = 'Bearer ' . $this->secretKey;

        $this->client = new Client(
            [
                'base_uri' => $this->baseUrl,
                'headers' => [
                    'Authorization' => $authBearer,
                    'Content-Type' => 'application/json',
                    'Accept' => 'application/json',
                ],
                'verify' => false,
            ]
        );
    }

    /**

     * Initiate a payment request to Paystack
     * Included the option to pass the payload to this method for situations
     * when the payload is built on the fly (not passed to the controller from a view)
     * @return Paystack
     */

    public function makePaymentRequest($data = null)
    {
        if ($data == null) {

            $quantity = intval(request()->quantity ?? 1);

            $data = array_filter([
                'amount' => intval(request()->amount) * $quantity,
                'reference' => request()->reference,
                'email' => request()->email,
                'channels' => request()->channels,
                'plan' => request()->plan,
                'first_name' => request()->first_name,
                'last_name' => request()->last_name,
                'callback_url' => request()->callback_url,
                'currency' => (request()->currency != '' ? request()->currency : 'NGN'),

                /*
                    Paystack allows for transactions to be split into a subaccount -
                    The following lines trap the subaccount ID - as well as the ammount to charge the subaccount (if overriden in the form)
                    both values need to be entered within hidden input fields
                */
                'subaccount' => request()->subaccount,
                'transaction_charge' => request()->transaction_charge,

                /**
                 * Paystack allows for transaction to be split into multi accounts(subaccounts)
                 * The following lines trap the split ID handling the split
                 * More details here: https://paystack.com/docs/payments/multi-split-payments/#using-transaction-splits-with-payments
                 */
                'split_code' => request()->split_code,

                /**
                 * Paystack allows transaction to be split into multi account(subaccounts) on the fly without predefined split
                 * form need an input field: <input type="hidden" name="split" value="{{ json_encode($split) }}" >
                 * array must be set up as:
                 *  $split = [
                 *    "type" => "percentage",
                 *     "currency" => "KES",
                 *     "subaccounts" => [
                 *       { "subaccount" => "ACCT_li4p6kte2dolodo", "share" => 10 },
                 *       { "subaccount" => "ACCT_li4p6kte2dolodo", "share" => 30 },
                 *     ],
                 *     "bearer_type" => "all",
                 *     "main_account_share" => 70,
                 * ]
                 * More details here: https://paystack.com/docs/payments/multi-split-payments/#dynamic-splits
                 */
                'split' => request()->split,
                /*
                * to allow use of metadata on Paystack dashboard and a means to return additional data back to redirect url
                * form need an input field: <input type="hidden" name="metadata" value="{{ json_encode($array) }}" >
                * array must be set up as:
                * $array = [ 'custom_fields' => [
                *                   ['display_name' => "Cart Id", "variable_name" => "cart_id", "value" => "2"],
                *                   ['display_name' => "Sex", "variable_name" => "sex", "value" => "female"],
                *                   .
                *                   .
                *                   .
                *                  ]
                *          ]
                */
                'metadata' => request()->metadata,
            ]);
        }

        $this->setHttpResponse('/transaction/initialize', 'POST', $data);

        return $this;
    }

    /**
     * @param string $relativeUrl
     * @param string $method
     * @param array $body
     * @return Paystack
     * @throws IsNullException
     */
    private function setHttpResponse($relativeUrl, $method, $body = [])
    {
        if (is_null($method)) {
            throw new IsNullException('Empty method not allowed');
        }

        $this->response = $this->client->{strtolower($method)}(
            $this->baseUrl . $relativeUrl,
            ['body' => json_encode($body)]
        );

        return $this;
    }

    /**
     * Get the authorization url from the callback response
     * @return Paystack
     */
    public function getAuthorizationUrl($data = null)
    {
        $this->makePaymentRequest($data);

        $this->url = $this->getResponse()['data']['authorization_url'];

        return $this;
    }

    /**
     * Get the authorization callback response
     * In situations where Laravel serves as an backend for a detached UI, the api cannot redirect
     * and might need to take different actions based on the success or not of the transaction
     * @return array
     */
    public function getAuthorizationResponse($data)
    {
        $this->makePaymentRequest($data);

        $this->url = $this->getResponse()['data']['authorization_url'];

        return $this->getResponse();
    }

    /**
     * Hit Paystack Gateway to Verify that the transaction is valid
     */
    private function verifyTransactionAtGateway($transaction_id = null)
    {
        $transactionRef = $transaction_id ?? request()->query('trxref');

        $relativeUrl = "/transaction/verify/{$transactionRef}";

        $this->response = $this->client->get($this->baseUrl . $relativeUrl, []);
    }

    /**
     * True or false condition whether the transaction is verified
     * @return boolean
     */
    public function isTransactionVerificationValid($transaction_id = null)
    {
        $this->verifyTransactionAtGateway($transaction_id);

        $result = $this->getResponse()['message'];

        return match ($result) {
            self::VS => true,
            default => false,
        };
    }

    /**
     * Get Payment details if the transaction was verified successfully
     * @throws PaymentVerificationFailedException
     */
    public function getPaymentData(): array
    {
        if ($this->isTransactionVerificationValid()) {
            return $this->getResponse();
        } else {
            throw new PaymentVerificationFailedException('Invalid Transaction Reference');
        }
    }

    /**
     * Fluent method to redirect to Paystack Payment Page
     */
    public function redirectNow()
    {
        return redirect($this->url);
    }

    /**
     * Get Access code from transaction callback respose
     * @return string
     */
    public function getAccessCode()
    {
        return $this->getResponse()['data']['access_code'];
    }

    /**
     * Generate a Unique Transaction Reference
     * @return string
     */
    public function genTranxRef()
    {
        return TransRef::getHashedToken();
    }

    /**
     * Get all the customers that have made transactions on your platform
     * @return array
     */
    public function getAllCustomers()
    {
        $this->setRequestOptions();

        return $this->setHttpResponse('/customer', 'GET', [])->getData();
    }

    /**
     * Get all the plans that you have on Paystack
     * @return array
     */
    public function getAllPlans()
    {
        $this->setRequestOptions();

        return $this->setHttpResponse('/plan', 'GET', [])->getData();
    }

    /**
     * Get all the transactions that have happened overtime
     * @return array
     */
    public function getAllTransactions()
    {
        $this->setRequestOptions();

        return $this->setHttpResponse('/transaction', 'GET', [])->getData();
    }

    /**
     * Get the data response from a get operation
     * @return array
     */
    private function getData()
    {
        return $this->getResponse()['data'];
    }

    /**
     * Create a plan
     */
    public function createPlan()
    {
        $data = [
            'name' => request()->name,
            'description' => request()->desc,
            'amount' => intval(request()->amount),
            'interval' => request()->interval,
            'send_invoices' => request()->send_invoices,
            'send_sms' => request()->send_sms,
            'currency' => request()->currency,
        ];

        $this->setRequestOptions();

        return $this->setHttpResponse('/plan', 'POST', $data)->getResponse();
    }

    /**
     * Fetch any plan based on its plan id or code
     * @param $plan_code
     * @return array
     */
    public function fetchPlan($plan_code)
    {
        $this->setRequestOptions();

        return $this->setHttpResponse('/plan/' . $plan_code, 'GET', [])->getResponse();
    }

    /**
     * Update any plan's details based on its id or code
     * @param $plan_code
     * @return array
     */
    public function updatePlan($plan_code)
    {
        $data = [
            'name' => request()->name,
            'description' => request()->desc,
            'amount' => intval(request()->amount),
            'interval' => request()->interval,
            'send_invoices' => request()->send_invoices,
            'send_sms' => request()->send_sms,
            'currency' => request()->currency,
        ];

        $this->setRequestOptions();

        return $this->setHttpResponse('/plan/' . $plan_code, 'PUT', $data)->getResponse();
    }

    /**
     * Create a customer
     */
    public function createCustomer($data = null)
    {
        if ($data == null) {

            $data = [
                'email' => request()->email,
                'first_name' => request()->fname,
                'last_name' => request()->lname,
                'phone' => request()->phone,
                'metadata' => request()->additional_info, /* key => value pairs array */

            ];
        }

        $this->setRequestOptions();

        return $this->setHttpResponse('/customer', 'POST', $data)->getResponse();
    }

    /**
     * Fetch a customer based on id or code
     * @param $customer_id
     * @return array
     */
    public function fetchCustomer($customer_id)
    {
        $this->setRequestOptions();

        return $this->setHttpResponse('/customer/' . $customer_id, 'GET', [])->getResponse();
    }

    /**
     * Update a customer's details based on their id or code
     * @param $customer_id
     * @return array
     */
    public function updateCustomer($customer_id)
    {
        $data = [
            'email' => request()->email,
            'first_name' => request()->fname,
            'last_name' => request()->lname,
            'phone' => request()->phone,
            'metadata' => request()->additional_info, /* key => value pairs array */

        ];

        $this->setRequestOptions();

        return $this->setHttpResponse('/customer/' . $customer_id, 'PUT', $data)->getResponse();
    }

    /**
     * Export transactions in .CSV
     * @return array
     */
    public function exportTransactions()
    {
        $data = [
            'from' => request()->from,
            'to' => request()->to,
            'settled' => request()->settled,
        ];

        $this->setRequestOptions();

        return $this->setHttpResponse('/transaction/export', 'GET', $data)->getResponse();
    }

    /**
     * Create a subscription to a plan from a customer.
     */
    public function createSubscription()
    {
        $data = [
            'customer' => request()->customer, //Customer email or code
            'plan' => request()->plan,
            'authorization' => request()->authorization_code,
        ];

        $this->setRequestOptions();

        return $this->setHttpResponse('/subscription', 'POST', $data)->getResponse();
    }

    /**
     * Get all the subscriptions made on Paystack.
     *
     * @return array
     */
    public function getAllSubscriptions()
    {
        $this->setRequestOptions();

        return $this->setHttpResponse('/subscription', 'GET', [])->getData();
    }

    /**
     * Get customer subscriptions
     *
     * @param integer $customer_id
     * @return array
     */
    public function getCustomerSubscriptions($customer_id)
    {
        $this->setRequestOptions();

        return $this->setHttpResponse('/subscription?customer=' . $customer_id, 'GET', [])->getData();
    }

    /**
     * Get plan subscriptions
     *
     * @param  integer $plan_id
     * @return array
     */
    public function getPlanSubscriptions($plan_id)
    {
        $this->setRequestOptions();

        return $this->setHttpResponse('/subscription?plan=' . $plan_id, 'GET', [])->getData();
    }

    /**
     * Enable a subscription using the subscription code and token
     * @return array
     */
    public function enableSubscription()
    {
        $data = [
            'code' => request()->code,
            'token' => request()->token,
        ];

        $this->setRequestOptions();

        return $this->setHttpResponse('/subscription/enable', 'POST', $data)->getResponse();
    }

    /**
     * Disable a subscription using the subscription code and token
     * @return array
     */
    public function disableSubscription()
    {
        $data = [
            'code' => request()->code,
            'token' => request()->token,
        ];

        $this->setRequestOptions();

        return $this->setHttpResponse('/subscription/disable', 'POST', $data)->getResponse();
    }

    /**
     * Fetch details about a certain subscription
     * @param mixed $subscription_id
     * @return array
     */
    public function fetchSubscription($subscription_id)
    {
        $this->setRequestOptions();

        return $this->setHttpResponse('/subscription/' . $subscription_id, 'GET', [])->getResponse();
    }

    /**
     * Create pages you can share with users using the returned slug
     */
    public function createPage()
    {
        $data = [
            'name' => request()->name,
            'description' => request()->description,
            'amount' => request()->amount,
        ];

        $this->setRequestOptions();

        return $this->setHttpResponse('/page', 'POST', $data)->getResponse();
    }

    /**
     * Fetches all the pages the merchant has
     * @return array
     */
    public function getAllPages()
    {
        $this->setRequestOptions();

        return $this->setHttpResponse('/page', 'GET', [])->getResponse();
    }

    /**
     * Fetch details about a certain page using its id or slug
     * @param mixed $page_id
     * @return array
     */
    public function fetchPage($page_id)
    {
        $this->setRequestOptions();

        return $this->setHttpResponse('/page/' . $page_id, 'GET', [])->getResponse();
    }

    /**
     * Update the details about a particular page
     * @param $page_id
     * @return array
     */
    public function updatePage($page_id)
    {
        $data = [
            'name' => request()->name,
            'description' => request()->description,
            'amount' => request()->amount,
        ];

        $this->setRequestOptions();

        return $this->setHttpResponse('/page/' . $page_id, 'PUT', $data)->getResponse();
    }

    /**
     * Creates a subaccount to be used for split payments . Required    params are business_name , settlement_bank , account_number ,   percentage_charge
     *
     * @return array
     */

    public function createSubAccount()
    {
        $data = [
            'business_name' => request()->business_name,
            'settlement_bank' => request()->settlement_bank,
            'account_number' => request()->account_number,
            'percentage_charge' => request()->percentage_charge,
            'primary_contact_email' => request()->primary_contact_email,
            'primary_contact_name' => request()->primary_contact_name,
            'primary_contact_phone' => request()->primary_contact_phone,
            'metadata' => request()->metadata,
            'settlement_schedule' => request()->settlement_schedule,
        ];

        $this->setRequestOptions();

        return $this->setHttpResponse('/subaccount', 'POST', array_filter($data))->getResponse();
    }

    /**
     * Fetches details of a subaccount
     * @param $subaccount_code code
     * @return array
     */
    public function fetchSubAccount($subaccount_code)
    {

        $this->setRequestOptions();

        return $this->setHttpResponse("/subaccount/{$subaccount_code}", 'GET', [])->getResponse();
    }

    /**
     * Lists all the subaccounts associated with the account
     * @param $per_page - Specifies how many records to retrieve per page , $page - SPecifies exactly what page to retrieve
     * @return array
     */
    public function listSubAccounts($per_page, $page)
    {

        $this->setRequestOptions();

        return $this->setHttpResponse('/subaccount/?perPage=' . (int) $per_page . '&page=' . (int) $page, 'GET')->getResponse();
    }

    /**
     * Updates a subaccount to be used for split payments . Required params are business_name , settlement_bank , account_number , percentage_charge
     * @param subaccount code
     * @return array
     */

    public function updateSubAccount($subaccount_code)
    {
        $data = [
            'business_name' => request()->business_name,
            'settlement_bank' => request()->settlement_bank,
            'account_number' => request()->account_number,
            'percentage_charge' => request()->percentage_charge,
            'description' => request()->description,
            'primary_contact_email' => request()->primary_contact_email,
            'primary_contact_name' => request()->primary_contact_name,
            'primary_contact_phone' => request()->primary_contact_phone,
            'metadata' => request()->metadata,
            'settlement_schedule' => request()->settlement_schedule,
        ];

        $this->setRequestOptions();

        return $this->setHttpResponse("/subaccount/{$subaccount_code}", 'PUT', array_filter($data))->getResponse();
    }

    /**
     * Get a list of all supported banks and their properties
     * @param $country - The country from which to obtain the list of supported banks, $per_page - Specifies how many records to retrieve per page ,
     * $use_cursor - Flag to enable cursor pagination on the endpoint
     * @return array
     */
    public function getBanks(?string $country, int $per_page = 50, bool $use_cursor = false)
    {
        if (! $country) {
            $country = request()->country ?? 'nigeria';
        }

        $this->setRequestOptions();

        return $this->setHttpResponse('/bank/?country=' . $country . '&use_cursor=' . $use_cursor . '&perPage=' . (int) $per_page, 'GET')->getResponse();
    }

    /**
     * Confirm an account belongs to the right customer
     * @param $account_number - Account Number, $bank_code - You can get the list of bank codes by calling the List Banks endpoint
     * @return array
     */
    public function confirmAccount(string $account_number, string $bank_code)
    {

        $this->setRequestOptions();

        return $this->setHttpResponse('/bank/resolve/?account_number=' . $account_number . '&bank_code=' . $bank_code, 'GET')->getResponse();
    }

    public function refundOrder($paymentId, $amount)
    {
        $relativeUrl = '/refund';

        $data = [
            'body' => json_encode([
                'transaction' => $paymentId,
                'amount' => $amount * 100,
            ]),
        ];

        do_action('payment_before_making_api_request', PAYSTACK_PAYMENT_METHOD_NAME, $data);

        $this->response = $this->client->post($this->baseUrl . $relativeUrl, $data);

        do_action('payment_after_api_response', PAYSTACK_PAYMENT_METHOD_NAME, $data, (array) $this->response);

        if ($this->isValid()) {
            return $this->getResponse();
        }

        throw new Exception('Invalid Refund Order Paystack');
    }

    protected function getResponse(): array
    {
        return json_decode($this->response->getBody(), true);
    }

    public function isValid(): bool
    {
        return $this->getResponse()['status'];
    }

    public function getPaymentDetails($transactionId)
    {
        $relativeUrl = '/transaction/' . $transactionId;

        do_action('payment_before_making_api_request', PAYSTACK_PAYMENT_METHOD_NAME, ['transaction_id' => $transactionId]);

        $this->response = $this->client->get($this->baseUrl . $relativeUrl);

        do_action('payment_after_api_response', PAYSTACK_PAYMENT_METHOD_NAME, ['transaction_id' => $transactionId], (array) $this->response);

        if ($this->isValid()) {
            return $this->getResponse();
        }

        throw new Exception('Invalid Get Payment Details Paystack');
    }

    public function getListTransactions(array $params = [])
    {
        $relativeUrl = '/transaction' . ($params ? ('?' . http_build_query($params)) : '');

        do_action('payment_before_making_api_request', PAYSTACK_PAYMENT_METHOD_NAME, $params);

        $this->response = $this->client->get($this->baseUrl . $relativeUrl);

        do_action('payment_after_api_response', PAYSTACK_PAYMENT_METHOD_NAME, $params, (array) $this->response);

        if ($this->isValid()) {
            return $this->getResponse();
        }

        throw new Exception('Invalid Get List Transactions Paystack');
    }

    public function getRefundDetails($refundId)
    {
        $relativeUrl = '/refund/' . $refundId;

        do_action('payment_before_making_api_request', PAYSTACK_PAYMENT_METHOD_NAME, ['refund_id' => $refundId]);

        $this->response = $this->client->get($this->baseUrl . $relativeUrl);

        do_action('payment_after_api_response', PAYSTACK_PAYMENT_METHOD_NAME, ['refund_id' => $refundId], (array) $this->response);

        if ($this->isValid()) {
            return $this->getResponse();
        }

        throw new Exception('Invalid Refund Order Paystack');
    }
}
