<?php

namespace PortalPeople\netsuite;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use yii\base\Component;
use yii\base\Exception;
use yii\helpers\Json;

/**
 * NetSuite API Client with OAuth 2.0 JWT Authentication
 * 
 * This class provides a client for interacting with NetSuite's RESTlet API using OAuth 2.0 with JWT
 * 
 * @package PortalPeople\netsuite
 */
class NetSuiteClient extends Component
{
    /**
     * @var string NetSuite account ID
     */
    public $accountId;
    
    /**
     * @var string Client ID from NetSuite integration record
     */
    public $clientId;
    
    /**
     * @var string Client secret from NetSuite integration record
     */
    public $clientSecret;
    
    /**
     * @var string Certificate ID (Key ID) from NetSuite
     */
    public $certificateId;
    
    /**
     * @var string Path to private key file for JWT signing
     */
    public $privateKeyPath;
    
    /**
     * @var string NetSuite user internal ID for impersonation
     */
    public $userId;
    
    /**
     * @var string NetSuite REST API base URL
     */
    public $baseUrl;
    
    /**
     * @var Client Guzzle HTTP client instance
     */
    private $_httpClient;
    
    /**
     * @var string Current access token
     */
    private $_accessToken;
    
    /**
     * @var int Token expiration timestamp
     */
    private $_tokenExpiry;
    
    /**
     * @var array Default request headers
     */
    private $_defaultHeaders = [
        'Content-Type' => 'application/json',
        'Accept' => 'application/json'
    ];

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        
        if (empty($this->accountId)) {
            throw new Exception('NetSuite account ID is required');
        }
        
        if (empty($this->clientId) || empty($this->clientSecret)) {
            throw new Exception('NetSuite OAuth 2.0 client ID and secret are required');
        }
        
        if (empty($this->certificateId)) {
            throw new Exception('NetSuite certificate ID (Key ID) is required');
        }
        
        if (empty($this->privateKeyPath)) {
            throw new Exception('Private key path is required for JWT authentication');
        }
        
        if (empty($this->userId)) {
            throw new Exception('NetSuite user ID is required for impersonation');
        }
        
        if (empty($this->baseUrl)) {
            $this->baseUrl = "https://{$this->accountId}.suitetalk.api.netsuite.com/services/rest";
        }
        
        $this->_httpClient = new Client([
            'base_uri' => $this->baseUrl,
            'timeout' => 30,
            'headers' => $this->_defaultHeaders
        ]);
    }
    
    /**
     * Generate JWT assertion for OAuth 2.0
     * 
     * @return string JWT assertion
     * @throws Exception
     */
    protected function generateJWTAssertion()
    {
        if (!file_exists($this->privateKeyPath)) {
            throw new Exception("Private key file not found: {$this->privateKeyPath}");
        }
        
        $privateKey = file_get_contents($this->privateKeyPath);
        if ($privateKey === false) {
            throw new Exception("Could not read private key file: {$this->privateKeyPath}");
        }
        
        $now = time();
        $expiry = $now + 3600; // 1 hour expiration
        
        $payload = [
            'iss' => $this->clientId, // Issuer: Client ID
            'sub' => $this->userId,   // Subject: User ID for impersonation
            'aud' => "https://{$this->accountId}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token", // Audience: Token endpoint
            'exp' => $expiry,         // Expiration time
            'iat' => $now,           // Issued at time
            'scope' => 'rest_webservices' // Required scope for REST web services
        ];
        
        $header = [
            'alg' => 'ES256',
            'kid' => $this->certificateId,
            'typ' => 'JWT'
        ];
        
        try {
            return JWT::encode($payload, $privateKey, 'ES256', null, $header);
        } catch (\Exception $e) {
            throw new Exception('Failed to generate JWT: ' . $e->getMessage());
        }
    }
    
    /**
     * Get access token using JWT assertion
     * 
     * @return string Access token
     * @throws Exception
     */
    protected function getAccessToken()
    {
        // Return cached token if still valid
        if ($this->_accessToken && $this->_tokenExpiry && time() < $this->_tokenExpiry - 60) {
            return $this->_accessToken;
        }
        
        $jwtAssertion = $this->generateJWTAssertion();
        
        $tokenUrl = "https://{$this->accountId}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token";
        
        $tokenClient = new Client(['timeout' => 30]);
        
        try {
            $response = $tokenClient->post($tokenUrl, [
                'form_params' => [
                    'grant_type' => 'client_credentials',
                    'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
                    'client_assertion' => $jwtAssertion
                ],
                'headers' => [
                    'Content-Type' => 'application/x-www-form-urlencoded',
                    'Accept' => 'application/json'
                ]
            ]);
            
            $tokenData = Json::decode($response->getBody()->getContents());
            
            $this->_accessToken = $tokenData['access_token'];
            $this->_tokenExpiry = time() + ($tokenData['expires_in'] ?? 3600);
            
            return $this->_accessToken;
            
        } catch (GuzzleException $e) {
            throw new Exception('Failed to obtain access token: ' . $e->getMessage(), $e->getCode(), $e);
        }
    }
    
    /**
     * Make HTTP request to NetSuite API
     * 
     * @param string $method HTTP method
     * @param string $endpoint API endpoint
     * @param array $data Request data
     * @param array $params Query parameters
     * @return array Response data
     * @throws Exception
     */
    public function request($method, $endpoint, $data = [], $params = [])
    {
        try {
            $accessToken = $this->getAccessToken();
            
            $options = [
                'headers' => array_merge($this->_defaultHeaders, [
                    'Authorization' => 'Bearer ' . $accessToken
                ])
            ];
            
            if (!empty($params)) {
                $options['query'] = $params;
            }
            
            if (!empty($data) && in_array(strtoupper($method), ['POST', 'PUT', 'PATCH'])) {
                $options['json'] = $data;
            }
            
            $response = $this->_httpClient->request($method, $endpoint, $options);
            $responseData = Json::decode($response->getBody()->getContents());
            
            return $responseData;
            
        } catch (GuzzleException $e) {
            throw new Exception('NetSuite API request failed: ' . $e->getMessage(), $e->getCode(), $e);
        }
    }
    
    /**
     * Perform GET request
     * 
     * @param string $endpoint API endpoint
     * @param array $params Query parameters
     * @return array Response data
     */
    public function get($endpoint, $params = [])
    {
        return $this->request('GET', $endpoint, [], $params);
    }
    
    /**
     * Perform POST request
     * 
     * @param string $endpoint API endpoint
     * @param array $data Request data
     * @param array $params Query parameters
     * @return array Response data
     */
    public function post($endpoint, $data = [], $params = [])
    {
        return $this->request('POST', $endpoint, $data, $params);
    }
    
    /**
     * Perform PUT request
     * 
     * @param string $endpoint API endpoint
     * @param array $data Request data
     * @param array $params Query parameters
     * @return array Response data
     */
    public function put($endpoint, $data = [], $params = [])
    {
        return $this->request('PUT', $endpoint, $data, $params);
    }
    
    /**
     * Perform PATCH request
     * 
     * @param string $endpoint API endpoint
     * @param array $data Request data
     * @param array $params Query parameters
     * @return array Response data
     */
    public function patch($endpoint, $data = [], $params = [])
    {
        return $this->request('PATCH', $endpoint, $data, $params);
    }
    
    /**
     * Perform DELETE request
     * 
     * @param string $endpoint API endpoint
     * @param array $params Query parameters
     * @return array Response data
     */
    public function delete($endpoint, $params = [])
    {
        return $this->request('DELETE', $endpoint, [], $params);
    }
}