<?php

namespace classes;

use Exception;
use mysqli_result;
use Throwable;

class module_usage_service
{
    private module_usage_registry $registry;

    public function __construct(?module_usage_registry $registry = null)
    {
        module_usage_schema_bootstrap::ensureTables();
        $this->registry = $registry ?? new module_usage_registry();
    }

    public function summary(array $filters = []): array
    {
        $moduleFilter = isset($filters['module']) ? $this->registry->normalizeModuleKey((string)$filters['module']) : '';
        $periodFilter = isset($filters['period']) ? strtolower(trim((string)$filters['period'])) : '';
        $statusFilter = isset($filters['status']) ? strtolower(trim((string)$filters['status'])) : '';
        $date = isset($filters['date']) ? (string)$filters['date'] : null;

        $metrics = [];
        foreach ($this->registry->all() as $descriptor) {
            if ($moduleFilter !== '' && $descriptor['module_key'] !== $moduleFilter) {
                continue;
            }
            if ($periodFilter !== '' && $periodFilter !== 'all' && $descriptor['period'] !== $periodFilter) {
                continue;
            }

            $metric = $this->currentMetric($descriptor, $date);
            if ($statusFilter !== '' && $metric['status'] !== $statusFilter) {
                continue;
            }
            $metrics[] = $metric;
        }

        $modules = [];
        foreach ($metrics as $metric) {
            $moduleKey = $metric['module_key'];
            if (!isset($modules[$moduleKey])) {
                $modules[$moduleKey] = [
                    'key' => $moduleKey,
                    'label' => $metric['module_label'],
                    'status' => 'ok',
                    'metrics' => [],
                ];
            }
            $modules[$moduleKey]['metrics'][] = $metric;
            $modules[$moduleKey]['status'] = $this->worseStatus($modules[$moduleKey]['status'], $metric['status']);
        }

        return [
            'generated_at' => date('c'),
            'filters' => [
                'module' => $moduleFilter !== '' ? $moduleFilter : null,
                'period' => $periodFilter !== '' ? $periodFilter : null,
                'status' => $statusFilter !== '' ? $statusFilter : null,
                'date' => $date,
            ],
            'modules' => array_values($modules),
            'metrics' => $metrics,
        ];
    }

    public function moduleDetail(string $moduleKey, array $filters = []): array
    {
        $moduleKey = $this->registry->normalizeModuleKey($moduleKey);
        $descriptors = $this->registry->forModule($moduleKey);
        $date = isset($filters['date']) ? (string)$filters['date'] : null;
        $metrics = array_map(fn(array $descriptor): array => $this->currentMetric($descriptor, $date), $descriptors);

        return [
            'generated_at' => date('c'),
            'module_key' => $moduleKey,
            'metrics' => $metrics,
            'history' => $this->historyForModule($moduleKey, $filters),
        ];
    }

    public function metricsForModule(string $moduleKey): array
    {
        $moduleKey = $this->registry->normalizeModuleKey($moduleKey);
        return array_map(
            fn(array $descriptor): array => $this->currentMetric($descriptor),
            $this->registry->forModule($moduleKey)
        );
    }

    /**
     * Atomically records usage and enforces blocking quotas when the metric is configured for block mode.
     *
     * @throws Exception
     */
    public function reserveOrFail(string $moduleKey, string $metricKey, float $quantity = 1.0, array $metadata = []): array
    {
        $descriptor = $this->requireDescriptor($moduleKey, $metricKey);
        $quantity = max(0.0, $quantity);
        if ($quantity <= 0.0) {
            return $this->currentMetric($descriptor);
        }

        $setting = $this->settingFor($descriptor);
        if (($setting['enabled'] ?? true) !== true || ($setting['enforce_mode'] ?? 'observe') !== 'block') {
            return $this->recordUsage($moduleKey, $metricKey, $quantity, $metadata);
        }

        $limit = $this->resolveLimitQuantity($descriptor);
        if ($limit === null) {
            return $this->recordUsage($moduleKey, $metricKey, $quantity, $metadata);
        }

        $period = $this->periodWindow((string)$descriptor['period']);
        $this->insertCounterIfMissing($descriptor, $period, $limit);

        global $db;
        $where = $this->counterWhereSql($descriptor, $period);
        $quantitySql = $this->numberSql($quantity);
        $limitSql = $this->numberSql($limit);
        $metadataSql = $this->jsonSql($metadata);

        $db->query(
            "UPDATE module_usage_counters
             SET used_quantity = used_quantity + {$quantitySql},
                 limit_quantity = {$limitSql},
                 metadata_json = {$metadataSql},
                 status = CASE
                    WHEN {$limitSql} <= 0 OR ((used_quantity + {$quantitySql}) >= {$limitSql}) THEN 'exhausted'
                    WHEN ((used_quantity + {$quantitySql}) / {$limitSql}) * 100 >= " . module_usage_registry::DEFAULT_SOFT_LIMIT_PERCENT . " THEN 'near_limit'
                    ELSE 'ok'
                 END
             WHERE {$where} AND (used_quantity + {$quantitySql}) <= {$limitSql}"
        );

        if ((int)$db->conn()->affected_rows <= 0) {
            throw new Exception($this->quotaExceededMessage($descriptor));
        }

        return $this->currentMetric($descriptor);
    }

    public function recordUsage(string $moduleKey, string $metricKey, float $quantity = 1.0, array $metadata = []): array
    {
        $descriptor = $this->requireDescriptor($moduleKey, $metricKey);
        $quantity = max(0.0, $quantity);
        if ($quantity <= 0.0 || !$this->databaseReady()) {
            return $this->currentMetric($descriptor);
        }

        $limit = $this->resolveLimitQuantity($descriptor);
        $setting = $this->settingFor($descriptor);
        $period = $this->periodWindow((string)$descriptor['period']);
        $this->insertCounterIfMissing($descriptor, $period, $limit);

        global $db;
        $where = $this->counterWhereSql($descriptor, $period);
        $quantitySql = $this->numberSql($quantity);
        $limitSql = $this->nullableNumberSql($limit);
        $metadataSql = $this->jsonSql($metadata);
        $softLimitSql = $this->numberSql((float)$setting['soft_limit_percent']);
        $statusSql = (($setting['enabled'] ?? true) !== true)
            ? $this->sqlString('disabled')
            : "CASE
                WHEN {$limitSql} IS NULL THEN 'unlimited'
                WHEN {$limitSql} <= 0 AND (used_quantity + {$quantitySql}) > 0 THEN 'exhausted'
                WHEN {$limitSql} <= 0 THEN 'ok'
                WHEN (used_quantity + {$quantitySql}) >= {$limitSql} THEN 'exhausted'
                WHEN ((used_quantity + {$quantitySql}) / {$limitSql}) * 100 >= {$softLimitSql} THEN 'near_limit'
                ELSE 'ok'
             END";

        $db->query(
            "UPDATE module_usage_counters
             SET used_quantity = used_quantity + {$quantitySql},
                 limit_quantity = {$limitSql},
                 metadata_json = {$metadataSql},
                 status = {$statusSql}
             WHERE {$where}"
        );

        return $this->currentMetric($descriptor);
    }

    public function currentUsedQuantity(string $moduleKey, string $metricKey): int
    {
        $descriptor = $this->requireDescriptor($moduleKey, $metricKey);
        $metric = $this->currentMetric($descriptor);
        return (int)floor((float)($metric['used'] ?? 0));
    }

    public function updateQuotaSetting(string $moduleKey, string $metricKey, array $payload): array
    {
        $descriptor = $this->requireDescriptor($moduleKey, $metricKey);
        $setting = $this->settingFor($descriptor);

        $enabled = array_key_exists('enabled', $payload) ? $this->toBool($payload['enabled']) : (bool)$setting['enabled'];
        $enforceMode = array_key_exists('enforce_mode', $payload) ? strtolower(trim((string)$payload['enforce_mode'])) : (string)$setting['enforce_mode'];
        if (!in_array($enforceMode, ['observe', 'block'], true)) {
            throw new Exception('Invalid enforce mode.');
        }

        $softLimitPercent = array_key_exists('soft_limit_percent', $payload)
            ? (float)$payload['soft_limit_percent']
            : (float)$setting['soft_limit_percent'];
        if ($softLimitPercent < 1.0 || $softLimitPercent > 100.0) {
            throw new Exception('Soft limit percent must be between 1 and 100.');
        }

        if (array_key_exists('limit', $payload) || array_key_exists('hard_limit', $payload) || array_key_exists('hard_limit_quantity', $payload)) {
            if (empty($descriptor['writable_limit']) || empty($descriptor['config_module']) || empty($descriptor['config_variable'])) {
                throw new Exception('quota_not_writable');
            }
            $limitValue = $payload['limit'] ?? $payload['hard_limit'] ?? $payload['hard_limit_quantity'];
            if (!is_numeric($limitValue) || (int)$limitValue < 0) {
                throw new Exception('Limit must be a non-negative integer.');
            }
            $this->writeConfigLimit($descriptor, (int)$limitValue);
        }

        if ($this->databaseReady()) {
            global $db;
            $moduleKeySql = $this->sqlString((string)$descriptor['module_key']);
            $metricKeySql = $this->sqlString((string)$descriptor['metric_key']);
            $enabledSql = $enabled ? '1' : '0';
            $modeSql = $this->sqlString($enforceMode);
            $softLimitSql = $this->numberSql($softLimitPercent);
            $db->query(
                "INSERT INTO module_quota_settings (module_key, metric_key, enabled, enforce_mode, soft_limit_percent)
                 VALUES ({$moduleKeySql}, {$metricKeySql}, {$enabledSql}, {$modeSql}, {$softLimitSql})
                 ON DUPLICATE KEY UPDATE
                    enabled = VALUES(enabled),
                    enforce_mode = VALUES(enforce_mode),
                    soft_limit_percent = VALUES(soft_limit_percent)"
            );
        }

        return $this->currentMetric($descriptor);
    }

    public function recordProviderSnapshotFromLegacyUsage(string $moduleKey, array $usage, array $rawPayload = []): ?array
    {
        $moduleKey = $this->registry->normalizeModuleKey($moduleKey);
        $descriptor = null;
        foreach ($this->registry->forModule($moduleKey) as $candidate) {
            if (($candidate['source'] ?? '') === 'provider_snapshot') {
                $descriptor = $candidate;
                break;
            }
        }

        if ($descriptor === null) {
            return null;
        }

        $used = $this->firstNumeric($usage, ['used', 'calls_used', 'messages_used']);
        $limit = $this->firstNumeric($usage, ['limit', 'quota', 'quota_calls', 'total_calls']);
        $remaining = $this->firstNumeric($usage, ['remaining', 'calls_remaining', 'messages_remaining']);
        $percent = $this->firstNumeric($usage, ['usage_percent', 'percent']);
        $usageAvailable = !array_key_exists('usage_available', $usage) || $this->toBool($usage['usage_available']);
        $unavailableReason = trim((string)($usage['unavailable_reason'] ?? ''));

        if ($used === null && $limit === null && $usageAvailable && $unavailableReason === '') {
            return null;
        }

        if ($remaining === null && $used !== null && $limit !== null) {
            $remaining = max(0.0, $limit - $used);
        }
        if ($percent === null && $used !== null && $limit !== null && $limit > 0) {
            $percent = round(($used / $limit) * 100, 4);
        }

        $status = (!$usageAvailable || $unavailableReason !== '')
            ? 'unknown'
            : $this->statusForUsage($used, $limit, $this->settingFor($descriptor));
        $payload = array_merge($rawPayload, ['usage' => $this->redactPayload($usage)]);

        if ($this->databaseReady()) {
            try {
                global $db;
                $db->query(
                    "INSERT INTO module_usage_snapshots
                    (module_key, metric_key, source, period_key, unit, used_quantity, limit_quantity, remaining_quantity, usage_percent, status, raw_payload_json, checked_at)
                    VALUES (
                        " . $this->sqlString((string)$descriptor['module_key']) . ",
                        " . $this->sqlString((string)$descriptor['metric_key']) . ",
                        'provider',
                        'provider',
                        " . $this->sqlString((string)$descriptor['unit']) . ",
                        " . $this->nullableNumberSql($used) . ",
                        " . $this->nullableNumberSql($limit) . ",
                        " . $this->nullableNumberSql($remaining) . ",
                        " . $this->nullableNumberSql($percent) . ",
                        " . $this->sqlString($status) . ",
                        " . $this->jsonSql($payload) . ",
                        " . $this->sqlString(date('Y-m-d H:i:s')) . "
                    )"
                );
            } catch (Throwable) {
                // Provider snapshots are observability data. They must never break probes.
            }
        }

        return $this->providerMetricFromValues($descriptor, $used, $limit, $remaining, $percent, $status, date('c'), $usage);
    }

    public function primarySystemUsage(array $metrics): ?array
    {
        $metrics = array_values(array_filter(
            $metrics,
            static fn(array $metric): bool => ($metric['limit'] ?? null) !== null || ($metric['used'] ?? null) !== null
        ));
        if ($metrics === []) {
            return null;
        }

        usort($metrics, function (array $left, array $right): int {
            if (($left['primary'] ?? false) !== ($right['primary'] ?? false)) {
                return ($right['primary'] ?? false) <=> ($left['primary'] ?? false);
            }
            $leftRank = $this->statusRank((string)($left['status'] ?? 'unknown'));
            $rightRank = $this->statusRank((string)($right['status'] ?? 'unknown'));
            return $rightRank <=> $leftRank;
        });

        $metric = $metrics[0];
        return [
            'provider' => $metric['module_key'],
            'metric_key' => $metric['metric_key'],
            'unit' => $metric['unit'],
            'period' => $metric['period'],
            'calls_used' => $metric['used'],
            'quota_calls' => $metric['limit'],
            'calls_remaining' => $metric['remaining'],
            'usage_percent' => $metric['usage_percent'],
            'status' => $metric['status'],
            'source' => $metric['source'],
            'enforce_mode' => $metric['enforce_mode'],
        ];
    }

    public function currentMetric(array $descriptor, ?string $date = null): array
    {
        $descriptor = $this->normalizeDescriptor($descriptor);
        $setting = $this->settingFor($descriptor);
        $window = $this->periodWindow((string)$descriptor['period'], $date);
        $limit = $this->resolveLimitQuantity($descriptor);
        $used = null;
        $updatedAt = null;
        $historyAvailable = false;
        $snapshotExtra = [];

        if (($descriptor['source'] ?? '') === 'provider_snapshot') {
            $snapshot = $this->latestProviderSnapshot($descriptor);
            if ($snapshot !== null) {
                $used = $snapshot['used_quantity'];
                $limit = $snapshot['limit_quantity'];
                $updatedAt = $snapshot['checked_at'];
                $snapshotExtra = $snapshot['extra'];
                $historyAvailable = true;
            }
        } else {
            $counter = $this->counterFor($descriptor, $window);
            if ($counter !== null) {
                $used = $counter['used_quantity'];
                $limit = $counter['limit_quantity'] ?? $limit;
                $updatedAt = $counter['updated_at'] ?? $counter['created_at'] ?? null;
                $historyAvailable = true;
            } else {
                $derived = $this->derivedOrLegacyUsage($descriptor, $window);
                if ($derived !== null) {
                    $used = $derived;
                    $historyAvailable = true;
                } elseif (($descriptor['source'] ?? '') === 'internal_counter') {
                    $used = 0.0;
                    $historyAvailable = true;
                }
            }
        }

        $status = $this->statusForUsage($used, $limit, $setting);
        $remaining = ($used !== null && $limit !== null) ? max(0.0, $limit - $used) : null;
        $usagePercent = ($used !== null && $limit !== null && $limit > 0) ? round(($used / $limit) * 100, 2) : null;

        return array_merge([
            'module_key' => $descriptor['module_key'],
            'module_label' => $descriptor['module_label'],
            'metric_key' => $descriptor['metric_key'],
            'metric_label' => $descriptor['metric_label'],
            'unit' => $descriptor['unit'],
            'period' => $descriptor['period'],
            'scope_type' => $descriptor['scope_type'],
            'scope_id' => $descriptor['scope_id'],
            'source' => $descriptor['source'],
            'primary' => (bool)$descriptor['primary'],
            'writable_limit' => (bool)$descriptor['writable_limit'],
            'limit_source' => isset($descriptor['config_variable']) ? 'module_config' : (($descriptor['source'] ?? '') === 'provider_snapshot' ? 'provider' : null),
            'config_module' => $descriptor['config_module'] ?? null,
            'config_variable' => $descriptor['config_variable'] ?? null,
            'used' => $used,
            'limit' => $limit,
            'remaining' => $remaining,
            'usage_percent' => $usagePercent,
            'status' => $status,
            'enabled' => (bool)$setting['enabled'],
            'enforce_mode' => $setting['enforce_mode'],
            'soft_limit_percent' => (float)$setting['soft_limit_percent'],
            'window' => [
                'start' => $window['start_c'],
                'end' => $window['end_c'],
                'timezone' => date_default_timezone_get(),
            ],
            'updated_at' => $updatedAt,
            'history_available' => $historyAvailable,
        ], $snapshotExtra);
    }

    private function requireDescriptor(string $moduleKey, string $metricKey): array
    {
        $descriptor = $this->registry->find($moduleKey, $metricKey);
        if ($descriptor === null) {
            throw new Exception('Unknown module usage metric.');
        }
        return $descriptor;
    }

    private function normalizeDescriptor(array $descriptor): array
    {
        $found = $this->registry->find((string)$descriptor['module_key'], (string)$descriptor['metric_key']);
        return $found ?? $descriptor;
    }

    private function settingFor(array $descriptor): array
    {
        $default = [
            'enabled' => true,
            'enforce_mode' => (string)($descriptor['default_enforce_mode'] ?? 'observe'),
            'soft_limit_percent' => (float)($descriptor['soft_limit_percent'] ?? module_usage_registry::DEFAULT_SOFT_LIMIT_PERCENT),
        ];

        if (!$this->databaseReady()) {
            return $default;
        }

        try {
            global $db;
            $result = $db->query(
                "SELECT enabled, enforce_mode, soft_limit_percent
                 FROM module_quota_settings
                 WHERE module_key = " . $this->sqlString((string)$descriptor['module_key']) . "
                   AND metric_key = " . $this->sqlString((string)$descriptor['metric_key']) . "
                 LIMIT 1"
            );
            $row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
            if (!is_array($row)) {
                return $default;
            }
            return [
                'enabled' => (bool)((int)($row['enabled'] ?? 1)),
                'enforce_mode' => in_array((string)($row['enforce_mode'] ?? ''), ['observe', 'block'], true)
                    ? (string)$row['enforce_mode']
                    : $default['enforce_mode'],
                'soft_limit_percent' => is_numeric($row['soft_limit_percent'] ?? null)
                    ? (float)$row['soft_limit_percent']
                    : $default['soft_limit_percent'],
            ];
        } catch (Throwable) {
            return $default;
        }
    }

    private function resolveLimitQuantity(array $descriptor): ?float
    {
        if (empty($descriptor['config_module']) || empty($descriptor['config_variable']) || !$this->databaseReady()) {
            return null;
        }

        try {
            global $db;
            $result = $db->query(
                "SELECT value
                 FROM module_config
                 WHERE module = " . $this->sqlString((string)$descriptor['config_module']) . "
                   AND variable = " . $this->sqlString((string)$descriptor['config_variable']) . "
                 LIMIT 1"
            );
            $row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
            if (!is_array($row) || !is_numeric($row['value'] ?? null)) {
                return null;
            }
            return max(0.0, (float)$row['value']);
        } catch (Throwable) {
            return null;
        }
    }

    private function writeConfigLimit(array $descriptor, int $limit): void
    {
        if (!$this->databaseReady()) {
            return;
        }

        global $db;
        $module = (string)$descriptor['config_module'];
        $variable = (string)$descriptor['config_variable'];
        $type = (string)($descriptor['config_type'] ?? 'int');

        $existing = $db->query(
            "SELECT id
             FROM module_config
             WHERE module = " . $this->sqlString($module) . "
               AND variable = " . $this->sqlString($variable) . "
             LIMIT 1"
        );
        if ($existing instanceof mysqli_result && $existing->num_rows > 0) {
            $db->query(
                "UPDATE module_config
                 SET value = " . $this->sqlString((string)$limit) . ", type = " . $this->sqlString($type) . "
                 WHERE module = " . $this->sqlString($module) . "
                   AND variable = " . $this->sqlString($variable)
            );
        } else {
            $db->query(
                "INSERT INTO module_config (module, variable, value, type)
                 VALUES (" . $this->sqlString($module) . ", " . $this->sqlString($variable) . ", " . $this->sqlString((string)$limit) . ", " . $this->sqlString($type) . ")"
            );
        }
        system_search_cache::markDirtyTable('module_config');
    }

    private function insertCounterIfMissing(array $descriptor, array $period, ?float $limit): void
    {
        if (!$this->databaseReady()) {
            return;
        }

        global $db;
        $baseline = $this->derivedOrLegacyUsage($descriptor, $period);
        $baseline = $baseline === null ? 0.0 : max(0.0, (float)$baseline);
        $metadata = [
            'created_from' => $baseline > 0 ? 'legacy_or_derived_baseline' : 'counter',
        ];

        $db->query(
            "INSERT IGNORE INTO module_usage_counters
             (module_key, metric_key, scope_type, scope_id, period_key, period_start, period_end, unit, used_quantity, limit_quantity, status, metadata_json)
             VALUES (
                " . $this->sqlString((string)$descriptor['module_key']) . ",
                " . $this->sqlString((string)$descriptor['metric_key']) . ",
                " . $this->sqlString((string)$descriptor['scope_type']) . ",
                " . $this->sqlString((string)$descriptor['scope_id']) . ",
                " . $this->sqlString($period['key']) . ",
                " . $this->sqlString($period['start_sql']) . ",
                " . ($period['end_sql'] === null ? 'NULL' : $this->sqlString($period['end_sql'])) . ",
                " . $this->sqlString((string)$descriptor['unit']) . ",
                " . $this->numberSql($baseline) . ",
                " . $this->nullableNumberSql($limit) . ",
                " . $this->sqlString($this->statusForUsage($baseline, $limit, $this->settingFor($descriptor))) . ",
                " . $this->jsonSql($metadata) . "
             )"
        );
    }

    private function counterFor(array $descriptor, array $period): ?array
    {
        if (!$this->databaseReady()) {
            return null;
        }

        try {
            global $db;
            $result = $db->query(
                "SELECT used_quantity, limit_quantity, status, created_at, updated_at
                 FROM module_usage_counters
                 WHERE " . $this->counterWhereSql($descriptor, $period) . "
                 LIMIT 1"
            );
            $row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
            if (!is_array($row)) {
                return null;
            }
            return [
                'used_quantity' => (float)$row['used_quantity'],
                'limit_quantity' => $row['limit_quantity'] === null ? null : (float)$row['limit_quantity'],
                'status' => (string)$row['status'],
                'created_at' => $row['created_at'] ?? null,
                'updated_at' => $row['updated_at'] ?? null,
            ];
        } catch (Throwable) {
            return null;
        }
    }

    private function latestProviderSnapshot(array $descriptor): ?array
    {
        if (!$this->databaseReady()) {
            return null;
        }

        try {
            global $db;
            $result = $db->query(
                "SELECT used_quantity, limit_quantity, remaining_quantity, usage_percent, status, raw_payload_json, checked_at
                 FROM module_usage_snapshots
                 WHERE module_key = " . $this->sqlString((string)$descriptor['module_key']) . "
                   AND metric_key = " . $this->sqlString((string)$descriptor['metric_key']) . "
                 ORDER BY checked_at DESC, id DESC
                 LIMIT 1"
            );
            $row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
            if (!is_array($row)) {
                return null;
            }
            $raw = json_decode((string)($row['raw_payload_json'] ?? ''), true);
            $usage = is_array($raw) && isset($raw['usage']) && is_array($raw['usage']) ? $raw['usage'] : [];
            $extra = [];
            if (isset($usage['version'])) {
                $extra['version'] = (string)$usage['version'];
            }
            if (array_key_exists('usage_available', $usage)) {
                $extra['usage_available'] = $this->toBool($usage['usage_available']);
            }
            if (isset($usage['unavailable_reason'])) {
                $extra['unavailable_reason'] = (string)$usage['unavailable_reason'];
            }
            if (isset($usage['detected_keys']) && is_array($usage['detected_keys'])) {
                $extra['detected_keys'] = array_values(array_map('strval', $usage['detected_keys']));
            }

            return [
                'used_quantity' => $row['used_quantity'] === null ? null : (float)$row['used_quantity'],
                'limit_quantity' => $row['limit_quantity'] === null ? null : (float)$row['limit_quantity'],
                'remaining_quantity' => $row['remaining_quantity'] === null ? null : (float)$row['remaining_quantity'],
                'usage_percent' => $row['usage_percent'] === null ? null : (float)$row['usage_percent'],
                'status' => (string)$row['status'],
                'checked_at' => $row['checked_at'] ? date('c', strtotime((string)$row['checked_at'])) : null,
                'extra' => $extra,
            ];
        } catch (Throwable) {
            return null;
        }
    }

    private function providerMetricFromValues(array $descriptor, ?float $used, ?float $limit, ?float $remaining, ?float $percent, string $status, string $checkedAt, array $usage): array
    {
        return [
            'module_key' => $descriptor['module_key'],
            'module_label' => $descriptor['module_label'],
            'metric_key' => $descriptor['metric_key'],
            'metric_label' => $descriptor['metric_label'],
            'unit' => $descriptor['unit'],
            'period' => $descriptor['period'],
            'scope_type' => $descriptor['scope_type'],
            'scope_id' => $descriptor['scope_id'],
            'source' => $descriptor['source'],
            'primary' => (bool)$descriptor['primary'],
            'writable_limit' => false,
            'limit_source' => 'provider',
            'used' => $used,
            'limit' => $limit,
            'remaining' => $remaining,
            'usage_percent' => $percent,
            'status' => $status,
            'enabled' => true,
            'enforce_mode' => 'observe',
            'soft_limit_percent' => module_usage_registry::DEFAULT_SOFT_LIMIT_PERCENT,
            'window' => ['start' => null, 'end' => null, 'timezone' => date_default_timezone_get()],
            'updated_at' => $checkedAt,
            'history_available' => true,
            'version' => isset($usage['version']) ? (string)$usage['version'] : null,
            'usage_available' => array_key_exists('usage_available', $usage) ? $this->toBool($usage['usage_available']) : true,
            'unavailable_reason' => isset($usage['unavailable_reason']) ? (string)$usage['unavailable_reason'] : null,
            'detected_keys' => isset($usage['detected_keys']) && is_array($usage['detected_keys'])
                ? array_values(array_map('strval', $usage['detected_keys']))
                : [],
        ];
    }

    private function derivedOrLegacyUsage(array $descriptor, array $period): ?float
    {
        try {
            if (isset($descriptor['legacy_count_table'])) {
                return $this->countRowsInPeriod((string)$descriptor['legacy_count_table'], 'created_at', $period);
            }

            if (isset($descriptor['legacy_log_module'])) {
                return $this->countActionLogs(
                    (string)$descriptor['legacy_log_module'],
                    isset($descriptor['legacy_log_action']) ? (string)$descriptor['legacy_log_action'] : null,
                    $period
                );
            }

            return match ($descriptor['module_key'] . '.' . $descriptor['metric_key']) {
                'backups.backup_jobs' => $this->countRowsInPeriod('backup_jobs', 'created_at', $period),
                'backups.stored_bytes' => $this->sumColumn('backup_records', 'total_bytes'),
                'coolify.operations' => $this->countRowsInPeriod('coolify_operations', 'created_at', $period),
                'selfserve.wash_sessions' => $this->countRowsInPeriod('selfserve_wash_sessions', 'created_at', $period),
                'xlvask.usage_rows' => $this->countRowsInPeriod('xlvask_usage_logs', 'StartTime', $period),
                'attachments.stored_files' => $this->countRowsInPeriod('object_attachments', null, $period),
                'forms.submissions' => $this->countRowsInPeriod('form_submissions', 'created_at', $period),
                'notifications.notification_sends' => $this->countRowsInPeriod('notifications', 'created_at', $period),
                'edgegateway.relay_commands' => $this->countRowsInPeriod('edge_gateway_operations', 'created_at', $period),
                'system.cron_runs' => $this->countRowsInPeriod('cron_task_runs', 'created_at', $period),
                default => null,
            };
        } catch (Throwable) {
            return null;
        }
    }

    private function countActionLogs(string $module, ?string $action, array $period): ?float
    {
        if (!$this->tableExists('module_usage_logs')) {
            return null;
        }

        global $db;
        $where = "UPPER(module) = " . $this->sqlString(strtoupper($module));
        if ($action !== null && $action !== '') {
            $where .= " AND UPPER(action) = " . $this->sqlString(strtoupper($action));
        }
        $where .= $this->periodWhereSql('created_at', $period);

        $result = $db->query("SELECT COUNT(*) AS usage_count FROM module_usage_logs WHERE {$where}");
        $row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
        return is_array($row) ? (float)$row['usage_count'] : null;
    }

    private function countRowsInPeriod(string $table, ?string $dateColumn, array $period): ?float
    {
        if (!$this->tableExists($table)) {
            return null;
        }
        if ($dateColumn !== null && !$this->columnExists($table, $dateColumn)) {
            return null;
        }

        global $db;
        $where = '1=1';
        if ($dateColumn !== null) {
            $where .= $this->periodWhereSql($dateColumn, $period);
        } elseif ($period['key'] !== 'all_time') {
            return null;
        }

        $result = $db->query("SELECT COUNT(*) AS usage_count FROM `{$table}` WHERE {$where}");
        $row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
        return is_array($row) ? (float)$row['usage_count'] : null;
    }

    private function sumColumn(string $table, string $column): ?float
    {
        if (!$this->tableExists($table) || !$this->columnExists($table, $column)) {
            return null;
        }

        global $db;
        $result = $db->query("SELECT COALESCE(SUM(`{$column}`), 0) AS usage_sum FROM `{$table}`");
        $row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
        return is_array($row) ? (float)$row['usage_sum'] : null;
    }

    private function historyForModule(string $moduleKey, array $filters): array
    {
        if (!$this->databaseReady()) {
            return [];
        }

        $limit = isset($filters['limit']) && is_numeric($filters['limit']) ? max(1, min(200, (int)$filters['limit'])) : 100;
        $rows = [];

        try {
            global $db;
            $result = $db->query(
                "SELECT module_key, metric_key, period_key, period_start, period_end, used_quantity, limit_quantity, status, updated_at, created_at
                 FROM module_usage_counters
                 WHERE module_key = " . $this->sqlString($moduleKey) . "
                 ORDER BY period_start DESC, id DESC
                 LIMIT {$limit}"
            );
            if ($result instanceof mysqli_result) {
                while ($row = $result->fetch_assoc()) {
                    $rows[] = $row;
                }
            }
        } catch (Throwable) {
            return [];
        }

        return $rows;
    }

    private function statusForUsage(?float $used, ?float $limit, array $setting): string
    {
        if (($setting['enabled'] ?? true) !== true) {
            return 'disabled';
        }
        if ($used === null) {
            return 'unknown';
        }
        if ($limit === null) {
            return 'unlimited';
        }
        if ($limit <= 0.0) {
            return $used > 0.0 ? 'exhausted' : 'ok';
        }

        $percent = ($used / $limit) * 100;
        if ($used >= $limit || $percent >= 100.0) {
            return 'exhausted';
        }
        if ($percent >= (float)($setting['soft_limit_percent'] ?? module_usage_registry::DEFAULT_SOFT_LIMIT_PERCENT)) {
            return 'near_limit';
        }
        return 'ok';
    }

    private function worseStatus(string $current, string $candidate): string
    {
        return $this->statusRank($candidate) > $this->statusRank($current) ? $candidate : $current;
    }

    private function statusRank(string $status): int
    {
        return match ($status) {
            'exhausted' => 5,
            'near_limit' => 4,
            'unknown' => 3,
            'disabled' => 2,
            'unlimited' => 1,
            'ok' => 0,
            default => 0,
        };
    }

    private function quotaExceededMessage(array $descriptor): string
    {
        return match ((string)$descriptor['period']) {
            'day' => 'Daily limit exceeded',
            'month' => 'Monthly limit exceeded',
            default => 'Quota limit exceeded',
        };
    }

    private function periodWindow(string $period, ?string $date = null): array
    {
        $timestamp = $date ? strtotime($date) : time();
        if ($timestamp === false) {
            $timestamp = time();
        }

        return match ($period) {
            'day' => $this->periodFromTimestamps('day', strtotime(date('Y-m-d 00:00:00', $timestamp)), strtotime(date('Y-m-d 00:00:00', $timestamp) . ' +1 day')),
            'month' => $this->periodFromTimestamps('month', strtotime(date('Y-m-01 00:00:00', $timestamp)), strtotime(date('Y-m-01 00:00:00', $timestamp) . ' +1 month')),
            'provider' => ['key' => 'provider', 'start_sql' => date('Y-m-d 00:00:00', $timestamp), 'end_sql' => null, 'start_c' => null, 'end_c' => null],
            default => ['key' => 'all_time', 'start_sql' => '1970-01-01 00:00:00', 'end_sql' => null, 'start_c' => null, 'end_c' => null],
        };
    }

    private function periodFromTimestamps(string $key, int $start, int $end): array
    {
        return [
            'key' => $key,
            'start_sql' => date('Y-m-d H:i:s', $start),
            'end_sql' => date('Y-m-d H:i:s', $end),
            'start_c' => date('c', $start),
            'end_c' => date('c', $end),
        ];
    }

    private function periodWhereSql(string $dateColumn, array $period): string
    {
        if ($period['key'] === 'all_time' || $period['key'] === 'provider') {
            return '';
        }

        $dateColumn = preg_replace('/[^a-zA-Z0-9_]/', '', $dateColumn);
        if ($dateColumn === '') {
            return '';
        }

        return " AND `{$dateColumn}` >= " . $this->sqlString($period['start_sql']) . " AND `{$dateColumn}` < " . $this->sqlString((string)$period['end_sql']);
    }

    private function counterWhereSql(array $descriptor, array $period): string
    {
        return "module_key = " . $this->sqlString((string)$descriptor['module_key'])
            . " AND metric_key = " . $this->sqlString((string)$descriptor['metric_key'])
            . " AND scope_type = " . $this->sqlString((string)$descriptor['scope_type'])
            . " AND scope_id = " . $this->sqlString((string)$descriptor['scope_id'])
            . " AND period_key = " . $this->sqlString($period['key'])
            . " AND period_start = " . $this->sqlString($period['start_sql']);
    }

    private function databaseReady(): bool
    {
        global $db;
        return isset($db) && is_object($db) && method_exists($db, 'query') && method_exists($db, 'conn');
    }

    private function tableExists(string $table): bool
    {
        if (!$this->databaseReady()) {
            return false;
        }

        try {
            global $db;
            $table = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
            if ($table === '') {
                return false;
            }
            $result = $db->query("SHOW TABLES LIKE " . $this->sqlString($table));
            return $result instanceof mysqli_result && $result->num_rows > 0;
        } catch (Throwable) {
            return false;
        }
    }

    private function columnExists(string $table, string $column): bool
    {
        if (!$this->databaseReady()) {
            return false;
        }

        try {
            global $db;
            $table = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
            $column = preg_replace('/[^a-zA-Z0-9_]/', '', $column);
            if ($table === '' || $column === '') {
                return false;
            }
            $result = $db->query("SHOW COLUMNS FROM `{$table}` LIKE " . $this->sqlString($column));
            return $result instanceof mysqli_result && $result->num_rows > 0;
        } catch (Throwable) {
            return false;
        }
    }

    private function firstNumeric(array $payload, array $keys): ?float
    {
        foreach ($keys as $key) {
            if (isset($payload[$key]) && is_numeric($payload[$key])) {
                return (float)$payload[$key];
            }
        }
        return null;
    }

    private function redactPayload(array $payload): array
    {
        $redacted = [];
        foreach ($payload as $key => $value) {
            $normalized = strtolower((string)$key);
            if (str_contains($normalized, 'key') || str_contains($normalized, 'token') || str_contains($normalized, 'secret')) {
                $redacted[$key] = '[redacted]';
                continue;
            }
            $redacted[$key] = is_array($value) ? $this->redactPayload($value) : $value;
        }
        return $redacted;
    }

    private function toBool(mixed $value): bool
    {
        if (is_bool($value)) {
            return $value;
        }
        return in_array(strtolower(trim((string)$value)), ['1', 'true', 'yes', 'on'], true);
    }

    private function sqlString(string $value): string
    {
        global $db;
        return "'" . $db->escape_string($value) . "'";
    }

    private function numberSql(float $value): string
    {
        return rtrim(rtrim(sprintf('%.4F', $value), '0'), '.') ?: '0';
    }

    private function nullableNumberSql(?float $value): string
    {
        return $value === null ? 'NULL' : $this->numberSql($value);
    }

    private function jsonSql(array $value): string
    {
        return $this->sqlString(json_encode($this->redactPayload($value), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }
}
