<?php

namespace Aws;

use Aws\Credentials\CredentialsInterface;
use Aws\Credentials\CredentialSources;
use Aws\Token;
use Aws\Token\TokenInterface;

/**
 * A placeholder for gathering metrics in a request.
 *
 * @internal
 */
final class MetricsBuilder
{
    const WAITER = "B";
    const PAGINATOR = "C";
    const RETRY_MODE_LEGACY = "D";
    const RETRY_MODE_STANDARD = "E";
    const RETRY_MODE_ADAPTIVE = "F";
    const S3_TRANSFER = "G";
    const S3_CRYPTO_V1N = "H";
    const S3_CRYPTO_V2 = "I";
    const S3_EXPRESS_BUCKET = "J";
    const GZIP_REQUEST_COMPRESSION = "L";
    const ENDPOINT_OVERRIDE = "N";
    const ACCOUNT_ID_ENDPOINT = "O";
    const ACCOUNT_ID_MODE_PREFERRED = "P";
    const ACCOUNT_ID_MODE_DISABLED = "Q";
    const ACCOUNT_ID_MODE_REQUIRED = "R";
    const BEARER_SERVICE_ENV_VARS = "3";
    const SIGV4A_SIGNING = "S";
    const RESOLVED_ACCOUNT_ID = "T";
    const FLEXIBLE_CHECKSUMS_REQ_CRC32 = "U";
    const FLEXIBLE_CHECKSUMS_REQ_CRC32C = "V";
    const FLEXIBLE_CHECKSUMS_REQ_CRC64 = "W";
    const FLEXIBLE_CHECKSUMS_REQ_SHA1 = "X";
    const FLEXIBLE_CHECKSUMS_REQ_SHA256 = "Y";
    const FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED = "Z";
    const FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED = "a";
    const FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED = "b";
    const FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED = "c";
    const CREDENTIALS_CODE = "e";
    const CREDENTIALS_ENV_VARS = "g";
    const CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN = "h";
    const CREDENTIALS_STS_ASSUME_ROLE = "i";
    const CREDENTIALS_STS_ASSUME_ROLE_WEB_ID = "k";
    const CREDENTIALS_PROFILE = "n";
    const CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN = "q";
    const CREDENTIALS_HTTP = "z";
    const CREDENTIALS_IMDS = "0";
    const CREDENTIALS_PROFILE_PROCESS = "v";
    const CREDENTIALS_PROFILE_SSO = "r";
    const CREDENTIALS_PROFILE_SSO_LEGACY = "t";

    /** @var int */
    private static $MAX_METRICS_SIZE = 1024; // 1KB or 1024 B

    /** @var string */
    private static $METRIC_SEPARATOR = ",";

    /** @var array $metrics */
    private $metrics;

    /** @var int $metricsSize */
    private $metricsSize;

    public function __construct()
    {
        $this->metrics = [];
        // The first metrics does not include the separator
        // therefore it is reduced by default.
        $this->metricsSize = -(strlen(self::$METRIC_SEPARATOR));
    }

    /**
     * Build the metrics string value.
     *
     * @return string
     */
    public function build(): string
    {
        if (empty($this->metrics)) {
            return "";
        }

        return $this->encode();
    }

    /**
     * Encodes the metrics by separating each metric
     * with a comma. Example: for the metrics[A,B,C] then
     * the output would be "A,B,C".
     *
     * @return string
     */
    private function encode(): string
    {
        return implode(self::$METRIC_SEPARATOR, array_keys($this->metrics));
    }

    /**
     * Appends a metric to the internal metrics holder after validating it.
     * Increases the current metrics size by the length of the new metric
     * plus the length of the encoding separator.
     * Example: $currentSize = $currentSize + len($newMetric) + len($separator)
     *
     * @param string $metric The metric to append.
     *
     * @return void
     */
    public function append(string $metric): void
    {
        if (!$this->canMetricBeAppended($metric)) {
            return;
        }

        $this->metrics[$metric] = true;
        $this->metricsSize += strlen($metric) + strlen(self::$METRIC_SEPARATOR);
    }

    /**
     * Receives a feature group and a value to identify which one is the metric.
     * For example, a group could be `signature` and a value could be `v4a`,
     * then the metric will be `SIGV4A_SIGNING`.
     *
     * @param string $featureGroup the feature group such as `signature`.
     * @param mixed $value the value for identifying the metric.
     *
     * @return void
     */
    public function identifyMetricByValueAndAppend(
        string $featureGroup,
        mixed $value
    ): void
    {
        if (empty($value)) {
            return;
        }

        static $appendMetricFns = [
            'signature' => 'appendSignatureMetric',
            'request_compression' => 'appendRequestCompressionMetric',
            'request_checksum' => 'appendRequestChecksumMetric',
            'credentials' => 'appendCredentialsMetric',
            'account_id_endpoint_mode' => 'appendAccountIdEndpointMode',
            'account_id_endpoint' => 'appendAccountIdEndpoint',
            'request_checksum_calculation' => 'appendRequestChecksumCalculationMetric',
            'token' => 'appendTokenMetric'
        ];

        $fn = $appendMetricFns[$featureGroup];
        $this->{$fn}($value);
    }

    /**
     * Appends the signature metric based on the signature value.
     *
     * @param string $signature
     *
     * @return void
     */
    private function appendSignatureMetric(string $signature): void
    {
        if ($signature === 'v4-s3express') {
            $this->append(self::S3_EXPRESS_BUCKET);
        } elseif ($signature === 'v4a') {
            $this->append(self::SIGV4A_SIGNING);
        }
    }

    /**
     * Appends the request compression metric based on the format resolved.
     *
     * @param string $format
     *
     * @return void
     */
    private function appendRequestCompressionMetric(string $format): void
    {
        if ($format === 'gzip') {
            $this->append(self::GZIP_REQUEST_COMPRESSION);
        }
    }

    /**
     * Appends the request checksum metric based on the algorithm.
     *
     * @param string $algorithm
     *
     * @return void
     */
    private function appendRequestChecksumMetric(string $algorithm): void
    {
        if ($algorithm === 'crc32') {
            $this->append(self::FLEXIBLE_CHECKSUMS_REQ_CRC32);
        } elseif ($algorithm === 'crc32c') {
            $this->append(self::FLEXIBLE_CHECKSUMS_REQ_CRC32C);
        } elseif ($algorithm === 'crc64') {
            $this->append(self::FLEXIBLE_CHECKSUMS_REQ_CRC64);
        } elseif ($algorithm === 'sha1') {
            $this->append(self::FLEXIBLE_CHECKSUMS_REQ_SHA1);
        } elseif ($algorithm === 'sha256') {
            $this->append(self::FLEXIBLE_CHECKSUMS_REQ_SHA256);
        }
    }


    /**
     * Appends the credentials metric based on the type of credentials
     * resolved.
     *
     * @param CredentialsInterface $credentials
     *
     * @return void
     */
    private function appendCredentialsMetric(
        CredentialsInterface $credentials
    ): void
    {
        $source = $credentials->toArray()['source'] ?? null;
        if (empty($source)) {
            return;
        }

        static $credentialsMetricMapping = [
            CredentialSources::STATIC =>
                self::CREDENTIALS_CODE,
            CredentialSources::ENVIRONMENT =>
                self::CREDENTIALS_ENV_VARS,
            CredentialSources::ENVIRONMENT_STS_WEB_ID_TOKEN =>
                self::CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN,
            CredentialSources::STS_ASSUME_ROLE =>
                self::CREDENTIALS_STS_ASSUME_ROLE,
            CredentialSources::STS_WEB_ID_TOKEN =>
                self::CREDENTIALS_STS_ASSUME_ROLE_WEB_ID,
            CredentialSources::PROFILE =>
                self::CREDENTIALS_PROFILE,
            CredentialSources::IMDS =>
                self::CREDENTIALS_IMDS,
            CredentialSources::ECS =>
                self::CREDENTIALS_HTTP,
            CredentialSources::PROFILE_STS_WEB_ID_TOKEN =>
                self::CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN,
            CredentialSources::PROFILE_PROCESS =>
                self::CREDENTIALS_PROFILE_PROCESS,
            CredentialSources::PROFILE_SSO =>
                self::CREDENTIALS_PROFILE_SSO,
            CredentialSources::PROFILE_SSO_LEGACY =>
                self::CREDENTIALS_PROFILE_SSO_LEGACY,
        ];
        if (isset($credentialsMetricMapping[$source])) {
            $this->append($credentialsMetricMapping[$source]);
        }
    }

    private function appendTokenMetric(TokenInterface $token): void
    {
        $source = $token->getSource();
        if (empty($source)) {
            return;
        }

        static $tokenMetricMapping = [
            'bearer_service_env_vars' => self::BEARER_SERVICE_ENV_VARS
        ];

        if (isset($tokenMetricMapping[$source])) {
            $this->append($tokenMetricMapping[$source]);
        }
    }

    private function appendRequestChecksumCalculationMetric(
        string $checkSumCalculation
    ): void
    {
        static $checksumCalculationMetricMapping = [
            'when_supported' => self::FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED,
            'when_required' => self::FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED,
        ];

        if (isset($checksumCalculationMetricMapping[$checkSumCalculation])) {
            $this->append($checksumCalculationMetricMapping[$checkSumCalculation]);
        }
    }

    /**
     * Appends the account_id_endpoint_mode metrics based on
     * the value resolved.
     *
     * @param string $accountIdEndpointMode
     *
     * @return void
     */
    private function appendAccountIdEndpointMode(
        string $accountIdEndpointMode
    ): void
    {
        if (empty($accountIdEndpointMode)) {
            return;
        }

        if ($accountIdEndpointMode === 'preferred') {
            $this->append(self::ACCOUNT_ID_MODE_PREFERRED);
        } elseif ($accountIdEndpointMode === 'disabled') {
            $this->append(self::ACCOUNT_ID_MODE_DISABLED);
        } elseif ($accountIdEndpointMode === 'required') {
            $this->append(self::ACCOUNT_ID_MODE_REQUIRED);
        }
    }

    /**
     * Appends the account_id_endpoint metric whenever a resolved endpoint
     * matches an account_id endpoint pattern which also defined here.
     *
     * @param string $endpoint
     *
     * @return void
     */
    private function appendAccountIdEndpoint(string $endpoint): void
    {
        static $pattern = "/(https|http):\\/\\/\\d{12}\\.ddb/";
        if (preg_match($pattern, $endpoint)) {
            $this->append(self::ACCOUNT_ID_ENDPOINT);
        }
    }

    /**
     * Resolves metrics from client arguments.
     *
     * @param array $args
     *
     * @return void
     */
    public function resolveAndAppendFromArgs(array $args = []): void
    {
        static $metricsFnList = [
            'appendEndpointMetric',
            'appendRetryConfigMetric',
            'appendResponseChecksumValidationMetric',
        ];
        foreach ($metricsFnList as $metricFn) {
            $this->{$metricFn}($args);
        }
    }

    /**
     * Appends the endpoint metric into the metrics builder,
     * just if a custom endpoint was provided at client construction.
     *
     * @param array $args
     *
     * @return void
     */
    private function appendEndpointMetric(array $args): void
    {
        if (!empty($args['endpoint_override'])) {
            $this->append(MetricsBuilder::ENDPOINT_OVERRIDE);
        }
    }

    /**
     * Appends the retry mode metric into the metrics builder,
     * based on the resolved retry config mode.
     *
     * @param array $args
     *
     * @return void
     */
    private function appendRetryConfigMetric(array $args): void
    {
        $retries = $args['retries'] ?? null;
        if ($retries === null) {
            return;
        }

        $retryMode = '';
        if ($retries instanceof \Aws\Retry\Configuration) {
            $retryMode = $retries->getMode();
        } elseif (is_array($retries)
            && isset($retries["mode"])
        ) {
            $retryMode = $retries["mode"];
        }

        if ($retryMode === 'legacy') {
            $this->append(
                MetricsBuilder::RETRY_MODE_LEGACY
            );
        } elseif ($retryMode === 'standard') {
            $this->append(
                MetricsBuilder::RETRY_MODE_STANDARD
            );
        } elseif ($retryMode === 'adaptive') {
            $this->append(
                MetricsBuilder::RETRY_MODE_ADAPTIVE
            );
        }
    }

    /**
     * Appends the provided/resolved response checksum validation mode.
     *
     * @param array $args
     *
     * @return void
     */
    private function appendResponseChecksumValidationMetric(array $args): void
    {
        if (empty($args['response_checksum_validation'])) {
            return;
        }

        $checksumValidation = $args['response_checksum_validation'];
        static $checksumValidationMetricMapping = [
            'when_supported' => MetricsBuilder::FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED,
            'when_required' => MetricsBuilder::FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED,
        ];

        if (isset($checksumValidationMetricMapping[$checksumValidation])) {
            $this->append($checksumValidationMetricMapping[$checksumValidation]);
        }
    }

    /**
     * Validates if a metric can be appended by ensuring the total size,
     * including the new metric and separator, does not exceed the limit.
     * Also checks that the metric does not already exist.
     * Example: Appendable if:
     *  $currentSize + len($newMetric) + len($separator) <= MAX_SIZE
     *  and:
     * $newMetric not in $existingMetrics
     *
     * @param string $newMetric The metric to validate.
     *
     * @return bool True if the metric can be appended, false otherwise.
     */
    private function canMetricBeAppended(string $newMetric): bool
    {
        if ($newMetric === "") {
            return false;
        }

        if ($this->metricsSize
            + (strlen($newMetric) + strlen(self::$METRIC_SEPARATOR))
            > self::$MAX_METRICS_SIZE
        ) {
            return false;
        }

        if (isset($this->metrics[$newMetric])) {
            return false;
        }

        return true;
    }

    /**
     * Returns the metrics builder from the property @context of a command.
     *
     * @param Command $command
     *
     * @return MetricsBuilder
     */
    public static function fromCommand(CommandInterface $command): MetricsBuilder
    {
        return $command->getMetricsBuilder();
    }

    /**
     * Helper method for appending a metrics capture middleware into a
     * handler stack given. The middleware appended here is on top of the
     * build step.
     *
     * @param HandlerList $handlerList
     * @param $metric
     *
     * @return void
     */
    public static function appendMetricsCaptureMiddleware(
        HandlerList $handlerList,
        $metric
    ): void
    {
        $middlewareName = 'metrics-capture-'.$metric;
        if (!$handlerList->hasMiddleware($middlewareName)) {
            $handlerList->appendBuild(
                Middleware::tap(
                    function (CommandInterface $command) use ($metric) {
                        self::fromCommand($command)->append(
                            $metric
                        );
                    }
                ),
                $middlewareName
            );
        }
    }
}
