<?php

namespace Openpay\Data;

use Openpay\Data\Openpay as Openpay;

class OpenpayApiConnector
{

    private static $instance;
    private $apiKey;

    private function __construct()
    {
        $this->apiKey = '';
    }

    private static function getInstance()
    {
        if (!self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    // ---------------------------------------------------------
    // ------------------  PRIVATE FUNCTIONS  ------------------

    private function _request($method, $url, $params)
    {
        if (!class_exists('Openpay\\Data\\Openpay')) {
            throw new OpenpayApiError("Library install error, there are some missing classes");
        }
        OpenpayApiConsole::trace('OpenpayApiConnector @_request');

        $myId = Openpay::getId();
        if (!$myId) {
            throw new OpenpayApiAuthError("Empty or no Merchant ID provided");
        } else if (!preg_match('/^[a-z0-9]{20}$/i', $myId)) {
            throw new OpenpayApiAuthError("Invalid Merchant ID '" . $myId . "'");
        }

        $myApiKey = Openpay::getApiKey();
        if (!$myApiKey) {
            throw new OpenpayApiAuthError("Empty or no Private Key provided");
        } else if (!preg_match('/^sk_[a-z0-9]{32}$/i', $myApiKey)) {
            throw new OpenpayApiAuthError("Invalid Private Key '" . $myApiKey . "'");
        }

        $absUrl = Openpay::getEndpointUrl();
        if (!$absUrl) {
            throw new OpenpayApiConnectionError("No API endpoint set");
        }
        $absUrl .= '/' . $myId . $url;

        //$params = self::_encodeObjects($params);

        $userAgent = Openpay::getUserAgent();

        if (empty($userAgent))
            $headers = array('User-Agent: OpenpayPhp/v2');
        else
            $headers = array('User-Agent: ' . $userAgent);

        list($rbody, $rcode) = $this->_curlRequest($method, $absUrl, $headers, $params, $myApiKey);
        return $this->interpretResponse($rbody, $rcode);
    }

    private function _curlRequest($method, $absUrl, $headers, $params, $auth = null)
    {
        OpenpayApiConsole::trace('OpenpayApiConnector @_curlRequest');

        $opts = array();
        if (!is_array($headers)) {
            $headers = array();
        }

        if ($method == 'get') {
            $opts[CURLOPT_HTTPGET] = 1;
            if (count($params) > 0) {
                $encoded = $this->encodeToQueryString($params);
                $absUrl = $absUrl . '?' . $encoded;
            }
        } else if ($method == 'post') {
            $data = $this->encodeToJson($params);
            $opts[CURLOPT_POST] = 1;
            $opts[CURLOPT_POSTFIELDS] = $data;
            array_push($headers, 'Content-Type: application/json');
            array_push($headers, 'Content-Length: ' . strlen($data));
        } else if ($method == 'put') {
            $data = $this->encodeToJson($params);
            $opts[CURLOPT_CUSTOMREQUEST] = 'PUT';
            $opts[CURLOPT_POSTFIELDS] = $data;
            array_push($headers, 'Content-Type: application/json');
            array_push($headers, 'Content-Length: ' . strlen($data));
        } else if ($method == 'delete') {
            $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
            if (count($params) > 0) {
                $encoded = $this->encodeToQueryString($params);
                $absUrl = $absUrl . '?' . $encoded;
            }
        } else {
            throw new OpenpayApiError("Invalid request method '" . $method . "'");
        }


        $opts[CURLOPT_URL] = $absUrl;
        $opts[CURLOPT_RETURNTRANSFER] = TRUE;
        $opts[CURLOPT_CONNECTTIMEOUT] = 30;
        $opts[CURLOPT_TIMEOUT] = 80;
        $opts[CURLOPT_HTTPHEADER] = $headers;
        $opts[CURLOPT_SSL_VERIFYPEER] = TRUE;

        if ($auth) {
            $opts[CURLOPT_USERPWD] = $auth . ':';
        }

        $curl = curl_init();
        curl_setopt_array($curl, $opts);

        OpenpayApiConsole::debug('Executing cURL: ' . strtoupper($method) . ' > ' . $absUrl);

        $rbody = curl_exec($curl);
        $errorCode = curl_errno($curl);

        // if request fails because bad certificate verification, then
        // retry the request by using the CA certificates bundle
        // CURLE_SSL_CACERT || CURLE_SSL_CACERT_BADFILE
        if ($errorCode == 60 || $errorCode == 77) {
            curl_setopt($curl, CURLOPT_CAINFO, dirname(__FILE__) . '/cacert.pem');
            $rbody = curl_exec($curl);
        }

        if ($rbody === false) {
            OpenpayApiConsole::error('cURL request error: ' . curl_errno($curl));
            $message = curl_error($curl);
            $errorCode = curl_errno($curl);
            curl_close($curl);

            $this->handleCurlError($errorCode, $message);
        }
        $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

        curl_close($curl);

        if (mb_detect_encoding($rbody, 'UTF-8', true) != 'UTF-8') {
            OpenpayApiConsole::warn('Response body is not an UTF-8 string');
        }

        OpenpayApiConsole::debug('cURL body: ' . $rbody);
        OpenpayApiConsole::debug('cURL code: ' . $rcode);

        return array($rbody, $rcode);
    }

    private function encodeToQueryString($arr, $prefix = null)
    {
        if (!is_array($arr))
            return $arr;

        $r = array();
        foreach ($arr as $k => $v) {
            if (is_null($v))
                continue;

            if ($prefix && $k && !is_int($k))
                $k = $prefix . "[" . $k . "]";
            else if ($prefix)
                $k = $prefix . "[]";

            if (is_array($v)) {
                $r[] = $this->encodeToQueryString($v, $k, true);
            } else {
                $r[] = urlencode($k) . "=" . urlencode($v);
            }
        }
        $string = implode("&", $r);
        OpenpayApiConsole::debug('Query string: ' . $string);
        return $string;
    }

    private function encodeToJson($arr)
    {
        $encoded = json_encode($arr);
        if (mb_detect_encoding($encoded, 'UTF-8', true) != 'UTF-8') {
            $encoded = utf8_encode($encoded);
        }
        OpenpayApiConsole::debug('JSON UTF8 string: ' . $encoded);
        return $encoded;
    }

    private function interpretResponse($responseBody, $responseCode)
    {
        OpenpayApiConsole::trace('OpenpayApiConnector @interpretResponse');
        try {
            // return json as an array NOT an object
            if (!empty($responseBody)) {
                $traslatedResponse = json_decode($responseBody, true);
            } else {
                $traslatedResponse = array();
            }
        } catch (\Exception $e) {
            throw new OpenpayApiRequestError("Invalid response: " . $responseBody, $responseCode);
        }

        if ($responseCode < 200 || $responseCode >= 300) {
            OpenpayApiConsole::error('Request finished with HTTP code ' . $responseCode);
            $this->handleRequestError($responseBody, $responseCode, $traslatedResponse);
            return array();
        }
        return $traslatedResponse;
    }

    private function handleRequestError($responseBody, $responseCode, $traslatedResponse)
    {
        if (!is_array($traslatedResponse) || !isset($traslatedResponse['error_code'])) {
            throw new OpenpayApiRequestError("Invalid response body received from Openpay API Server");
        }

        $message = isset($traslatedResponse['description']) ? $traslatedResponse['description'] : 'No description';
        $error = $traslatedResponse['error_code'];
        $category = isset($traslatedResponse['category']) ? $traslatedResponse['category'] : null;
        $request_id = isset($traslatedResponse['request_id']) ? $traslatedResponse['request_id'] : null;
        $fraud_rules = isset($traslatedResponse['fraud_rules']) ? $traslatedResponse['fraud_rules'] : null;

        switch ($responseCode) {

            // Unauthorized - Forbidden
            case 401:
            case 403:
                throw new OpenpayApiAuthError($message, $error, $category, $request_id, $responseCode, $fraud_rules);
                break;

            // Bad Request - Request Entity too large - Request Entity too large - Internal Server Error - Service Unavailable
            case 400:
            case 404:
            case 413:
            case 422:
            case 500:
            case 503:
                throw new OpenpayApiRequestError($message, $error, $category, $request_id, $responseCode, $fraud_rules);
                break;

            // Payment Required - Conflict - Preconditon Failed - Unprocessable Entity - Locked
            case 402:
            case 409:
            case 412:
            case 423:
                throw new OpenpayApiTransactionError($message, $error, $category, $request_id, $responseCode, $fraud_rules);
                break;

            // Not Found
            default:
                throw new OpenpayApiError($message, $error, $category, $request_id, $responseCode, $fraud_rules);
        }
    }

    private function handleCurlError($errorCode, $message)
    {
        switch ($errorCode) {
            case CURLE_COULDNT_CONNECT:
            case CURLE_COULDNT_RESOLVE_HOST:
            case CURLE_OPERATION_TIMEOUTED:
                $msg = "Could not connect to Openpay.  Please check your internet connection and try again";
                break;
            default:
                $msg = "Unexpected error connecting to Openpay";
        }

        $msg .= " (Network error " . $errorCode . ")";
        throw new OpenpayApiConnectionError($msg);
    }

    // ---------------------------------------------------------
    // ------------------  PUBLIC FUNCTIONS  -------------------

    public static function request($method, $url, $params = null)
    {
        OpenpayApiConsole::trace('OpenpayApiConnector @request ' . $url);

        if (!$params) {
            $params = array();
        }

        $method = strtolower($method);
        if (!in_array($method, array('get', 'post', 'delete', 'put'))) {
            throw new OpenpayApiError("Invalid request method '" . $method . "'");
        }

        $connector = self::getInstance();
        return $connector->_request($method, $url, $params);
    }

}

?>
