<?php

/**
 * Copyright 2014-Present Couchbase, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

declare(strict_types=1);

namespace Couchbase;

use Couchbase\Exception\CouchbaseException;
use Couchbase\Exception\FeatureNotAvailableException;
use Couchbase\Exception\InvalidArgumentException;
use Couchbase\Exception\TimeoutException;
use Couchbase\Exception\UnsupportedOperationException;
use Couchbase\Management\AnalyticsIndexManager;
use Couchbase\Management\BucketManager;
use Couchbase\Management\QueryIndexManager;
use Couchbase\Management\SearchIndexManager;
use Couchbase\Management\User;
use Couchbase\Management\UserManager;
use Couchbase\Observability\ObservabilityHandler;
use Couchbase\Observability\ObservabilityContext;
use Couchbase\Observability\ObservabilityConstants;

/**
 * Cluster is an object containing functionality for performing cluster level operations
 * against a cluster and for access to buckets.
 *
 * @since 4.0.0
 */
class Cluster implements ClusterInterface
{
    private ClusterOptions $options;
    private ObservabilityContext $observability;
    private string $connectionHash;
    /**
     * @var resource
     */
    private $core;

    /**
     * @throws InvalidArgumentException
     */
    public function __construct(string $connectionString, ClusterOptions $options)
    {
        if (
            preg_match("/^protostellar:\/\//", $connectionString) ||
            preg_match("/^couchbase2:\/\//", $connectionString)
        ) {
            throw new InvalidArgumentException("Please use Cluster::connect() to connect to CNG.");
        }
        ExtensionNamespaceResolver::defineExtensionNamespace();
        // TODO(PCBC-1056): We should consider taking into account all the options that are passed to the backend for the connection hash
        $this->connectionHash = hash(
            "sha256",
            sprintf(
                "--%s--%s--%b--%b--%b--%s--",
                $connectionString,
                $options->authenticatorHash(),
                $options->enableCoreTracing(),
                $options->enableCoreMetrics(),
                $options->bufferCoreSpans(),
                COUCHBASE_EXTENSION_NAMESPACE
            )
        );
        $function = COUCHBASE_EXTENSION_NAMESPACE . '\\createConnection';
        $this->core = $function($this->connectionHash, $connectionString, $options->export());
        $this->options = $options;

        $tracer = ClusterOptions::getTracer($options);
        if (is_null($tracer)) {
            $tracer = new ThresholdLoggingTracer($this->core);
        }
        $meter = ClusterOptions::getMeter($options);
        if (is_null($meter)) {
            $meter = new LoggingMeter($this->core);
        }
        $this->observability = new ObservabilityContext($this->core, $tracer, $meter);
    }

    /**
     * @throws InvalidArgumentException
     * @throws FeatureNotAvailableException
     *
     * Note: The couchbase2:// scheme has stability @UNCOMMITTED
     */
    public static function connect(string $connectionString, ClusterOptions $options): ClusterInterface
    {
        if (preg_match("/^couchbases?:\/\//", $connectionString)) {
            return new Cluster($connectionString, $options);
        }

        if (
            preg_match("/^protostellar:\/\//", $connectionString) ||
            preg_match("/^couchbase2:\/\//", $connectionString)
        ) {
            Cluster::enableProtostellar();
        }
        return ClusterRegistry::connect($connectionString, $options);
    }

    /**
     * @throws InvalidArgumentException if runtime dependencies are missing ("protobuf" and "grpc" modules)
     */
    private static function enableProtostellar(): void
    {
        if (!extension_loaded("protobuf")) {
            throw new InvalidArgumentException("couchbase2:// protocol requires protobuf extension");
        }
        if (!extension_loaded("grpc")) {
            throw new InvalidArgumentException("couchbase2:// protocol requires grpc extension");
        }
        ClusterRegistry::registerConnectionHandler(
            "/^protostellar:\/\//",
            function (string $connectionString, ClusterOptions $options) {
                return new Protostellar\Cluster($connectionString, $options);
            }
        );

        ClusterRegistry::registerConnectionHandler(
            "/^couchbase2:\/\//",
            function (string $connectionString, ClusterOptions $options) {
                return new Protostellar\Cluster($connectionString, $options);
            }
        );
    }

    /**
     * Notifies the SDK about usage of `fork(2)` syscall. Typically PHP exposes it using `pcntl_fork()` function, but
     * the library should have chance to properly close descriptors and reach safe point to allow forking the process.
     * This is not a problem in case of `proc_open()` as in this case the memory and descriptors are not inherited by
     * the child process.
     *
     * Allowed values for `$event` are:
     *
     * * ForkEvent::PREPARE - must be used before `fork()` to ensure the SDK reaches safe point
     * * ForkEvent::CHILD - must be used in the child process, the branch where `pcntl_fork()` returns zero
     * * ForkEvent::PARENT - must be used in the parent process, the branch where `pcntl_fork()` returns pid of the child process
     *
     * In case `pcntl_fork()` returns negative value, and the application decides to continue, `notifyFork(ForkEvent::PARENT)`
     * must be invoked to resume the SDK.
     *
     * @see https://www.php.net/manual/en/function.pcntl-fork.php
     * @see https://www.php.net/manual/en/function.proc-open.php
     *
     * @param string $event type of the event to send to the library (one of the constants in ForkEvent).
     * @return void
     *
     * @since 4.2.1
     */
    public static function notifyFork(string $event)
    {
        ExtensionNamespaceResolver::defineExtensionNamespace();
        $function = COUCHBASE_EXTENSION_NAMESPACE . '\\notifyFork';
        return $function($event);
    }

    /**
     * Returns a new bucket object.
     *
     * @param string $name the name of the bucket
     *
     * @return Bucket
     * @since 4.0.0
     */
    public function bucket(string $name): BucketInterface
    {
        return new Bucket($name, $this->core, $this->observability);
    }

    /**
     * Executes a N1QL query against the cluster.
     * Note: On Couchbase Server versions < 6.5 a bucket must be opened before using query.
     *
     * @param string $statement the N1QL query statement to execute
     * @param QueryOptions|null $options the options to use when executing the query
     *
     * @return QueryResult
     * @throws InvalidArgumentException
     * @throws TimeoutException
     * @throws CouchbaseException
     * @since 4.0.0
     */
    public function query(string $statement, ?QueryOptions $options = null): QueryResult
    {
        return $this->observability->recordOperation(
            ObservabilityConstants::OP_QUERY,
            QueryOptions::getParentSpan($options),
            function (ObservabilityHandler $obsHandler) use ($statement, $options) {
                $obsHandler->addService(ObservabilityConstants::ATTR_VALUE_SERVICE_QUERY);
                $obsHandler->addQueryStatement($statement, $options);

                $function = COUCHBASE_EXTENSION_NAMESPACE . '\\query';
                $result = $function($this->core, $statement, QueryOptions::export($options), $obsHandler->getCoreSpansArray());

                return new QueryResult($result, QueryOptions::getTranscoder($options));
            }
        );
    }

    /**
     * Executes an analytics query against the cluster.
     * Note: On Couchbase Server versions < 6.5 a bucket must be opened before using analyticsQuery.
     *
     * @param string $statement the analytics query statement to execute
     * @param AnalyticsOptions|null $options the options to use when executing the query
     *
     * @return AnalyticsResult
     * @throws TimeoutException
     * @throws CouchbaseException
     * @since 4.0.0
     */
    public function analyticsQuery(string $statement, ?AnalyticsOptions $options = null): AnalyticsResult
    {
        return $this->observability->recordOperation(
            ObservabilityConstants::OP_ANALYTICS_QUERY,
            AnalyticsOptions::getParentSpan($options),
            function (ObservabilityHandler $obsHandler) use ($statement, $options) {
                $obsHandler->addService(ObservabilityConstants::ATTR_VALUE_SERVICE_ANALYTICS);
                $obsHandler->addQueryStatement($statement, $options);

                $function = COUCHBASE_EXTENSION_NAMESPACE . '\\analyticsQuery';
                $result = $function($this->core, $statement, AnalyticsOptions::export($options), $obsHandler->getCoreSpansArray());

                return new AnalyticsResult($result, AnalyticsOptions::getTranscoder($options));
            }
        );
    }

    /**
     * Executes a full text search query against the cluster.
     * Note: On Couchbase Server versions < 6.5 a bucket must be opened before using searchQuery.
     *
     * @param string $indexName the fts index to use for the query
     * @param SearchQuery $query the search query to execute
     * @param SearchOptions|null $options the options to use when executing the query
     *
     * @return SearchResult
     * @since 4.0.0
     */
    public function searchQuery(string $indexName, SearchQuery $query, ?SearchOptions $options = null): SearchResult
    {
        return $this->observability->recordOperation(
            ObservabilityConstants::OP_SEARCH_QUERY,
            SearchOptions::getParentSpan($options),
            function (ObservabilityHandler $obsHandler) use ($indexName, $query, $options) {
                $obsHandler->addService(ObservabilityConstants::ATTR_VALUE_SERVICE_SEARCH);

                $function = COUCHBASE_EXTENSION_NAMESPACE . '\\searchQuery';
                $result = $function($this->core, $indexName, json_encode($query), SearchOptions::export($options), $obsHandler->getCoreSpansArray());

                return new SearchResult($result);
            }
        );
    }

    /**
     * Executes a search query against the full text search services.
     *
     * This can be used to perform a traditional FTS query, and/or a vector search.
     *
     * @param string $indexName the cluster-level FTS index to use for the search request
     * @param SearchRequest $request The search request to run
     * @param SearchOptions|null $options The options to use when executing the search request
     *
     * @return SearchResult
     * @throws InvalidArgumentException
     * @since 4.1.7
     */
    public function search(string $indexName, SearchRequest $request, ?SearchOptions $options = null): SearchResult
    {
        return $this->observability->recordOperation(
            ObservabilityConstants::OP_SEARCH_QUERY,
            SearchOptions::getParentSpan($options),
            function (ObservabilityHandler $obsHandler) use ($indexName, $request, $options) {
                $obsHandler->addService(ObservabilityConstants::ATTR_VALUE_SERVICE_SEARCH);

                $exportedRequest = SearchRequest::export($request);
                $exportedOptions = SearchOptions::export($options);
                $exportedOptions["showRequest"] = false;
                $query = $exportedRequest['searchQuery'];

                if (!$exportedRequest['vectorSearch']) {
                    $function = COUCHBASE_EXTENSION_NAMESPACE . '\\searchQuery';
                    $result = $function($this->core, $indexName, json_encode($query), $exportedOptions, $obsHandler->getCoreSpansArray());
                    return new SearchResult($result);
                }

                $vectorSearch = $exportedRequest['vectorSearch'];
                $function = COUCHBASE_EXTENSION_NAMESPACE . '\\vectorSearch';
                $result = $function(
                    $this->core,
                    $indexName,
                    json_encode($query),
                    json_encode($vectorSearch),
                    $exportedOptions,
                    VectorSearchOptions::export($vectorSearch->options()),
                    $obsHandler->getCoreSpansArray()
                );
                return new SearchResult($result);
            }
        );
    }

    /**
     * Replaces the current authenticator used by this cluster.
     *
     * NOTE: Setting a new authenticator does not change the authentication status of existing connections.
     *
     * @param Authenticator $authenticator The authenticator to replace
     *
     * @return void
     * @throws InvalidArgumentException if TLS is not enabled but provided authenticator requires TLS
     * @since 4.5.0
     */
    public function setAuthenticator(Authenticator $authenticator): void
    {
        $function = COUCHBASE_EXTENSION_NAMESPACE . '\\authenticatorSet';
        $function($this->core, $authenticator->export());
    }

    /**
     * Creates a new bucket manager object for managing buckets.
     *
     * @return BucketManager
     * @since 4.0.0
     */
    public function buckets(): BucketManager
    {
        return new BucketManager($this->core, $this->observability);
    }

    /**
     * Creates a new user manager object for managing users and groups.
     *
     * @return UserManager
     * @since 4.0.0
     */
    public function users(): UserManager
    {
        return new UserManager($this->core, $this->observability);
    }

    /**
     * Creates a new manager object for managing analytics query indexes.
     *
     * @return AnalyticsIndexManager
     * @since 4.0.0
     */
    public function analyticsIndexes(): AnalyticsIndexManager
    {
        return new AnalyticsIndexManager($this->core, $this->observability);
    }

    /**
     * Creates a new manager object for managing N1QL query indexes.
     *
     * @return QueryIndexManager
     * @since 4.0.0
     */
    public function queryIndexes(): QueryIndexManager
    {
        return new QueryIndexManager($this->core, $this->observability);
    }

    /**
     * Creates a new search index manager object for managing search query indexes.
     *
     * @return SearchIndexManager
     */
    public function searchIndexes(): SearchIndexManager
    {
        return new SearchIndexManager($this->core, $this->observability);
    }

    /**
     * Executes a ping for each service against each node in the cluster. This can be used for determining
     * the current health of the cluster.
     *
     * @param mixed|null $services the services to ping against
     * @param mixed|null $reportId a name which will be included within the ping result
     *
     * @see ServiceType::KEY_VALUE
     * @see ServiceType::QUERY
     * @see ServiceType::ANALYTICS
     * @see ServiceType::SEARCH
     * @see ServiceType::VIEWS
     * @see ServiceType::MANAGEMENT
     * @see ServiceType::EVENTING
     * @since 4.0.0
     */
    public function ping($services = null, $reportId = null)
    {
        $options = [];
        if ($services != null) {
            $options['serviceTypes'] = $services;
        }
        if ($reportId != null) {
            $options['reportId'] = $reportId;
        }
        $function = COUCHBASE_EXTENSION_NAMESPACE . '\\ping';
        return $function($this->core, $options);
    }

    /**
     * Returns diagnostics information about connections that the SDK has to the cluster. This does not perform
     * any operations.
     *
     * @param string|null $reportId a name which will be included within the ping result
     *
     * @since 4.0.0
     */
    public function diagnostics(?string $reportId = null)
    {
        if ($reportId == null) {
            $reportId = uniqid();
        }
        $function = COUCHBASE_EXTENSION_NAMESPACE . '\\diagnostics';
        return $function($this->core, $reportId);
    }

    /**
     * Creates a new bucket manager object for managing buckets.
     *
     * @param TransactionsConfiguration|null $config
     *
     * @return Transactions
     * @since 4.0.0
     */
    public function transactions(?TransactionsConfiguration $config = null): Transactions
    {
        return new Transactions($this->core, $config ?: $this->options->getTransactionsConfiguration());
    }

    /**
     * @param string $bucketName
     *
     * @return string|null
     * @internal
     *
     */
    public function version(string $bucketName): ?string
    {
        $function = COUCHBASE_EXTENSION_NAMESPACE . "\\clusterVersion";
        return $function($this->core, $bucketName);
    }

    /**
     * @param string $bucketName
     *
     * @return bool if replicas enabled, and cluster has enough nodes to use them
     * @internal
     *
     */
    public function replicasConfiguredFor(string $bucketName): bool
    {
        $function = COUCHBASE_EXTENSION_NAMESPACE . '\\replicasConfiguredForBucket';
        return $function($this->core, $bucketName);
    }

    /**
     * @internal
     * @deprecated will be removed once all managers will be implemented
     */
    public function core()
    {
        return $this->core;
    }
}
