<?php

namespace onespace\tools\activeApi\components\clients;

use Exception;
use onespace\tools\activeApi\helpers\ContextHelper;
use onespace\tools\activeApi\models\zoho\ZohoSettings;
use Yii;
use yii\base\Component;
use yii\helpers\ArrayHelper;
use yii\httpclient\Client;

/**
 * Client for connecting to Zoho's books API
 * @link [API link notes](https://help.zoho.com/portal/en/community/topic/update-regarding-zoho-finance-applications-domain-for-api-users-2-2-2024)
 */

class ZohoClient extends Component {
    use ContextHelper;

    public string $clientId;
    public string $clientSecret;
    public string $orgId;
    public string $grantToken;
    public string $accessToken;
    public string $refreshToken;

    public string $activeEndpoint;

    protected const OAUTH_URL = 'https://accounts.zoho.com/oauth/v3/device/';
    protected const REFRESH_URL = 'https://accounts.zoho.com/oauth/v2/token';

    public const SERVER_URL_BOOKS = 'https://www.zohoapis.com/books/v3/';
    public const SERVER_URL_INVOICE = 'https://www.zohoapis.com/invoice/v3';
    public const SERVER_URL_BILLING = 'https://www.zohoapis.com/billing/v1';
    /**
     * @see https://www.zoho.com/crm/developer/docs/api/v7/get-records.html
     */
    public const SERVER_URL_CRM = 'https://www.zohoapis.com/crm/v7';

    protected const ACCESS_TOKEN_START_COUNT = 20;

    public array $defaultHeaders = [];

    public function init(): void {
        parent::init();
        $this->setTokens();
    }

    public function setActiveEndpoint(string $endpoint): static {
        $availableEndpoints = [
            self::SERVER_URL_BOOKS,
            self::SERVER_URL_INVOICE,
            self::SERVER_URL_BILLING,
        ];

        if (!in_array($endpoint, $availableEndpoints)) {
            throw new Exception("Invalid endpoint {$endpoint}");
        }

        $this->activeEndpoint = $endpoint;
        return $this;
    }

    public function setTokens(): void {
        $tokens = ArrayHelper::map(ZohoSettings::findAllTokens(), 'setting_key', 'setting_value');
        if (isset($tokens['accessToken'])) {
            $this->accessToken = $tokens['accessToken'];
        }
        if (isset($tokens['refreshToken'])) {
            $this->refreshToken = $tokens['refreshToken'];
        }
    }

    protected function getRawClient(): Client {
        return new Client();
    }

    public function getClient(): Client {
        if (!$this->validateAccessToken()) {
            $regenrated = $this->regenerateAccessToken();
            if (!ArrayHelper::getValue($regenrated, "success", false)) {
                throw new Exception("Unable to regenerated Zoho Access Token");
            }
        }
        $client = new Client();

        $this->defaultHeaders = ['Authorization' => "Zoho-oauthtoken {$this->accessToken}"];

        return $client;
    }

    public function getAllContacts(): array {
        $complete = false;
        $data = [];
        $page = 1;
        $perPage = 200;
        $this->setActiveEndpoint(self::SERVER_URL_BOOKS);
        do {
            $client = $this->getClient();
            $url = $this->activeEndpoint . 'contacts?' . http_build_query([
                'organization_id' => $this->orgId,
                'page' => $page,
                'per_page' => $perPage,
            ]);

            $response = $client->get($url, headers: $this->defaultHeaders)->send();

            if (!$response->isOk) {
                return [
                    'success' => false,
                    'data' => $response->content,
                ];
            }

            $results = $response->data;
            $data = array_merge($data, $results['contacts']);
            $pageContext = $results['page_context'];
            $page++;
            $complete = ($pageContext['has_more_page'] ?? 1) == 0;
        } while (!$complete);

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

    public function getContactPerson(string $id): array {
        $client = $this->getClient();
        $this->setActiveEndpoint(self::SERVER_URL_BOOKS);
        $url = $this->activeEndpoint . "contacts/{$id}/contactpersons?" . http_build_query([
            'organization_id' => $this->orgId,
        ]);

        $response = $client->get($url, headers: $this->defaultHeaders)->send();

        if (!$response->isOk) {
            return [
                'success' => false,
                'data' => $response->content,
            ];
        }

        return [
            'success' => true,
            'data' => $response->data,
        ];
    }

    public function generateGrantToken(): array {
        $client = $this->getRawClient();

        $scopes = ArrayHelper::getColumn(ZohoSettings::findAll([
            'setting_group' => 'oAuthScope',
            'setting_value' => 1,
        ]), 'setting_key');

        $url = self::OAUTH_URL . 'code?' . http_build_query([
            'grant_type'  => 'device_request',
            'client_id'   => $this->clientId,
            'scope'       => implode(',', $scopes),
            'access_type' => 'offline',
        ]);

        $response = $client->post($url)->send();

        if ($response->isOk) {
            return [
                'success' => true,
                'data' => $response->data,
            ];
        }

        return ['success' => false];
    }

    public function generateAccessRefreshTokens(bool $sleep = true): array {
        $client = $this->getRawClient();

        $this->grantToken ??= Yii::$app->request->post('grantToken');

        $url = self::OAUTH_URL . 'token?' . http_build_query([
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'grant_type' => 'device_token',
            'code' => $this->grantToken,
        ]);

        if ($sleep) {
            Yii::debug("Sleeping for 10s to wait for authorization", __METHOD__);
            sleep(10);
        }

        $response = $client->post($url)->send();

        if ($response->isOk) {
            $data = $response->data;

            $token = ArrayHelper::getValue($data, "access_token", "");
            $expires = ArrayHelper::getValue($data, "expires_in");

            if (empty($token) || empty($expires)) {
                Yii::error("Missing access_token or refresh_token in response:", __METHOD__);
                Yii::warning($data, __METHOD__);
                throw new Exception("Missing Tokens");
            }

            Yii::info($data, __METHOD__);
            $this->accessToken = $token;
            ZohoSettings::newOrUpdateValue('oAuth', 'accessToken', $this->accessToken);
            ZohoSettings::newOrUpdateValue('timer', 'accessTokenExpires', $expires);
            ZohoSettings::newOrUpdateValue('timer', 'accessTokenTimeStarts', (string)time());
            ZohoSettings::newOrUpdateValue('counter', 'accessTokenCounter', (string)self::ACCESS_TOKEN_START_COUNT);
            $this->refreshToken = $data['refresh_token'];
            ZohoSettings::newOrUpdateValue('oAuth', 'refreshToken', $this->refreshToken);
            return [
                'success' => true,
                'data' => $response->data,
            ];
        }

        return ['success' => false];
    }

    public function regenerateAccessToken(): array {
        $client = $this->getRawClient();

        if (!isset($this->clientId) || !isset($this->clientSecret) || !isset($this->refreshToken)) {
            if ($this->isCli()) {
                exit("\nZoho Sync not initialized. Please perform the validation in the app first.\n\n");
            } else {
                Yii::error("Zoho Sync not initialized. Please perform the validation in the app first.", __METHOD__);
                throw new Exception("Zoho Sync not initialized. Please perform the validation in the app first.");
            }
        }

        $url = self::REFRESH_URL . '?' . http_build_query([
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'refresh_token' => $this->refreshToken,
            'grant_type' => 'refresh_token',
        ]);

        $response = $client->post($url)->send();

        if ($response->isOk) {
            $data = $response->data;

            $token = ArrayHelper::getValue($data, "access_token", "");
            $expires = ArrayHelper::getValue($data, "expires_in");

            if (empty($token) || empty($expires)) {
                Yii::error("token or expires at value in response:", __METHOD__);
                Yii::warning($data, __METHOD__);
                throw new Exception("Missing Tokens");
            }

            $this->accessToken = $token;
            ZohoSettings::newOrUpdateValue('oAuth', 'accessToken', $this->accessToken);
            ZohoSettings::newOrUpdateValue('timer', 'accessTokenExpires', $expires);
            ZohoSettings::newOrUpdateValue('timer', 'accessTokenTimeStarts', (string)time());
            ZohoSettings::newOrUpdateValue('counter', 'accessTokenCounter', (string)self::ACCESS_TOKEN_START_COUNT);

            return [
                'success' => true,
                'data' => $response->data,
            ];
        }

        return ['success' => false];
    }

    public function validateAccessToken(): bool {
        $token = ZohoSettings::findOne([
            'setting_group' => 'oAuth',
            'setting_key' => 'accessToken',
        ]);
        $expires = ZohoSettings::findOne([
            'setting_group' => 'timer',
            'setting_key' => 'accessTokenExpires',
        ]);
        $start = ZohoSettings::findOne([
            'setting_group' => 'timer',
            'setting_key' => 'accessTokenTimeStarts',
        ]);
        $counter = ZohoSettings::findOne([
            'setting_group' => 'counter',
            'setting_key' => 'accessTokenCounter',
        ]);

        // If any values don't exist
        foreach ([$token, $expires, $start, $counter] as $item) {
            if ($item === null) {
                return false;
            }
        }

        // If counter less that 1
        if ($counter->setting_value < 1) {
            return false;
        }

        // If time difference between now and time set is greater than the expirey time
        if ((time() - (int)$start->setting_value) > $expires->setting_value) {
            return false;
        }
        return true;
    }
}
