<?php

namespace App\Libraries;

use CodeIgniter\Config\Services;

/**
 * PesaPal API 3.0 (JSON) integration.
 * Handles authentication (Bearer token), IPN registration, submit order, transaction status, refund, cancel.
 * Default currency: UGX.
 */
class PesaPalService
{
    private string $baseUrl;
    private string $consumerKey;
    private string $consumerSecret;
    private string $currency;
    private const CACHE_KEY_TOKEN = 'pesapal_token';
    private const CACHE_KEY_EXPIRY = 'pesapal_token_expiry';
    /** Buffer seconds before token expiry to refresh */
    private const TOKEN_BUFFER_SECONDS = 60;

    public function __construct()
    {
        $config = config('PesaPal');
        $env = $_ENV['pesapal.environment'] ?? $config->environment;
        $this->baseUrl = $_ENV['pesapal.api_base_url'] ?? ($env === 'production' ? $config->productionBaseUrl : $config->sandboxBaseUrl);
        $this->consumerKey = $_ENV['pesapal.consumer_key'] ?? $config->consumerKey;
        $this->consumerSecret = $_ENV['pesapal.consumer_secret'] ?? $config->consumerSecret;
        $this->currency = $_ENV['pesapal.currency'] ?? $config->currency;
    }

    /**
     * Get Bearer token (cached until near expiry), then refresh.
     */
    private function getToken(): array
    {
        $cache = \Config\Services::cache();
        $expiry = $cache->get(self::CACHE_KEY_EXPIRY);
        $now = time();
        if ($expiry && $expiry > ($now + self::TOKEN_BUFFER_SECONDS)) {
            $token = $cache->get(self::CACHE_KEY_TOKEN);
            if ($token) {
                return ['success' => true, 'token' => $token];
            }
        }

        $client = Services::curlrequest();
        $response = $client->post($this->baseUrl . '/Auth/RequestToken', [
            'json' => [
                'consumer_key'    => $this->consumerKey,
                'consumer_secret' => $this->consumerSecret,
            ],
            'headers' => [
                'Accept'       => 'application/json',
                'Content-Type' => 'application/json',
            ],
            'timeout' => 15,
            'http_errors' => false,
        ]);

        $body = json_decode($response->getBody(), true);
        $status = $response->getStatusCode();

        if ($status !== 200 || empty($body['token'])) {
            return [
                'success' => false,
                'message' => $body['message'] ?? $body['error']['message'] ?? 'Authentication failed',
                'status'  => (string) $status,
            ];
        }

        $token = $body['token'];
        $expiryStr = $body['expiryDate'] ?? null;
        $expiryTs = $expiryStr ? (strtotime($expiryStr) ?: $now + 300) : $now + 300;
        $cache->save(self::CACHE_KEY_TOKEN, $token, 300);
        $cache->save(self::CACHE_KEY_EXPIRY, $expiryTs, 300);

        return ['success' => true, 'token' => $token];
    }

    /**
     * Make authenticated request to PesaPal API.
     *
     * @param string $method GET or POST
     * @param string $path   Path relative to base URL (e.g. /Transactions/SubmitOrderRequest)
     * @param array|null $body JSON body for POST
     * @return array { success: bool, data?: array, message?: string, status?: string }
     */
    private function request(string $method, string $path, ?array $body = null): array
    {
        $auth = $this->getToken();
        if (!$auth['success']) {
            return $auth;
        }

        $url = rtrim($this->baseUrl, '/') . $path;

        $client = Services::curlrequest();
        $options = [
            'headers' => [
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
                'Authorization' => 'Bearer ' . $auth['token'],
            ],
            'timeout' => 30,
            'http_errors' => false,
        ];

        if ($method === 'POST' && $body !== null) {
            $options['json'] = $body;
        }

        $response = $method === 'GET'
            ? $client->get($url, $options)
            : $client->post($url, $options);

        $responseBody = json_decode($response->getBody(), true);
        $statusCode = $response->getStatusCode();

        if ($statusCode >= 400) {
            return [
                'success' => false,
                'message' => $responseBody['message'] ?? $responseBody['error']['message'] ?? 'Request failed',
                'status'  => (string) $statusCode,
                'data'    => $responseBody,
            ];
        }

        return [
            'success' => true,
            'data'    => $responseBody,
            'status'  => (string) $statusCode,
        ];
    }

    /**
     * Register IPN URL. Use ipn_notification_type "POST" as preferred.
     *
     * @param string $url Full IPN URL
     * @param string $ipnNotificationType GET or POST
     * @return array { success: bool, ipn_id?: string, message?: string, ... }
     */
    public function registerIpn(string $url, string $ipnNotificationType = 'POST'): array
    {
        $result = $this->request('POST', '/URLSetup/RegisterIPN', [
            'url'                   => $url,
            'ipn_notification_type' => $ipnNotificationType,
        ]);

        if (!$result['success']) {
            return $result;
        }

        $data = $result['data'];
        $ipnId = $data['ipn_id'] ?? null;
        if (!$ipnId) {
            return [
                'success' => false,
                'message' => 'No ipn_id in response',
                'data'    => $data,
            ];
        }

        return [
            'success' => true,
            'ipn_id'  => $ipnId,
            'data'    => $data,
        ];
    }

    /**
     * Get list of registered IPN URLs.
     */
    public function getIpnList(): array
    {
        return $this->request('GET', '/URLSetup/GetIpnList');
    }

    /**
     * Validate merchant reference: max 50 chars, alphanumeric, - _ . :
     */
    public static function validateMerchantReference(string $id): bool
    {
        if (strlen($id) > 50) {
            return false;
        }
        return (bool) preg_match('/^[A-Za-z0-9_\-.:]+$/', $id);
    }

    /**
     * Submit order request. Default currency UGX.
     *
     * @param array $params id (required), amount (required), description (required), callback_url (required),
     *                      notification_id (required), billing_address (required: email_address or phone_number),
     *                      currency (optional, default UGX), branch, redirect_mode, cancellation_url,
     *                      account_number, subscription_details { start_date, end_date, frequency }
     * @return array { success: bool, order_tracking_id?: string, redirect_url?: string, message?: string, ... }
     */
    public function submitOrder(array $params): array
    {
        $id = $params['id'] ?? '';
        if (!self::validateMerchantReference($id)) {
            return [
                'success' => false,
                'message' => 'Invalid id: max 50 chars, only alphanumeric, dash, underscore, dot, colon allowed.',
            ];
        }

        $billing = $params['billing_address'] ?? [];
        $email = $billing['email_address'] ?? '';
        $phone = $billing['phone_number'] ?? '';
        if (empty($email) && empty($phone)) {
            return [
                'success' => false,
                'message' => 'billing_address must contain email_address or phone_number',
            ];
        }

        $currency = $params['currency'] ?? $this->currency;
        $payload = [
            'id'               => $id,
            'currency'         => $currency,
            'amount'           => (float) ($params['amount'] ?? 0),
            'description'      => $params['description'] ?? '',
            'callback_url'     => $params['callback_url'] ?? '',
            'notification_id'  => $params['notification_id'] ?? '',
            'billing_address'  => [
                'email_address' => $billing['email_address'] ?? '',
                'phone_number'  => $billing['phone_number'] ?? '',
                'country_code'  => $billing['country_code'] ?? 'UG',
                'first_name'    => $billing['first_name'] ?? '',
                'middle_name'   => $billing['middle_name'] ?? '',
                'last_name'     => $billing['last_name'] ?? '',
                'line_1'        => $billing['line_1'] ?? '',
                'line_2'        => $billing['line_2'] ?? '',
                'city'          => $billing['city'] ?? '',
                'state'         => $billing['state'] ?? '',
                'postal_code'   => $billing['postal_code'] ?? '',
                'zip_code'      => $billing['zip_code'] ?? '',
            ],
        ];

        if (!empty($params['redirect_mode'])) {
            $payload['redirect_mode'] = $params['redirect_mode'];
        }
        if (!empty($params['cancellation_url'])) {
            $payload['cancellation_url'] = $params['cancellation_url'];
        }
        if (!empty($params['branch'])) {
            $payload['branch'] = $params['branch'];
        }
        if (isset($params['account_number']) && $params['account_number'] !== '') {
            $payload['account_number'] = $params['account_number'];
        }
        if (!empty($params['subscription_details']) && is_array($params['subscription_details'])) {
            $payload['subscription_details'] = $params['subscription_details'];
        }

        $result = $this->request('POST', '/Transactions/SubmitOrderRequest', $payload);
        if (!$result['success']) {
            return $result;
        }

        $data = $result['data'];
        $orderTrackingId = $data['order_tracking_id'] ?? null;
        $redirectUrl = $data['redirect_url'] ?? null;
        if (!$redirectUrl) {
            return [
                'success' => false,
                'message' => $data['message'] ?? 'No redirect_url in response',
                'data'    => $data,
            ];
        }

        return [
            'success'           => true,
            'order_tracking_id' => $orderTrackingId,
            'merchant_reference' => $data['merchant_reference'] ?? $id,
            'redirect_url'      => $redirectUrl,
            'data'              => $data,
        ];
    }

    /**
     * Get transaction status by PesaPal order tracking ID.
     *
     * @return array { success: bool, payment_status_description?: string, confirmation_code?: string, data?: array, ... }
     */
    public function getTransactionStatus(string $orderTrackingId): array
    {
        $path = '/Transactions/GetTransactionStatus?orderTrackingId=' . urlencode($orderTrackingId);
        $result = $this->request('GET', $path);
        if (!$result['success']) {
            return $result;
        }

        $data = $result['data'];
        return [
            'success'                    => true,
            'payment_status_description' => $data['payment_status_description'] ?? null,
            'status_code'                => $data['status_code'] ?? null,
            'confirmation_code'          => $data['confirmation_code'] ?? null,
            'amount'                    => $data['amount'] ?? null,
            'currency'                   => $data['currency'] ?? null,
            'merchant_reference'         => $data['merchant_reference'] ?? null,
            'payment_method'             => $data['payment_method'] ?? null,
            'data'                       => $data,
        ];
    }

    /**
     * Refund request. Uses confirmation_code from GetTransactionStatus.
     *
     * @param string $confirmationCode From GetTransactionStatus response
     * @param float $amount Amount to refund
     * @param string $username Identity of user initiating refund
     * @param string $remarks Reason for refund
     */
    public function refundRequest(string $confirmationCode, float $amount, string $username, string $remarks): array
    {
        $result = $this->request('POST', '/Transactions/RefundRequest', [
            'confirmation_code' => $confirmationCode,
            'amount'            => $amount,
            'username'          => $username,
            'remarks'           => $remarks,
        ]);
        if (!$result['success']) {
            return $result;
        }
        return [
            'success' => true,
            'message' => $result['data']['message'] ?? 'Refund request received',
            'data'    => $result['data'],
        ];
    }

    /**
     * Cancel order (failed or pending only, once).
     */
    public function cancelOrder(string $orderTrackingId): array
    {
        $result = $this->request('POST', '/Transactions/CancelOrder', [
            'order_tracking_id' => $orderTrackingId,
        ]);
        if (!$result['success']) {
            return $result;
        }
        return [
            'success' => true,
            'message' => $result['data']['message'] ?? 'Order cancelled',
            'data'    => $result['data'],
        ];
    }

    /**
     * Default currency (UGX).
     */
    public function getDefaultCurrency(): string
    {
        return $this->currency;
    }
}
