<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */
declare (strict_types=1);
namespace Piwik\Plugins\PrivacyManager;

use Piwik\Columns\Dimension;
use Piwik\DataTable;
use Piwik\DataTable\DataTableInterface;
use Piwik\DataTable\Row;
use Piwik\Metrics;
use Piwik\Plugin\Metric;
use Piwik\Plugin\ProcessedMetric;
use Piwik\Plugin\Report;
use Piwik\Plugins\PrivacyManager\Settings\DataRoundingEnabled;
use Piwik\Request;
use Piwik\Site;
use Throwable;
class DataRounding
{
    private const EXCLUDED_COLUMN_NAMES = ['label'];
    private const IDENTIFIER_COLUMN_NAMES = ['idsite', 'idgoal', 'idsubdatatable'];
    private const CHANGE_COLUMN_PATTERN = '/_change$/i';
    private const EXCLUDED_BY_NAME_PATTERN = '/(rate|percent|percentage|evolution|duration|visit_length|bandwidth|byte)/';
    private const EXCLUDED_SPECIFIC_MAX_METRIC_PATTERN = '/^(max_actions(?:_(?:returning|new|ai_agent|human))?|max_time_(?:network|server|transfer|dom_processing|dom_completion|on_load|generation)|max_bandwidth)$/';
    private const INCLUDED_COUNT_BY_NAME_PATTERN = '/(^nb_|_nb_|_count$|^count_|^items$|^orders$|^quantity$)/';
    private const IDENTIFIER_BY_NAME_PATTERN = '/(^id_|_id$)/';
    public static function shouldApplyForRequest(array $request) : bool
    {
        if (!self::requestHasNonEmptySegment($request)) {
            return \false;
        }
        return self::isDataRoundingEnabledForAnyRequestedSite(self::extractRequestedSiteIds($request));
    }
    /**
     * @param int[] $siteIds
     */
    public static function isDataRoundingEnabledForAnySites(array $siteIds) : bool
    {
        return self::isDataRoundingEnabledForAnyRequestedSite($siteIds);
    }
    public static function roundCountMetricsForRequest(DataTableInterface $dataTable, array $request, ?Report $report = null) : void
    {
        if (!self::requestHasNonEmptySegment($request)) {
            return;
        }
        $requestedSiteIds = self::extractRequestedSiteIds($request);
        if (empty($requestedSiteIds)) {
            return;
        }
        self::roundCountMetricsForRequestedSites($dataTable, $requestedSiteIds, $report, null, [self::class, 'isDataRoundingEnabledForSite']);
    }
    /**
     * @param array<string, mixed> $values
     * @return array<string, mixed>
     */
    public static function roundCountArrayValuesForRequest(array $values, array $request) : array
    {
        if (!self::shouldApplyForRequest($request)) {
            return $values;
        }
        $requestedSiteIds = self::extractRequestedSiteIds($request);
        if (empty($requestedSiteIds)) {
            return $values;
        }
        return self::roundArrayValuesForRequestedSites($values, $requestedSiteIds);
    }
    public static function roundCountMetrics(DataTableInterface $dataTable, ?Report $report = null) : void
    {
        $roundTable = function (DataTable $table) use($report) {
            self::roundDataTable($table, $report);
        };
        $dataTable->filter($roundTable);
        if ($dataTable instanceof DataTable || $dataTable instanceof DataTable\Map) {
            $dataTable->filterSubtables($roundTable);
        }
    }
    private static function roundDataTable(DataTable $table, ?Report $report = null) : void
    {
        $metricTypes = self::getMetricTypes($table, $report);
        $columnsToRound = self::collectColumnsToRound($table, $metricTypes);
        if (!empty($columnsToRound)) {
            foreach ($table->getRows() as $row) {
                self::roundRowColumns($row, $columnsToRound, $metricTypes, $report);
                self::roundRowComparisons($row, $report);
            }
        }
        self::roundTotalsRowIfPresent($table, $columnsToRound, $metricTypes, $report);
        self::roundTotalsMetadataIfPresent($table, $metricTypes);
        self::reconcileTotalsFromRoundedRowsForConstantRowsReport($table, $columnsToRound, $report);
        self::clearStaleRatioMetadata($table, $columnsToRound);
        self::recomputeProcessedPercentMetrics($table, $report);
    }
    /**
     * @param string[] $columnsToRound
     * @param array<string, string|null> $metricTypes
     */
    private static function roundTotalsRowIfPresent(DataTable $table, array $columnsToRound, array $metricTypes, ?Report $report) : void
    {
        $totalsRow = $table->getTotalsRow();
        if (empty($totalsRow)) {
            return;
        }
        self::roundRowColumns($totalsRow, $columnsToRound, $metricTypes, $report);
        self::roundRowComparisons($totalsRow, $report);
    }
    /**
     * @param string[] $columnsToRound
     * @param array<string, string|null> $metricTypes
     */
    private static function roundRowColumns(Row $row, array $columnsToRound, array $metricTypes, ?Report $report) : void
    {
        foreach ($columnsToRound as $columnName) {
            $rawColumnName = self::getRawMetricColumnName($row, $columnName);
            $value = $row->getColumn($rawColumnName);
            if (!self::shouldRoundValue($value)) {
                continue;
            }
            $row->setColumn($rawColumnName, self::roundToNearestTen((float) $value));
        }
        foreach ($row->getColumns() as $columnName => $value) {
            if (is_array($value)) {
                if (self::isGoalsContainerColumn((string) $columnName)) {
                    $row->setColumn((string) $columnName, self::roundGoalRowsArray($value, $metricTypes));
                } else {
                    $row->setColumn((string) $columnName, self::roundCountArrayValues($value, $metricTypes));
                }
                continue;
            }
            if ($value instanceof DataTableInterface) {
                self::roundCountMetrics($value, $report);
            }
        }
    }
    private static function roundRowComparisons(Row $row, ?Report $report) : void
    {
        $comparisons = $row->getComparisons();
        if (empty($comparisons)) {
            return;
        }
        self::roundDataTable($comparisons, $report);
    }
    /**
     * @param array<string, string|null> $metricTypes
     */
    private static function roundTotalsMetadataIfPresent(DataTable $table, array $metricTypes) : void
    {
        $totals = $table->getMetadata('totals');
        if (is_array($totals)) {
            $table->setMetadata('totals', self::roundTotals($totals, $metricTypes));
        }
        $totalsUnformatted = $table->getMetadata('totalsUnformatted');
        if (is_array($totalsUnformatted)) {
            $table->setMetadata('totalsUnformatted', self::roundTotals($totalsUnformatted, $metricTypes));
        }
    }
    /**
     * @param string[] $columnsToRound
     */
    private static function clearStaleRatioMetadata(DataTable $table, array $columnsToRound) : void
    {
        if (empty($columnsToRound)) {
            return;
        }
        foreach ($table->getRows() as $row) {
            foreach ($columnsToRound as $columnName) {
                self::clearRatioMetadata($row, $columnName);
            }
        }
    }
    private static function clearRatioMetadata(Row $row, string $columnName) : void
    {
        foreach (['_row_percentage', '_site_total_percentage'] as $suffix) {
            $metadataName = $columnName . $suffix;
            if ($row->getMetadata($metadataName) !== \false) {
                $row->deleteMetadata($metadataName);
            }
        }
    }
    private static function recomputeProcessedPercentMetrics(DataTable $table, ?Report $report) : void
    {
        $processedMetrics = Report::getProcessedMetricsForTable($table, $report);
        if (empty($processedMetrics)) {
            return;
        }
        foreach ($processedMetrics as $metricName => $processedMetric) {
            if (!self::shouldRecomputeProcessedPercentMetric($processedMetric)) {
                continue;
            }
            if (!$processedMetric->beforeCompute($report, $table)) {
                continue;
            }
            foreach ($table->getRows() as $row) {
                self::recomputeProcessedMetricForRow($row, $metricName, $processedMetric);
            }
            $totalsRow = $table->getTotalsRow();
            if (!empty($totalsRow)) {
                self::recomputeProcessedMetricForRow($totalsRow, $metricName, $processedMetric);
            }
        }
    }
    private static function shouldRecomputeProcessedPercentMetric(ProcessedMetric $processedMetric) : bool
    {
        return $processedMetric->getSemanticType() === Dimension::TYPE_PERCENT;
    }
    private static function recomputeProcessedMetricForRow(Row $row, string $metricName, ProcessedMetric $processedMetric) : void
    {
        if (!$row->hasColumn($metricName)) {
            return;
        }
        $existingValue = $row->getColumn($metricName);
        $computedValue = $processedMetric->compute($row);
        if ($computedValue === \false) {
            return;
        }
        // If the metric was already formatted upstream (e.g. an inner API call's post-processor turned it into "0%"),
        // re-format the recomputed quotient so we don't downgrade the output. The outer applyMetricsFormatting step
        // would otherwise skip this table because its PROCESSED_METRICS_FORMATTED_FLAG is already set.
        if (!is_numeric($existingValue) && is_numeric($computedValue)) {
            $computedValue = $processedMetric->format($computedValue, new Metrics\Formatter());
        }
        $row->setColumn($metricName, $computedValue);
    }
    /**
     * @param string[] $columnsToRound
     */
    private static function reconcileTotalsFromRoundedRowsForConstantRowsReport(DataTable $table, array $columnsToRound, ?Report $report) : void
    {
        if (empty($report) || !self::isConstantRowsCountReport($report) || empty($columnsToRound)) {
            return;
        }
        $totals = $table->getMetadata('totals');
        if (!is_array($totals)) {
            $totals = [];
        }
        $totalsUnformatted = $table->getMetadata('totalsUnformatted');
        if (!is_array($totalsUnformatted)) {
            $totalsUnformatted = [];
        }
        $totalsRow = $table->getTotalsRow();
        foreach ($columnsToRound as $columnName) {
            $sum = 0;
            foreach ($table->getRows() as $row) {
                $value = $row->getColumn($columnName);
                if (is_numeric($value) && $value >= 0) {
                    $sum += (int) $value;
                }
            }
            $totals[$columnName] = $sum;
            $totalsUnformatted[$columnName] = $sum;
            if (!empty($totalsRow)) {
                $totalsRow->setColumn($columnName, $sum);
            }
        }
        $table->setMetadata('totals', $totals);
        $table->setMetadata('totalsUnformatted', $totalsUnformatted);
    }
    private static function isConstantRowsCountReport(Report $report) : bool
    {
        return $report->hasConstantRowsCount();
    }
    /**
     * @return array<string, string|null>
     */
    private static function getMetricTypes(DataTable $table, ?Report $report = null) : array
    {
        if (!empty($report)) {
            $metricTypes = $report->getMetricSemanticTypes();
        } else {
            $metricTypes = Metrics::getDefaultMetricSemanticTypes();
        }
        $metrics = Report::getMetricsForTable($table, $report, Metric::class);
        foreach ($metrics as $metric) {
            $name = $metric->getName();
            $metricTypes[$name] = $metric->getSemanticType() ?: $metricTypes[$name] ?? null;
        }
        return $metricTypes;
    }
    /**
     * @param array<string, string|null> $metricTypes
     * @return string[]
     */
    private static function collectColumnsToRound(DataTable $table, array $metricTypes) : array
    {
        $sampleValuesByColumn = [];
        foreach ($table->getRows() as $row) {
            foreach ($row->getColumns() as $columnName => $value) {
                $columnName = (string) $columnName;
                if (!array_key_exists($columnName, $sampleValuesByColumn) || !self::shouldRoundValue($sampleValuesByColumn[$columnName])) {
                    $sampleValuesByColumn[$columnName] = $value;
                }
            }
        }
        $totalsRow = $table->getTotalsRow();
        if (!empty($totalsRow)) {
            foreach ($totalsRow->getColumns() as $columnName => $value) {
                $columnName = (string) $columnName;
                if (!array_key_exists($columnName, $sampleValuesByColumn) || !self::shouldRoundValue($sampleValuesByColumn[$columnName])) {
                    $sampleValuesByColumn[$columnName] = $value;
                }
            }
        }
        if (empty($sampleValuesByColumn)) {
            return [];
        }
        $columns = [];
        foreach ($sampleValuesByColumn as $columnName => $value) {
            $columnName = (string) $columnName;
            $normalizedColumnName = self::normalizeRawMetricColumnName($columnName);
            if (!self::shouldRoundColumn($normalizedColumnName, $metricTypes[$normalizedColumnName] ?? null)) {
                continue;
            }
            if (self::shouldRoundValue($value)) {
                $columns[] = $normalizedColumnName;
            }
        }
        return $columns;
    }
    /**
     * @param array<string, mixed> $totals
     * @param array<string, string|null> $metricTypes
     * @return array<string, mixed>
     */
    private static function roundTotals(array $totals, array $metricTypes) : array
    {
        return self::roundArrayValuesRecursive($totals, $metricTypes, \true);
    }
    /**
     * @param array<string, mixed> $values
     * @param array<string, string|null> $metricTypes
     * @return array<string, mixed>
     */
    public static function roundCountArrayValues(array $values, array $metricTypes = []) : array
    {
        return self::roundArrayValuesRecursive($values, $metricTypes, \true);
    }
    /**
     * @param array<string, mixed> $values
     * @param array<string, string|null> $metricTypes
     * @return array<string, mixed>
     */
    private static function roundArrayValuesRecursive(array $values, array $metricTypes, bool $allowSemanticTypes, ?string $parentColumnName = null) : array
    {
        foreach ($values as $columnName => $value) {
            $columnName = (string) $columnName;
            if (is_array($value)) {
                if (self::isGoalsContainerColumn($columnName)) {
                    $values[$columnName] = self::roundGoalRowsArray($value, $metricTypes);
                } else {
                    $values[$columnName] = self::roundArrayValuesRecursive($value, $metricTypes, \false, $columnName);
                }
                continue;
            }
            $semanticType = $allowSemanticTypes ? $metricTypes[$columnName] ?? null : null;
            if (self::isGoalsContainerColumn($parentColumnName)) {
                $metricName = self::getGoalMetricNameFromRawColumn($columnName) ?? $columnName;
                $semanticType = null;
            } else {
                $metricName = self::normalizeRawMetricColumnName($columnName);
                if ($metricName !== $columnName) {
                    $semanticType = $allowSemanticTypes ? $metricTypes[$metricName] ?? null : null;
                }
            }
            if (self::shouldRoundColumn($metricName, $semanticType) && self::shouldRoundValue($value)) {
                $values[$columnName] = self::roundToNearestTen((float) $value);
            }
        }
        return $values;
    }
    private static function shouldRoundValue($value) : bool
    {
        return is_numeric($value) && $value >= 0;
    }
    /**
     * @param array<string, mixed> $goalRows
     * @param array<string, string|null> $metricTypes
     * @return array<string, mixed>
     */
    private static function roundGoalRowsArray(array $goalRows, array $metricTypes) : array
    {
        foreach ($goalRows as $goalId => $goalValues) {
            if (!is_array($goalValues)) {
                continue;
            }
            $goalRows[$goalId] = self::roundArrayValuesRecursive($goalValues, $metricTypes, \false, 'goals');
        }
        return $goalRows;
    }
    private static function isGoalsContainerColumn(?string $columnName) : bool
    {
        if ($columnName === null) {
            return \false;
        }
        return $columnName === 'goals' || $columnName === (string) Metrics::INDEX_GOALS;
    }
    private static function getGoalMetricNameFromRawColumn(string $columnName) : ?string
    {
        if (!is_numeric($columnName)) {
            return null;
        }
        $columnId = (int) $columnName;
        return Metrics::$mappingFromIdToNameGoal[$columnId] ?? null;
    }
    private static function normalizeRawMetricColumnName(string $columnName) : string
    {
        if (!is_numeric($columnName)) {
            return $columnName;
        }
        $columnId = (int) $columnName;
        return Metrics::$mappingFromIdToName[$columnId] ?? $columnName;
    }
    /**
     * @return string|int
     */
    private static function getRawMetricColumnName(Row $row, string $columnName)
    {
        if ($row->hasColumn($columnName)) {
            return $columnName;
        }
        $mapping = Metrics::getMappingFromNameToId();
        $rawColumnId = $mapping[$columnName] ?? null;
        if ($rawColumnId !== null) {
            if ($row->hasColumn($rawColumnId)) {
                return $rawColumnId;
            }
            $rawColumnName = (string) $rawColumnId;
            if ($row->hasColumn($rawColumnName)) {
                return $rawColumnName;
            }
        }
        return $columnName;
    }
    private static function roundToNearestTen(float $value) : int
    {
        if ($value === 0.0) {
            return 0;
        }
        // Keep any non-zero count at a minimum of 10 so segmented responses do not expose near-single-user volumes.
        return max(10, (int) (floor(($value + 5) / 10) * 10));
    }
    private static function shouldRoundColumn(string $columnName, ?string $semanticType) : bool
    {
        if ($columnName === '' || in_array($columnName, self::EXCLUDED_COLUMN_NAMES, \true) || self::isIdentifierColumn($columnName) || preg_match(self::CHANGE_COLUMN_PATTERN, $columnName)) {
            return \false;
        }
        $columnName = strtolower($columnName);
        if (preg_match(self::EXCLUDED_BY_NAME_PATTERN, $columnName)) {
            return \false;
        }
        if (preg_match(self::EXCLUDED_SPECIFIC_MAX_METRIC_PATTERN, $columnName)) {
            return \false;
        }
        if (!empty($semanticType)) {
            return $semanticType === Dimension::TYPE_NUMBER;
        }
        return (bool) preg_match(self::INCLUDED_COUNT_BY_NAME_PATTERN, $columnName);
    }
    private static function isIdentifierColumn(string $columnName) : bool
    {
        $columnName = strtolower($columnName);
        if (in_array($columnName, self::IDENTIFIER_COLUMN_NAMES, \true)) {
            return \true;
        }
        return (bool) preg_match(self::IDENTIFIER_BY_NAME_PATTERN, $columnName);
    }
    private static function requestHasNonEmptySegment(array $request) : bool
    {
        try {
            $segment = (new Request($request))->getStringParameter('segment', '');
            return trim($segment) !== '';
        } catch (Throwable $e) {
            return \false;
        }
    }
    /**
     * @return int[]
     */
    private static function extractRequestedSiteIds(array $request) : array
    {
        try {
            $requestObject = new Request($request);
            $idSite = $requestObject->getParameter('idSite', null);
            if (is_null($idSite)) {
                $idSite = $requestObject->getParameter('idsite', null);
            }
            if (!is_array($idSite) && !is_scalar($idSite)) {
                return [];
            }
            return Site::getIdSitesFromIdSitesString($idSite, \false, \false);
        } catch (Throwable $e) {
            return [];
        }
    }
    private static function isDataRoundingEnabledForSite(?int $idSite) : bool
    {
        try {
            return DataRoundingEnabled::getInstance($idSite)->getValue() === \true;
        } catch (Throwable $e) {
            return \false;
        }
    }
    /**
     * @param int[] $requestedSiteIds
     */
    private static function isDataRoundingEnabledForAnyRequestedSite(array $requestedSiteIds) : bool
    {
        return self::isAnyRequestedSiteEnabled($requestedSiteIds, [self::class, 'isDataRoundingEnabledForSite']);
    }
    /**
     * @param int[] $requestedSiteIds
     */
    private static function roundCountMetricsForRequestedSites(DataTableInterface $dataTable, array $requestedSiteIds, ?Report $report, ?int $currentSiteId = null, ?callable $isSiteEnabled = null) : void
    {
        $isSiteEnabled = $isSiteEnabled ?: [self::class, 'isDataRoundingEnabledForSite'];
        if ($dataTable instanceof DataTable\Map) {
            foreach ($dataTable->getDataTables() as $key => $childTable) {
                self::roundCountMetricsForRequestedSites($childTable, $requestedSiteIds, $report, self::resolveCurrentSiteIdFromMapKey($dataTable, $key, $currentSiteId), $isSiteEnabled);
            }
            return;
        }
        if ($currentSiteId !== null) {
            if (!(bool) call_user_func($isSiteEnabled, $currentSiteId)) {
                return;
            }
        } elseif (!self::isAnyRequestedSiteEnabled($requestedSiteIds, $isSiteEnabled)) {
            return;
        }
        self::roundDataTableForRequestedSites($dataTable, $requestedSiteIds, $report, $currentSiteId, $isSiteEnabled);
    }
    /**
     * @param int[] $requestedSiteIds
     */
    private static function isAnyRequestedSiteEnabled(array $requestedSiteIds, callable $isSiteEnabled) : bool
    {
        foreach ($requestedSiteIds as $siteId) {
            if ((bool) call_user_func($isSiteEnabled, (int) $siteId)) {
                return \true;
            }
        }
        return \false;
    }
    /**
     * @param array<string, mixed> $values
     * @param int[] $requestedSiteIds
     * @return array<string, mixed>
     */
    private static function roundArrayValuesForRequestedSites(array $values, array $requestedSiteIds, ?int $currentSiteId = null, ?callable $isSiteEnabled = null) : array
    {
        $isSiteEnabled = $isSiteEnabled ?: [self::class, 'isDataRoundingEnabledForSite'];
        $rowSiteId = self::extractSiteIdFromArrayRow($values);
        if ($rowSiteId !== null) {
            $currentSiteId = $rowSiteId;
        }
        if (!self::containsNestedArrays($values)) {
            if ($currentSiteId !== null && !(bool) call_user_func($isSiteEnabled, $currentSiteId)) {
                return $values;
            }
            if ($currentSiteId === null && !self::isAnyRequestedSiteEnabled($requestedSiteIds, $isSiteEnabled)) {
                return $values;
            }
            return self::roundCountArrayValues($values);
        }
        foreach ($values as $key => $value) {
            if (!is_array($value)) {
                continue;
            }
            $values[$key] = self::roundArrayValuesForRequestedSites($value, $requestedSiteIds, $currentSiteId, $isSiteEnabled);
        }
        if ($currentSiteId !== null) {
            if (!(bool) call_user_func($isSiteEnabled, $currentSiteId)) {
                return $values;
            }
        } elseif (!self::isAnyRequestedSiteEnabled($requestedSiteIds, $isSiteEnabled)) {
            return $values;
        }
        return self::roundDirectScalarArrayValues($values);
    }
    /**
     * @param int[] $requestedSiteIds
     */
    private static function roundDataTableForRequestedSites(DataTableInterface $dataTable, array $requestedSiteIds, ?Report $report, ?int $currentSiteId, callable $isSiteEnabled) : void
    {
        if ($dataTable instanceof DataTable\Map) {
            self::roundCountMetrics($dataTable, $report);
            return;
        }
        if (!$dataTable instanceof DataTable) {
            return;
        }
        $metricTypes = self::getMetricTypes($dataTable, $report);
        $columnsToRound = self::collectColumnsToRound($dataTable, $metricTypes);
        $shouldRoundFallbackRows = self::isAnyRequestedSiteEnabled($requestedSiteIds, $isSiteEnabled);
        if (!empty($columnsToRound)) {
            foreach ($dataTable->getRows() as $row) {
                $rowSiteId = $currentSiteId ?? self::extractSiteIdFromRow($row);
                $shouldRoundRow = $rowSiteId !== null ? (bool) call_user_func($isSiteEnabled, $rowSiteId) : $shouldRoundFallbackRows;
                if (!$shouldRoundRow) {
                    $subtable = $row->getSubtable();
                    if (!empty($subtable)) {
                        self::roundDataTableForRequestedSites($subtable, $requestedSiteIds, $report, $rowSiteId, $isSiteEnabled);
                    }
                    continue;
                }
                self::roundRowColumns($row, $columnsToRound, $metricTypes, $report);
                self::roundRowComparisons($row, $report);
                $subtable = $row->getSubtable();
                if (!empty($subtable)) {
                    self::roundDataTableForRequestedSites($subtable, $requestedSiteIds, $report, $rowSiteId, $isSiteEnabled);
                }
            }
        }
        if ($shouldRoundFallbackRows) {
            self::roundTotalsRowIfPresent($dataTable, $columnsToRound, $metricTypes, $report);
            self::roundTotalsMetadataIfPresent($dataTable, $metricTypes);
            self::reconcileTotalsFromRoundedRowsForConstantRowsReport($dataTable, $columnsToRound, $report);
            self::clearStaleRatioMetadata($dataTable, $columnsToRound);
            self::recomputeProcessedPercentMetrics($dataTable, $report);
        }
    }
    /**
     * @param array<string, mixed> $values
     */
    private static function containsNestedArrays(array $values) : bool
    {
        foreach ($values as $value) {
            if (is_array($value)) {
                return \true;
            }
        }
        return \false;
    }
    /**
     * @param array<string, mixed> $values
     */
    private static function extractSiteIdFromArrayRow(array $values) : ?int
    {
        foreach (['idsite', 'idSite'] as $siteColumn) {
            if (isset($values[$siteColumn]) && is_numeric($values[$siteColumn])) {
                return (int) $values[$siteColumn];
            }
        }
        return null;
    }
    private static function extractSiteIdFromRow(Row $row) : ?int
    {
        foreach (['idsite', 'idSite'] as $siteColumn) {
            $siteId = $row->getColumn($siteColumn);
            if (is_numeric($siteId)) {
                return (int) $siteId;
            }
            $siteId = $row->getMetadata($siteColumn);
            if (is_numeric($siteId)) {
                return (int) $siteId;
            }
        }
        return null;
    }
    /**
     * @param array<string, mixed> $values
     * @return array<string, mixed>
     */
    private static function roundDirectScalarArrayValues(array $values) : array
    {
        foreach ($values as $columnName => $value) {
            if (is_array($value)) {
                continue;
            }
            $metricName = self::normalizeRawMetricColumnName((string) $columnName);
            if (self::shouldRoundColumn($metricName, null) && self::shouldRoundValue($value)) {
                $values[$columnName] = self::roundToNearestTen((float) $value);
            }
        }
        return $values;
    }
    /**
     * @param string|int $key
     */
    private static function resolveCurrentSiteIdFromMapKey(DataTable\Map $dataTableMap, $key, ?int $currentSiteId) : ?int
    {
        if ($currentSiteId !== null) {
            return $currentSiteId;
        }
        $keyName = strtolower((string) $dataTableMap->getKeyName());
        if ($keyName !== 'site' && $keyName !== 'idsite') {
            return null;
        }
        if (is_numeric($key) && (string) (int) $key === (string) $key && (int) $key > 0) {
            return (int) $key;
        }
        return null;
    }
}
