<?php

namespace WHMCS\Module\Addon\creatia_button\Client;

use Exception;

/**
 * Class for interacting with Creatia.
 */
class PhpCreatia {
    // Constants
    private const CREATIA_AUTH_ISSUE = 'auth/issue/';
    private const CREATIA_AUTH_REVOKE = 'auth/revoke/';
    private const CREATIA_ASSOCIATE_SUBNET = 'internal/organisation/{id}/network';
    private const CREATIA_EXTERNAL_IDENTITIES = 'internal/organisation';
    private const CREATIA_CREATE_ORGANISATION = 'internal/organisation';
    private const CREATIA_GET_PREFIXES = 'internal/organisation/{id}/network';
    private const CREATIA_UPDATE_ORGANISATION = 'internal/v2/organisation/{id}';
    private const CREATIA_UPDATE_MERCURY = 'internal/organisation/{id}/mercury';
    private const CREATIA_GET_RIGHT = 'internal/organisation/{id}/right';
    private const CREATIA_GRANT_RIGHT = 'internal/organisation/{id}/right';
    private const CREATIA_REVOKE_RIGHT = 'internal/organisation/{id}/right';
    private const CREATIA_GET_USER = 'internal/user';
    private const CREATIA_CREATE_USER = 'internal/v2/organisation/{id}/user';

    private string $identifyToken;
    private string $secretToken;
    private string $endpoint;
    private ?string $bearerToken = null;
    private float $bearerExpires = 0;

    /**
     * Constructor
     */
    public function __construct(string $endpoint, string $identifyToken, string $secretToken) {
        $this->endpoint = "$endpoint/api/";
        $this->identifyToken = $identifyToken;
        $this->secretToken = $secretToken;
    }


    /**
     * Check if the current token is expired
     */
    private function isTokenExpired(): bool {
        return (microtime(true) > $this->bearerExpires);
    }

    /**
     * Authenticate with Creatia API and obtain a bearer token
     */
    public function authenticate(): bool {
        if ($this->isTokenExpired()) {
            $params = [
                'identifier' => $this->identifyToken,
                'token' => $this->secretToken
            ];
            
            $response = $this->post(self::CREATIA_AUTH_ISSUE, $params, false);
            
            if (isset($response["bearer"]) && isset($response["expiry"])) {
                $this->bearerToken = $response["bearer"];
                $this->bearerExpires = $response["expiry"];
                return true;
            }
            
            throw new Exception("Failed authentication");
        }
        
        return true;
    }

    /**
     * Revoke the current bearer token
     */
    public function revoke(): bool {
        $this->authenticate();

        if ($this->bearerToken !== null) {
            $this->post(self::CREATIA_AUTH_REVOKE, [], false);
            $this->bearerToken = null;
            $this->bearerExpires = 0;
            return true;
        }
        
        return false;
    }

    /**
     * Perform a GET request to the Creatia API
     */
    public function get(string $endpoint, array $params = []): array {
        $this->authenticate();
        
        $url = $this->endpoint . $endpoint;
        if (!empty($params)) {
            $url .= '?' . http_build_query($params);
        }
        
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $this->getHeaders());
        curl_setopt($curl, CURLOPT_TIMEOUT, 15);
        
        $response = curl_exec($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        
        if (curl_errno($curl)) {
            $error = curl_error($curl);
            curl_close($curl);
            throw new Exception("cURL Error: $error");
        }
        
        curl_close($curl);
        
        if ($httpCode !== 200) {
            throw new Exception("HTTP Error: $httpCode - $response");
        }
        
        return $this->decode($response, $endpoint);
    }

    /**
     * Perform a POST request to the Creatia API
     * 
     * @param string $endpoint API endpoint
     * @param array $payload Data to send in request body
     * @param bool $authenticate Whether to authenticate before making the request (default: true)
     * @param array $files Files to upload (optional)
     * @return array Response data as an associative array
     */
    public function post(string $endpoint, array $payload, bool $authenticate = true, array $files = []): array {
        if ($authenticate) {
            $this->authenticate();
        }
        
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $this->endpoint . $endpoint);
        curl_setopt($curl, CURLOPT_POST, 1);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_TIMEOUT, 15);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $this->getHeaders());
        
        if (empty($files)) {
            // Regular JSON post
            curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($payload));
            curl_setopt($curl, CURLOPT_HTTPHEADER, array_merge(
                $this->getHeaders(), 
                ['Content-Type: application/json']
            ));
        } else {
            // Multipart form with files
            $postFields = $payload;
            foreach ($files as $key => $filePath) {
                $postFields[$key] = curl_file_create($filePath);
            }
            curl_setopt($curl, CURLOPT_POSTFIELDS, $postFields);
            // Let cURL set the Content-Type header automatically for multipart/form-data
        }
        
        $response = curl_exec($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        
        if (curl_errno($curl)) {
            $error = curl_error($curl);
            curl_close($curl);
            throw new Exception("cURL Error: $error");
        }
        
        curl_close($curl);

        if ($httpCode !== 200) {
            throw new Exception("HTTP Error: $httpCode - $response");
        }
        
        return $this->decode($response, $endpoint);
    }

    /**
     * Perform a PUT request to the Creatia API
     */
    public function put(string $endpoint, array $payload): array {
        $this->authenticate();
        
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $this->endpoint . $endpoint);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT');
        curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($payload));
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HTTPHEADER, array_merge(
            $this->getHeaders(), 
            ['Content-Type: application/json']
        ));
        curl_setopt($curl, CURLOPT_TIMEOUT, 15);
        
        $response = curl_exec($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        
        if (curl_errno($curl)) {
            $error = curl_error($curl);
            curl_close($curl);
            throw new Exception("cURL Error: $error");
        }
        
        curl_close($curl);
        
        if ($httpCode !== 200) {
            throw new Exception("HTTP Error: $httpCode - $response");
        }
        
        return $this->decode($response, $endpoint);
    }

    /**
     * Perform a PATCH request to the Creatia API
     */
    public function patch(string $endpoint, array $payload): array {
        $this->authenticate();
        
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $this->endpoint . $endpoint);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PATCH');
        curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($payload));
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HTTPHEADER, array_merge(
            $this->getHeaders(), 
            ['Content-Type: application/json']
        ));
        curl_setopt($curl, CURLOPT_TIMEOUT, 15);
        
        $response = curl_exec($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        
        if (curl_errno($curl)) {
            $error = curl_error($curl);
            curl_close($curl);
            throw new Exception("cURL Error: $error");
        }
        
        curl_close($curl);
        
        if ($httpCode !== 200) {
            throw new Exception("HTTP Error: $httpCode - $response");
        }
        
        return $this->decode($response, $endpoint);
    }

    /**
     * Perform a DELETE request to the Creatia API
     */
    public function delete(string $endpoint, array $payload = []): array {
        $this->authenticate();
        
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $this->endpoint . $endpoint);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
        
        if (!empty($payload)) {
            curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($payload));
            curl_setopt($curl, CURLOPT_HTTPHEADER, array_merge(
                $this->getHeaders(), 
                ['Content-Type: application/json']
            ));
        } else {
            curl_setopt($curl, CURLOPT_HTTPHEADER, $this->getHeaders());
        }
        
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_TIMEOUT, 15);
        
        $response = curl_exec($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        
        if (curl_errno($curl)) {
            $error = curl_error($curl);
            curl_close($curl);
            throw new Exception("cURL Error: $error");
        }
        
        curl_close($curl);
        
        if ($httpCode !== 200) {
            throw new Exception("HTTP Error: $httpCode - $response");
        }
        
        return $this->decode($response, $endpoint);
    }

    /**
     * Decode a response body.
     *
     * json_decode() returns null for an empty or malformed body, which would be a TypeError
     * against the `array` return type of the verbs above rather than something a caller can
     * catch as an Exception. Fail as an Exception instead.
     */
    private function decode(?string $response, string $context): array {
        $decoded = json_decode((string) $response, true);

        if (!is_array($decoded)) {
            throw new Exception("Malformed response from $context: " . substr((string) $response, 0, 200));
        }

        return $decoded;
    }

    /**
     * Get HTTP headers for API requests
     */
    private function getHeaders(): array {
        $headers = ['User-Agent: phpCreatia'];
        
        if ($this->bearerToken !== null && !$this->isTokenExpired()) {
            $headers[] = 'Authorization: Bearer ' . $this->bearerToken;
        }

        return $headers;
    }

    // From this point on you have functions made specifically for just talking to Creatia without you needing to re-invent the wheel. Enjoy!

    /**
     * Associate a subnet with an organization in Creatia
     * 
     * @param string $creatiaId The organizational identifier for the organization
     * @param string $subnet The subnet in CIDR compliant format (e.g. "203.1.1.3/32")
     * @param string $name A user-friendly name for the prefix
     * @param bool $prefixOnly Whether subtenancy will grant control of this prefix itself
     * @param bool $childrenAllowed Whether subtenancy will grant control of the prefixes subnets
     * @param bool $override Whether typical prefix safety is respected
     * @param string $brand The brand responsible for this action (must be one of 'gsl', 'go-dedicated', 'streamline')
     * @return array Response data from the API
     */
    public function associateSubnet(string $creatiaId, string $subnet, string $name, bool $prefixOnly = true, bool $childrenAllowed = true, bool $override = false, string $brand = 'gsl'): array {
        $this->authenticate();
        
        $payload = [
            'subnet' => $subnet,
            'name' => $name,
            'prefix_only' => $prefixOnly,
            'children_allowed' => $childrenAllowed,
            'override' => $override,
            'brand' => $brand
        ];
        
        return $this->post(str_replace('{id}', $creatiaId, self::CREATIA_ASSOCIATE_SUBNET), $payload);
    }

    /**
     * Get external identities for organizations
     * 
     * @param string $application The application identifier (e.g., 'gsl-whmcs', 'gd-whmcs')
     * @param string|null $identity Optional specific identity to retrieve
     * @return array List of organizations with their Creatia identifiers
     */
    public function getExternalIdentities(string $application, ?string $identity = null): array {
        $params = ['application' => $application];
        
        if ($identity !== null) {
            $params['identity'] = $identity;
        }
        
        return $this->get(self::CREATIA_EXTERNAL_IDENTITIES, $params);
    }
    
    /**
     * Create a new organization in Creatia
     * 
     * @param string $name The name for the organization
     * @param array $identifiers Array of identifier objects with 'key' and 'identity' properties
     * @param string $subtenant The brand association ('gsl', 'go-dedicated', 'streamline')
     * @return array Response data from the API
     */
    public function createOrganisation(string $name, array $identifiers, string $subtenant = 'gsl'): array {
        $payload = [
            'name' => $name,
            'identifiers' => $identifiers,
            'subtenant' => $subtenant
        ];

        return $this->post(self::CREATIA_CREATE_ORGANISATION, $payload);
    }

    /**
     * Get subnets for a specific organization
     * 
     * @param string $creatiaId The Creatia ID of the organization
     * @param int $page Page number for pagination (optional)
     * @param int $perPage Number of items per page (optional)
     * @param string $sortField Field to sort by (optional)
     * @param string $sortOrder Sort direction ('asc' or 'desc', optional)
     * @return array List of subnets with their configuration details
     * @throws Exception If the request fails or validation fails
     */
    public function getSubnets(string $creatiaId, int $page = 1, int $perPage = 10, string $sortField = 'prefix', string $sortOrder = 'asc'): array {
        $params = [
            'page' => $page,
            'per_page' => $perPage,
            'sort_field' => $sortField,
            'sort_order' => $sortOrder
        ];
        
        return $this->get(str_replace("{id}", $creatiaId, self::CREATIA_GET_PREFIXES), $params);
    }

     /**
     * Update organization external identifiers
     * 
     * @param string $organisationId The ID of the organization to update
     * @param array $identifiers Array of identifier objects. This client targets the v2
     *                           endpoint, which requires 'key', 'identity' AND 'mode'
     *                           ('add' or 'remove') on every entry - v1 took the first two.
     * @return array Response data from the API
     * @throws Exception If the request fails
     */
    public function updateOrganisation(string $organisationId, array $identifiers): array {
        $payload = [
            'identifiers' => $identifiers
        ];
        
        return $this->put(str_replace("{id}", $organisationId, self::CREATIA_UPDATE_ORGANISATION), $payload);
    }
    
    /**
     * Update mercury settings for an organization
     * 
     * @param string $organisationId The ID of the organization to update
     * @param int|float $bandwidth The aggregate bandwidth value to set
     * @return array Response data from the API
     * @throws Exception If the request fails
     */
    public function updateMercury(string $organisationId, int|float $bandwidth): array {
        $payload = [
            'bandwidth' => $bandwidth
        ];
        
        return $this->put(str_replace("{id}", $organisationId, self::CREATIA_UPDATE_MERCURY), $payload);
    }
    
    /**
     * Get the organisations known product rights
     * 
     * @param string $organisationId The ID of the organization to update
     * @return array Response data from the API
     * @throws Exception If the request fails
     */
    public function getProductRight(string $organisationId): array {        
        return $this->get(str_replace("{id}", $organisationId, self::CREATIA_GET_RIGHT), []);
    }

    /**
     * Add a product right to an organization
     * 
     * @param string $organisationId The ID of the organization to update
     * @param string $right The name of the product right to add
     * @return array Response data from the API
     * @throws Exception If the request fails
     */
    public function addProductRight(string $organisationId, string $right): array {
        $payload = [
            'right' => $right
        ];
        
        return $this->post(str_replace("{id}", $organisationId, self::CREATIA_GRANT_RIGHT), $payload);
    }
    
    /**
     * Remove a product right from an organization
     * 
     * @param string $organisationId The ID of the organization to update
     * @param string $right The name of the product right to remove
     * @return array Response data from the API
     * @throws Exception If the request fails
     */
    public function removeProductRight(string $organisationId, string $right): array {
        $payload = [
            'right' => $right
        ];
        
        return $this->delete(str_replace("{id}", $organisationId, self::CREATIA_REVOKE_RIGHT), $payload);
    }

    /**
     * Get a known user and their corresponding organisation
     * 
     * @param string $email The email address for the new user
     * @return array Response data from the API containing the user email and organisation_id
     * @throws Exception If the request fails
     */
    public function getUser(string $email): array {        
        return $this->get(self::CREATIA_GET_USER, [ 'email' => $email ]);
    }

    /**
     * Create a new user for an organization
     * 
     * @param string $organisationId The ID of the organization to create the user for
     * @param string $email The email address for the new user
     * @param string $name The name for the new user
     * @return array Response data from the API containing the user ID
     * @throws Exception If the request fails
     */
    public function createUser(string $organisationId, string $email, string $name, bool $readonly = false): array {
        $payload = [
            'email' => $email,
            'name' => $name,
            'readonly' => $readonly
        ];
        
        return $this->post(str_replace("{id}", $organisationId, self::CREATIA_CREATE_USER), $payload);
    }
}