<?php

namespace App\Controllers;

use App\Libraries\PesaPalService;
use App\Models\PaymentTransactionModel;
use CodeIgniter\RESTful\ResourceController;

class PesaPal extends ResourceController
{
    private PesaPalService $pesapalService;
    private PaymentTransactionModel $paymentModel;

    public function __construct()
    {
        $this->pesapalService = new PesaPalService();
        $this->paymentModel  = new PaymentTransactionModel();
    }

    /**
     * Map PesaPal payment_status_description to our status.
     */
    private function mapStatus(?string $paymentStatusDescription): string
    {
        if (strtoupper((string) $paymentStatusDescription) === 'COMPLETED') {
            return 'SUCCESS';
        }
        if (in_array(strtoupper((string) $paymentStatusDescription), ['FAILED', 'INVALID', 'REVERSED'], true)) {
            return 'FAILED';
        }
        return 'PENDING';
    }

    /**
     * Update local transaction from PesaPal status response and store confirmation_code in metadata.
     */
    private function updateTransactionFromStatus(string $orderTrackingId, array $statusResult): bool
    {
        if (!$statusResult['success'] || empty($statusResult['data'])) {
            return false;
        }
        $data = $statusResult['data'];
        $mappedStatus = $this->mapStatus($data['payment_status_description'] ?? null);
        $confirmationCode = $data['confirmation_code'] ?? null;
        $row = $this->paymentModel
            ->where('gateway_reference', $orderTrackingId)
            ->where('payment_method', 'pesapal')
            ->first();
        if (!$row) {
            return false;
        }
        $metadata = $row['metadata'] ? (json_decode($row['metadata'], true) ?: []) : [];
        if ($confirmationCode !== null && $confirmationCode !== '') {
            $metadata['confirmation_code'] = $confirmationCode;
        }
        $this->paymentModel->update($row['id'], [
            'status'           => $mappedStatus,
            'gateway_response' => json_encode($data),
            'metadata'         => $metadata ? json_encode($metadata) : null,
            'processed_at'     => date('Y-m-d H:i:s'),
            'updated_at'       => date('Y-m-d H:i:s'),
        ]);
        return true;
    }

    /**
     * IPN endpoint (POST). PesaPal calls this on payment status change.
     * Must return JSON: orderNotificationType, orderTrackingId, orderMerchantReference, status (200 or 500).
     */
    public function ipn()
    {
        $orderTrackingId   = $this->request->getPost('OrderTrackingId') ?? $this->request->getJSON(true)['OrderTrackingId'] ?? null;
        $merchantReference = $this->request->getPost('OrderMerchantReference') ?? $this->request->getJSON(true)['OrderMerchantReference'] ?? null;
        $notificationType  = $this->request->getPost('OrderNotificationType') ?? $this->request->getJSON(true)['OrderNotificationType'] ?? 'IPNCHANGE';

        if (empty($orderTrackingId)) {
            log_message('error', 'PesaPal IPN: missing OrderTrackingId');
            return $this->response->setJSON([
                'orderNotificationType'   => $notificationType,
                'orderTrackingId'         => '',
                'orderMerchantReference'   => $merchantReference ?? '',
                'status'                  => 500,
            ])->setStatusCode(200);
        }

        log_message('info', 'PesaPal IPN received: OrderTrackingId=' . $orderTrackingId . ', OrderMerchantReference=' . ($merchantReference ?? '') . ', OrderNotificationType=' . $notificationType);
        $statusResult = $this->pesapalService->getTransactionStatus($orderTrackingId);
        $updated = $this->updateTransactionFromStatus($orderTrackingId, $statusResult);
        $ipnStatus = $updated ? 200 : 500;
        log_message($updated ? 'info' : 'warning', 'PesaPal IPN: OrderTrackingId=' . $orderTrackingId . ' ' . ($updated ? 'transaction updated' : 'transaction not found or update failed') . ', responding status=' . $ipnStatus);

        $body = [
            'orderNotificationType' => $notificationType,
            'orderTrackingId'      => $orderTrackingId,
            'orderMerchantReference' => $merchantReference ?? '',
            'status'               => $ipnStatus,
        ];
        return $this->response->setJSON($body)->setStatusCode(200);
    }

    /**
     * Callback (GET). User is redirected here after payment. Fetch status, update DB, redirect to result page.
     */
    public function callback()
    {
        $orderTrackingId   = $this->request->getGet('OrderTrackingId');
        $merchantReference = $this->request->getGet('OrderMerchantReference');
        $notificationType  = $this->request->getGet('OrderNotificationType');

        if (empty($orderTrackingId)) {
            log_message('error', 'PesaPal callback: missing OrderTrackingId');
            return redirect()->to($this->getCallbackBaseUrl() . '/?pesapal=1&status=error');
        }

        $returnUrl = $this->request->getGet('return_url');
        log_message('info', 'PesaPal callback: OrderTrackingId=' . $orderTrackingId . ', OrderMerchantReference=' . ($merchantReference ?? ''));
        $statusResult = $this->pesapalService->getTransactionStatus($orderTrackingId);
        $this->updateTransactionFromStatus($orderTrackingId, $statusResult);
        $mappedStatus = $statusResult['success'] ? $this->mapStatus($statusResult['data']['payment_status_description'] ?? null) : 'PENDING';
        log_message('info', 'PesaPal callback: OrderTrackingId=' . $orderTrackingId . ' status=' . $mappedStatus);
        $statusSlug   = $mappedStatus === 'SUCCESS' ? 'success' : 'failed';

        $baseUrl = $this->getCallbackBaseUrl();
        $url = $baseUrl . '/pesapal-result?order_tracking_id=' . urlencode($orderTrackingId) . '&merchant_reference=' . urlencode((string) $merchantReference) . '&status=' . $statusSlug;
        if (!empty($returnUrl)) {
            $url .= '&return_url=' . rawurlencode($returnUrl);
        }
        return redirect()->to($url);
    }

    /**
     * Result page shown after payment (success or failed). Single "Close window" button.
     * Close redirect: per-order return_url (from callback_url when creating order) takes precedence;
     * else falls back to pesapal.webview_close_url for a single default app scheme.
     */
    public function result()
    {
        $status   = $this->request->getGet('status') ?? 'failed';
        $orderId  = $this->request->getGet('order_tracking_id') ?? '';
        $merchantRef = $this->request->getGet('merchant_reference') ?? '';
        $returnUrlRaw = $this->request->getGet('return_url');
        $isSuccess = (strtolower($status) === 'success');
        $config = config('PesaPal');
        $closeUrl = '';
        if (!empty($returnUrlRaw)) {
            $closeUrl = str_replace(['{status}', '{order_tracking_id}', '{merchant_reference}'], [$status, $orderId, $merchantRef], $returnUrlRaw);
        }
        if ($closeUrl === '' && $config->getWebviewCloseUrl($status, $orderId, $merchantRef) !== '') {
            $closeUrl = $config->getWebviewCloseUrl($status, $orderId, $merchantRef);
        }
        return view('pesapal_result', [
            'isSuccess'     => $isSuccess,
            'status'        => $status,
            'orderTrackingId' => $orderId,
            'merchantReference' => $merchantRef,
            'closeRedirectUrl' => $closeUrl,
        ]);
    }

    private function getCallbackBaseUrl(): string
    {
        $config = config('PesaPal');
        return rtrim($_ENV['pesapal.callback_base_url'] ?? $config->getCallbackBaseUrl(), '/');
    }

    /**
     * Submit order (API). POST JSON: id, amount, description, billing_address, optional callback_url, cancellation_url, etc.
     */
    public function order()
    {
        $json = $this->request->getJSON(true);
        if (!$json) {
            return $this->respond(['success' => false, 'message' => 'Invalid JSON'], 400);
        }

        $config = config('PesaPal');
        $notificationId = $_ENV['pesapal.notification_id'] ?? $config->notificationId;
        if (empty($notificationId)) {
            return $this->respond(['success' => false, 'message' => 'PesaPal notification_id (IPN) not configured. Register IPN first.'], 400);
        }

        $callbackBaseUrl = $this->getCallbackBaseUrl();
        $callbackUrl = $json['callback_url'] ?? $callbackBaseUrl . '/pesapal-callback';
        if (strpos($callbackUrl, 'http') !== 0) {
            $callbackUrl = $callbackBaseUrl . '/' . ltrim($callbackUrl, '/');
        }
        $cancellationUrl = $json['cancellation_url'] ?? null;
        if ($cancellationUrl !== null && strpos($cancellationUrl, 'http') !== 0) {
            $cancellationUrl = $callbackBaseUrl . '/' . ltrim($cancellationUrl, '/');
        }

        $params = [
            'id'               => $json['id'] ?? $json['merchant_reference'] ?? '',
            'amount'           => $json['amount'] ?? 0,
            'description'      => $json['description'] ?? '',
            'callback_url'     => $callbackUrl,
            'notification_id'  => $notificationId,
            'billing_address'  => $json['billing_address'] ?? [],
            'currency'         => $json['currency'] ?? $this->pesapalService->getDefaultCurrency(),
            'branch'           => $json['branch'] ?? null,
            'redirect_mode'    => $json['redirect_mode'] ?? null,
            'cancellation_url'  => $cancellationUrl,
            'account_number'   => $json['account_number'] ?? null,
            'subscription_details' => $json['subscription_details'] ?? null,
        ];

        $result = $this->pesapalService->submitOrder($params);
        if (!$result['success']) {
            return $this->respond([
                'success' => false,
                'message' => $result['message'] ?? 'Submit order failed',
            ], 400);
        }

        $merchantRef = $params['id'];
        $orderTrackingId = $result['order_tracking_id'];
        $redirectUrl = $result['redirect_url'];

        $this->paymentModel->insert([
            'transaction_id'     => $merchantRef,
            'reference'          => $orderTrackingId,
            'msisdn'             => $json['billing_address']['phone_number'] ?? '',
            'amount'             => $params['amount'],
            'status'             => 'PENDING',
            'created_at'         => date('Y-m-d H:i:s'),
            'currency'           => $params['currency'],
            'description'        => $params['description'],
            'payment_method'     => 'pesapal',
            'gateway_reference'  => $orderTrackingId,
            'gateway_response'   => json_encode($result['data'] ?? []),
        ]);
        log_message('info', 'PesaPal order submitted: merchant_reference=' . $merchantRef . ', order_tracking_id=' . $orderTrackingId . ', amount=' . $params['amount'] . ' ' . $params['currency']);

        return $this->respond([
            'success'            => true,
            'order_tracking_id'  => $orderTrackingId,
            'merchant_reference' => $merchantRef,
            'redirect_url'       => $redirectUrl,
        ]);
    }

    /**
     * Get transaction status (API). GET api/pesapal/status/{order_tracking_id}
     */
    public function status(string $orderTrackingId)
    {
        $result = $this->pesapalService->getTransactionStatus($orderTrackingId);
        if (!$result['success']) {
            return $this->respond([
                'success' => false,
                'message' => $result['message'] ?? 'Failed to get status',
            ], 400);
        }
        $this->updateTransactionFromStatus($orderTrackingId, $result);
        return $this->respond([
            'success' => true,
            'data'    => $result['data'],
            'payment_status_description' => $result['payment_status_description'] ?? null,
            'confirmation_code' => $result['confirmation_code'] ?? null,
        ]);
    }

    /**
     * Refund (API). POST JSON: order_tracking_id or confirmation_code, amount, username, remarks.
     */
    public function refund()
    {
        $json = $this->request->getJSON(true) ?? $this->request->getPost();
        if (!$json) {
            return $this->respond(['success' => false, 'message' => 'Invalid input'], 400);
        }

        $confirmationCode = $json['confirmation_code'] ?? null;
        $orderTrackingId  = $json['order_tracking_id'] ?? null;
        if (empty($confirmationCode) && !empty($orderTrackingId)) {
            $row = $this->paymentModel
                ->where('gateway_reference', $orderTrackingId)
                ->where('payment_method', 'pesapal')
                ->first();
            if ($row && !empty($row['metadata'])) {
                $meta = json_decode($row['metadata'], true);
                $confirmationCode = $meta['confirmation_code'] ?? null;
            }
            if (empty($confirmationCode) && !empty($row['gateway_response'])) {
                $gr = json_decode($row['gateway_response'], true);
                $confirmationCode = $gr['confirmation_code'] ?? null;
            }
        }
        if (empty($confirmationCode)) {
            return $this->respond(['success' => false, 'message' => 'confirmation_code required or provide order_tracking_id of a completed payment'], 400);
        }

        $amount   = (float) ($json['amount'] ?? 0);
        $username = trim((string) ($json['username'] ?? ''));
        $remarks  = trim((string) ($json['remarks'] ?? ''));
        if ($amount <= 0 || $username === '' || $remarks === '') {
            return $this->respond(['success' => false, 'message' => 'amount, username, and remarks are required'], 400);
        }

        $result = $this->pesapalService->refundRequest($confirmationCode, $amount, $username, $remarks);
        if (!$result['success']) {
            return $this->respond([
                'success' => false,
                'message' => $result['message'] ?? 'Refund request failed',
            ], 400);
        }
        return $this->respond([
            'success' => true,
            'message' => $result['message'] ?? 'Refund request received',
            'data'    => $result['data'] ?? [],
        ]);
    }

    /**
     * Cancel order (API). POST JSON: order_tracking_id.
     */
    public function cancel()
    {
        $json = $this->request->getJSON(true) ?? $this->request->getPost();
        $orderTrackingId = $json['order_tracking_id'] ?? $this->request->getPost('order_tracking_id');
        if (empty($orderTrackingId)) {
            return $this->respond(['success' => false, 'message' => 'order_tracking_id required'], 400);
        }

        $result = $this->pesapalService->cancelOrder($orderTrackingId);
        if (!$result['success']) {
            return $this->respond([
                'success' => false,
                'message' => $result['message'] ?? 'Cancel failed',
            ], 400);
        }

        $row = $this->paymentModel
            ->where('gateway_reference', $orderTrackingId)
            ->where('payment_method', 'pesapal')
            ->first();
        if ($row) {
            $this->paymentModel->update($row['id'], [
                'status'      => 'CANCELLED',
                'updated_at'  => date('Y-m-d H:i:s'),
            ]);
        }

        return $this->respond([
            'success' => true,
            'message' => $result['message'] ?? 'Order cancelled',
            'data'    => $result['data'] ?? [],
        ]);
    }

    /**
     * Register IPN (setup). POST JSON: optional url (default from config). ipn_notification_type POST.
     */
    public function registerIpn()
    {
        $config = config('PesaPal');
        $url = $_ENV['pesapal.ipn_url'] ?? $config->getIpnUrl();
        $json = $this->request->getJSON(true);
        if (!empty($json['url'])) {
            $url = $json['url'];
        }
        $result = $this->pesapalService->registerIpn($url, 'POST');
        if (!$result['success']) {
            return $this->respond([
                'success' => false,
                'message' => $result['message'] ?? 'Register IPN failed',
            ], 400);
        }
        return $this->respond([
            'success' => true,
            'ipn_id'  => $result['ipn_id'],
            'message' => 'Store this ipn_id as pesapal.notification_id for Submit Order requests.',
            'data'    => $result['data'] ?? [],
        ]);
    }

    /**
     * Get IPN list (setup). GET api/pesapal/ipn-list
     */
    public function ipnList()
    {
        $result = $this->pesapalService->getIpnList();
        if (!$result['success']) {
            return $this->respond([
                'success' => false,
                'message' => $result['message'] ?? 'Get IPN list failed',
            ], 400);
        }
        $list = is_array($result['data']) ? $result['data'] : [];
        return $this->respond([
            'success' => true,
            'data'    => $list,
        ]);
    }

    /**
     * Get transaction by order_tracking_id (internal lookup from payment_transactions).
     */
    public function transaction(string $orderTrackingId)
    {
        $row = $this->paymentModel
            ->where('gateway_reference', $orderTrackingId)
            ->where('payment_method', 'pesapal')
            ->first();
        if (!$row) {
            return $this->respond(['success' => false, 'message' => 'Transaction not found'], 404);
        }
        return $this->respond(['success' => true, 'data' => $row]);
    }
}
