<?php
/**
 * LiveQuoteService.php
 * ------------------------------------------------------------
 * Production live quote aggregator for early-stage tracking.
 *
 * Purpose:
 * - Stage 1 and Stage 2 need live price first.
 * - Stage 3 and final execution still rely on closed candles.
 *
 * Provider routing:
 * - Crypto: Binance only.
 * - Metals: GoldAPI.io / Gold-API public / TwelveData / Metals API / MetalpriceAPI / Alpha Vantage.
 *   FMP is blocked for metals by default because many FMP subscriptions reject GCUSD/XAU commodity endpoints.
 * - Forex: FMP / TwelveData / Finnhub / Alpha Vantage.
 * - Stocks/indices: Finnhub / FMP / TwelveData / Alpha Vantage.
 */

require_once __DIR__ . '/../config/database.php';

if (file_exists(__DIR__ . '/ApiCreditGuard.php')) {
    require_once __DIR__ . '/ApiCreditGuard.php';
}
if (file_exists(__DIR__ . '/ApiProviderRotationService.php')) {
    require_once __DIR__ . '/ApiProviderRotationService.php';
}

class LiveQuoteService
{
    public const BUILD = 'LIVE_QUOTE_DB_CACHE_ENFORCED_V1_2026_06_06';

    private static array $activeProviderRows = [];

    public static function ensureTables(): void
    {
        try {
            db()->exec("
                CREATE TABLE IF NOT EXISTS market_quotes (
                    id BIGINT AUTO_INCREMENT PRIMARY KEY,
                    symbol VARCHAR(40) NOT NULL,
                    asset_type VARCHAR(30) NULL,
                    provider VARCHAR(60) NULL,
                    source VARCHAR(80) NULL,
                    provider_symbol VARCHAR(80) NULL,
                    price DECIMAL(24,10) NOT NULL,
                    bid DECIMAL(24,10) NULL,
                    ask DECIMAL(24,10) NULL,
                    quote_time DATETIME NULL,
                    raw_json LONGTEXT NULL,
                    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                    INDEX idx_symbol_created (symbol, created_at),
                    INDEX idx_symbol_quote_time (symbol, quote_time),
                    INDEX idx_provider_created (provider, created_at)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
            ");
        } catch (Throwable $e) {
        }
    }

    public static function getQuote(string $symbol, array $options = []): array
    {
        self::ensureTables();

        $symbol = self::normalizeSymbol($symbol);
        if ($symbol === '') {
            return self::fail('empty_symbol', 'Empty symbol.');
        }

        if (!self::settingEnabled('live_quote_service_enabled', true)) {
            return self::fail('disabled', 'LiveQuoteService disabled by system setting.', $symbol);
        }

        $assetType = self::assetType($symbol);
        $ttl = self::ttlSeconds($assetType);
        $force = !empty($options['force']);

        if (!$force) {
            $cached = self::cachedQuote($symbol, $ttl);
            if (!empty($cached['success'])) {
                return $cached;
            }
        }

        $providerOrder = self::providerOrder($assetType);
        $errors = [];

        foreach ($providerOrder as $providerName) {
            $providerName = self::normalizeProvider($providerName);
            if ($providerName === '') {
                continue;
            }

            foreach (self::providerQuoteEntries($providerName, $assetType, $symbol) as $entry) {
                $provider = self::normalizeProvider((string)($entry['provider'] ?? $providerName));
                $providerRow = is_array($entry['row'] ?? null) ? $entry['row'] : [];
                self::$activeProviderRows[$provider] = $providerRow;

                if (!self::providerAllowedForAsset($provider, $assetType, $symbol)) {
                    continue;
                }

                $guard = self::providerGuard($provider, $providerRow);
                if (empty($guard['allowed'])) {
                    $suffix = !empty($providerRow['id']) ? ' row #' . (int)$providerRow['id'] : '';
                    $errors[] = strtoupper($provider) . $suffix . ': ' . ($guard['reason'] ?? 'blocked by guard');
                    continue;
                }

                $quoteStartedAt = microtime(true);
                $result = self::fetchFromProvider($provider, $symbol, $assetType);
                $responseMs = (int)round((microtime(true) - $quoteStartedAt) * 1000);

                if (!empty($result['success']) && is_numeric($result['price'] ?? null)) {
                    $result['symbol'] = $symbol;
                    $result['asset_type'] = $assetType;
                    $result['api_setting_id'] = (int)($providerRow['id'] ?? 0);
                    $result['price'] = (float)$result['price'];
                    $result['quote_time'] = $result['quote_time'] ?? gmdate('Y-m-d H:i:s');
                    $result['timestamp'] = $result['timestamp'] ?? $result['quote_time'];

                    self::recordQuote($symbol, $assetType, $result);
                    self::recordProviderSuccess($provider, $providerRow, (string)($result['source'] ?? 'live_quote'), $symbol, null, $responseMs);

                    return $result;
                }

                $error = trim((string)($result['error'] ?? 'No usable live quote.'));
                $suffix = !empty($providerRow['id']) ? ' row #' . (int)$providerRow['id'] : '';
                $errors[] = strtoupper($provider) . $suffix . ': ' . $error;

                if (self::providerLimitDetected($error, $result['raw'] ?? null)) {
                    self::recordProviderLimit($provider, $error, $providerRow, (string)($result['source'] ?? 'live_quote'), $symbol, null, $responseMs);
                } else {
                    self::recordProviderFailure($provider, $error, $providerRow, (string)($result['source'] ?? 'live_quote'), $symbol, null, $responseMs);
                }
            }
        }

        return [
            'success' => false,
            'symbol' => $symbol,
            'asset_type' => $assetType,
            'provider' => 'live_quote_aggregator',
            'source' => 'no_provider_available',
            'price' => null,
            'timestamp' => gmdate('Y-m-d H:i:s'),
            'error' => implode(' | ', array_slice($errors, 0, 8)) ?: 'No live quote provider returned a usable price.',
        ];
    }

    public static function prefetchActiveAlertQuotes(int $maxAlerts = 100): array
    {
        self::ensureTables();

        /*
         * Credit-safe prefetch: this should never behave like a 1-minute bulk
         * quote scanner. It only warms a small number of active symbols that
         * do not already have a fresh cached quote.
         */
        $configuredMax = (int)self::setting('live_quote_prefetch_max_symbols', '10');
        $maxAlerts = max(1, min(50, min($maxAlerts, $configuredMax > 0 ? $configuredMax : 10)));
        $minSeconds = max(30, min(900, (int)self::setting('live_quote_prefetch_min_seconds', '180')));
        $symbols = [];

        try {
            $stmt = db()->query("
                SELECT DISTINCT symbol
                FROM institutional_alerts
                WHERE is_active = 1
                  AND triggered_at IS NULL
                  AND (expired_at IS NULL OR expired_at = '0000-00-00 00:00:00')
                  AND COALESCE(lifecycle_stage, 'TRACKING') NOT IN ('ALERT_SENT', 'INVALIDATED', 'EXPIRED')
                ORDER BY symbol ASC
                LIMIT {$maxAlerts}
            ");
            $symbols = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: [];
        } catch (Throwable $e) {
            return ['ok' => false, 'message' => 'Could not read active alert symbols: ' . $e->getMessage()];
        }

        $results = [];
        foreach ($symbols as $symbol) {
            $symbol = self::normalizeSymbol((string)$symbol);
            if ($symbol === '') {
                continue;
            }

            $cached = self::cachedQuote($symbol, $minSeconds);
            if (!empty($cached['success'])) {
                $results[$symbol] = [
                    'success' => true,
                    'price' => $cached['price'] ?? null,
                    'provider' => $cached['provider'] ?? null,
                    'source' => 'cached_prefetch_skip',
                    'error' => null,
                    'cached' => true,
                ];
                continue;
            }

            $quote = self::getQuote($symbol);
            $results[$symbol] = [
                'success' => !empty($quote['success']),
                'price' => $quote['price'] ?? null,
                'provider' => $quote['provider'] ?? null,
                'source' => $quote['source'] ?? null,
                'error' => $quote['error'] ?? null,
                'cached' => false,
            ];
        }

        return [
            'ok' => true,
            'checked_symbols' => count($results),
            'min_seconds' => $minSeconds,
            'max_symbols' => $maxAlerts,
            'results' => $results,
        ];
    }

    private static function fetchFromProvider(string $provider, string $symbol, string $assetType): array
    {
        switch ($provider) {
            case 'binance':
                return self::binanceQuote($symbol);
            case 'goldapiio':
            case 'goldapi':
                return self::goldApiIoQuote($symbol);
            case 'goldapi_free':
            case 'gold-api':
            case 'goldapi_public':
                return self::goldApiPublicQuote($symbol);
            case 'twelvedata':
                return self::twelveDataQuote($symbol);
            case 'fmp':
                return self::fmpQuote($symbol, $assetType);
            case 'finnhub':
                return self::finnhubQuote($symbol, $assetType);
            case 'alphavantage':
            case 'alpha_vantage':
                return self::alphaVantageQuote($symbol);
            case 'metalsapi':
            case 'metals-api':
                return self::metalsApiQuote($symbol);
            case 'metalpriceapi':
            case 'metalprice-api':
                return self::metalPriceApiQuote($symbol);
        }

        return self::fail($provider, 'Unsupported live quote provider: ' . $provider, $symbol);
    }

    private static function binanceQuote(string $symbol): array
    {
        $providerSymbol = self::binanceSymbol($symbol);
        $endpoints = [
            ['provider' => 'binance_futures', 'url' => 'https://fapi.binance.com/fapi/v1/ticker/price?' . http_build_query(['symbol' => $providerSymbol])],
            ['provider' => 'binance_spot', 'url' => 'https://api.binance.com/api/v3/ticker/price?' . http_build_query(['symbol' => $providerSymbol])],
            ['provider' => 'binance_vision', 'url' => 'https://data-api.binance.vision/api/v3/ticker/price?' . http_build_query(['symbol' => $providerSymbol])],
        ];

        $lastError = '';
        foreach ($endpoints as $endpoint) {
            $r = self::httpJson($endpoint['url'], 7);
            if (empty($r['ok'])) {
                $lastError = $r['error'] ?? 'HTTP failed';
                continue;
            }

            $data = $r['data'];
            if (isset($data['price']) && is_numeric($data['price'])) {
                return [
                    'success' => true,
                    'provider' => $endpoint['provider'],
                    'source' => 'binance_live_ticker',
                    'provider_symbol' => $providerSymbol,
                    'price' => (float)$data['price'],
                    'quote_time' => gmdate('Y-m-d H:i:s'),
                    'raw' => $data,
                ];
            }
        }

        return self::fail('binance', $lastError ?: 'No Binance ticker price.', $symbol);
    }

    private static function goldApiIoQuote(string $symbol): array
    {
        $metal = self::metalCode($symbol);
        if ($metal === '') {
            return self::fail('goldapiio', 'Symbol is not a supported precious metal.', $symbol);
        }

        $key = self::apiKey('goldapiio') ?: self::apiKey('goldapi');
        $key = trim((string)preg_replace('/^Bearer\s+/i', '', $key));
        if ($key === '') {
            return self::fail('goldapiio', 'GoldAPI.io key missing.', $symbol);
        }

        /*
         * GoldAPI.io official examples use:
         *   https://www.goldapi.io/api/XAU/USD
         * with the x-access-token header. Keep this provider metal-only and
         * count exactly one app-side credit per real HTTP request.
         */
        $endpoints = [
            'https://www.goldapi.io/api/' . rawurlencode($metal) . '/USD',
        ];

        $lastError = '';
        foreach ($endpoints as $url) {
            self::recordProviderUsage('goldapiio', 1, 'goldapiio_spot', $symbol);
            $r = self::httpJson($url, 10, [
                'x-access-token: ' . $key,
                'X-ACCESS-TOKEN: ' . $key,
                'Content-Type: application/json',
            ]);

            if (empty($r['ok'])) {
                $lastError = $r['error'] ?? 'GoldAPI.io request failed.';
                continue;
            }

            $data = $r['data'];
            if (isset($data['error'])) {
                $lastError = is_scalar($data['error']) ? (string)$data['error'] : json_encode($data['error']);
                continue;
            }
            if (isset($data['message']) && !isset($data['price'])) {
                $lastError = (string)$data['message'];
                continue;
            }

            $price = $data['price'] ?? $data['ask'] ?? $data['bid'] ?? null;
            if (!is_numeric($price) || (float)$price <= 0) {
                $lastError = 'No numeric price in GoldAPI.io response.';
                continue;
            }

            return [
                'success' => true,
                'provider' => 'goldapiio',
                'source' => 'goldapiio_spot',
                'provider_symbol' => $metal . '/USD',
                'price' => (float)$price,
                'bid' => isset($data['bid']) && is_numeric($data['bid']) ? (float)$data['bid'] : null,
                'ask' => isset($data['ask']) && is_numeric($data['ask']) ? (float)$data['ask'] : null,
                'quote_time' => !empty($data['timestamp']) && is_numeric($data['timestamp']) ? gmdate('Y-m-d H:i:s', (int)$data['timestamp']) : gmdate('Y-m-d H:i:s'),
                'raw' => $data,
            ];
        }

        return self::fail('goldapiio', $lastError ?: 'GoldAPI.io request failed.', $symbol);
    }

    private static function goldApiPublicQuote(string $symbol): array
    {
        $metal = self::metalCode($symbol);
        if ($metal === '') {
            return self::fail('gold-api', 'Symbol is not a supported precious metal.', $symbol);
        }

        $url = 'https://api.gold-api.com/price/' . rawurlencode($metal);
        $r = self::httpJson($url, 8);

        if (empty($r['ok'])) {
            return self::fail('gold-api', $r['error'] ?? 'Gold-API request failed.', $symbol);
        }

        $data = $r['data'];
        $price = $data['price'] ?? $data['ask'] ?? $data['bid'] ?? null;
        if (!is_numeric($price)) {
            return self::fail('gold-api', 'No numeric price in Gold-API response.', $symbol, $data);
        }

        return [
            'success' => true,
            'provider' => 'gold-api',
            'source' => 'gold_api_public_spot',
            'provider_symbol' => $metal,
            'price' => (float)$price,
            'bid' => isset($data['bid']) && is_numeric($data['bid']) ? (float)$data['bid'] : null,
            'ask' => isset($data['ask']) && is_numeric($data['ask']) ? (float)$data['ask'] : null,
            'quote_time' => gmdate('Y-m-d H:i:s'),
            'raw' => $data,
        ];
    }

    private static function twelveDataQuote(string $symbol): array
    {
        $key = self::apiKey('twelvedata');
        if ($key === '') {
            return self::fail('twelvedata', 'TwelveData key missing.', $symbol);
        }

        $providerSymbol = self::twelveDataSymbol($symbol);
        self::recordProviderUsage('twelvedata', 1, 'twelvedata_price', $symbol);

        $url = 'https://api.twelvedata.com/price?' . http_build_query([
            'symbol' => $providerSymbol,
            'apikey' => $key,
            'format' => 'JSON',
        ]);

        $r = self::httpJson($url, 8);
        if (empty($r['ok'])) {
            return self::fail('twelvedata', $r['error'] ?? 'TwelveData request failed.', $symbol);
        }

        $data = $r['data'];
        if (isset($data['code']) || (isset($data['status']) && strtolower((string)$data['status']) === 'error')) {
            return self::fail('twelvedata', (string)($data['message'] ?? 'TwelveData error.'), $symbol, $data);
        }

        if (!isset($data['price']) || !is_numeric($data['price'])) {
            return self::fail('twelvedata', 'No numeric price in TwelveData response.', $symbol, $data);
        }

        return [
            'success' => true,
            'provider' => 'twelvedata',
            'source' => 'twelvedata_price',
            'provider_symbol' => $providerSymbol,
            'price' => (float)$data['price'],
            'quote_time' => gmdate('Y-m-d H:i:s'),
            'raw' => $data,
        ];
    }

    private static function fmpQuote(string $symbol, string $assetType = ''): array
    {
        $assetType = strtoupper(trim($assetType));
        if ($assetType === 'METAL' && !self::settingEnabled('live_quote_fmp_metals_enabled', false)) {
            return self::fail('fmp', 'FMP metals/commodity quotes are disabled by default. Route XAUUSD/XAGUSD through GoldAPI.io, Gold-API public, TwelveData, Metals-API, MetalpriceAPI or Alpha Vantage. Set live_quote_fmp_metals_enabled=1 only if your FMP subscription supports metals/commodity quote endpoints.', $symbol);
        }

        $key = self::apiKey('fmp') ?: self::apiKey('financialmodelingprep');
        if ($key === '') {
            return self::fail('fmp', 'FMP key missing.', $symbol);
        }

        $candidates = self::fmpSymbolCandidates($symbol);
        $lastError = '';

        foreach ($candidates as $providerSymbol) {
            /*
             * FMP fix:
             * - Forex pairs such as EURUSD must use the current stable Forex Quote endpoint:
             *   /stable/quote?symbol=EURUSD
             * - Do not call /api/v3/* legacy endpoints.
             * - Do not call quote-short for forex. It is a stock short-quote endpoint and can
             *   lead to confusing legacy/unsupported endpoint errors on some accounts.
             */
            $urls = [
                'https://financialmodelingprep.com/stable/quote?' . http_build_query(['symbol' => $providerSymbol, 'apikey' => $key]),
            ];

            foreach ($urls as $url) {
                self::recordProviderUsage('fmp', 1, 'fmp_stable_quote', $symbol);
                $r = self::httpJson($url, 8);
                if (empty($r['ok'])) {
                    $lastError = $r['error'] ?? 'FMP HTTP failed.';
                    continue;
                }

                $data = $r['data'];
                if (isset($data['message'])) {
                    $lastError = (string)$data['message'];
                    if (self::providerLimitDetected($lastError, $data)) {
                        return self::fail('fmp', $lastError, $symbol, $data);
                    }
                    continue;
                }

                $row = null;
                if (isset($data[0]) && is_array($data[0])) {
                    $row = $data[0];
                } elseif (is_array($data)) {
                    $row = $data;
                }

                if (!$row) {
                    $lastError = 'FMP stable quote response empty.';
                    continue;
                }

                $price = $row['price'] ?? $row['bid'] ?? $row['ask'] ?? $row['c'] ?? null;
                if (is_numeric($price)) {
                    return [
                        'success' => true,
                        'provider' => 'fmp',
                        'source' => 'fmp_stable_quote',
                        'provider_symbol' => $providerSymbol,
                        'price' => (float)$price,
                        'quote_time' => gmdate('Y-m-d H:i:s'),
                        'raw' => $data,
                    ];
                }

                $lastError = 'No numeric price in FMP stable quote response.';
            }
        }

        return self::fail('fmp', $lastError ?: 'FMP stable quote failed.', $symbol);
    }

    private static function finnhubQuote(string $symbol, string $assetType): array
    {
        $key = self::apiKey('finnhub');
        $key = trim((string)preg_replace('/^Bearer\s+/i', '', $key));
        if ($key === '') {
            return self::fail('finnhub', 'Finnhub key missing.', $symbol);
        }

        $assetType = strtoupper(trim($assetType));

        /*
         * Finnhub's /quote endpoint is stock-style. For FX pairs such as
         * EURUSD, use /forex/rates?base=EUR and read quote[USD]. This avoids
         * repeated empty c=0 quote responses for OANDA/FXCM symbol guesses.
         */
        if ($assetType === 'FOREX' && preg_match('/^[A-Z]{6}$/', self::normalizeSymbol($symbol))) {
            [$base, $quote] = self::currencyPair($symbol);
            if ($base === '' || $quote === '') {
                return self::fail('finnhub', 'Could not parse forex pair for Finnhub forex/rates.', $symbol);
            }

            self::recordProviderUsage('finnhub', 1, 'finnhub_forex_rates', $symbol);
            $url = 'https://finnhub.io/api/v1/forex/rates?' . http_build_query([
                'base' => $base,
                'token' => $key,
            ]);

            $r = self::httpJson($url, 10);
            if (empty($r['ok'])) {
                return self::fail('finnhub', $r['error'] ?? 'Finnhub forex/rates request failed.', $symbol);
            }

            $data = $r['data'];
            if (isset($data['error'])) {
                $message = is_scalar($data['error']) ? (string)$data['error'] : json_encode($data['error']);
                return self::fail('finnhub', $message ?: 'Finnhub forex/rates error.', $symbol, $data);
            }

            $rate = $data['quote'][$quote] ?? $data['rates'][$quote] ?? null;
            if (is_numeric($rate) && (float)$rate > 0) {
                return [
                    'success' => true,
                    'provider' => 'finnhub',
                    'source' => 'finnhub_forex_rates',
                    'provider_symbol' => $base . '/' . $quote,
                    'price' => (float)$rate,
                    'bid' => null,
                    'ask' => null,
                    'quote_time' => gmdate('Y-m-d H:i:s'),
                    'raw' => $data,
                ];
            }

            return self::fail('finnhub', 'Finnhub forex/rates did not return a numeric ' . $quote . ' rate for base ' . $base . '.', $symbol, $data);
        }

        if ($assetType === 'METAL' && !self::settingEnabled('live_quote_finnhub_metals_enabled', false)) {
            return self::fail('finnhub', 'Finnhub metals quotes are disabled by default. Use GoldAPI.io, Gold-API public, TwelveData, Metals-API, MetalpriceAPI or Alpha Vantage for XAUUSD/XAGUSD.', $symbol);
        }

        $candidates = self::finnhubSymbolCandidates($symbol, $assetType);
        $lastError = '';

        foreach ($candidates as $providerSymbol) {
            self::recordProviderUsage('finnhub', 1, 'finnhub_quote', $symbol);
            $url = 'https://finnhub.io/api/v1/quote?' . http_build_query(['symbol' => $providerSymbol, 'token' => $key]);
            $r = self::httpJson($url, 8);
            if (empty($r['ok'])) {
                $lastError = $r['error'] ?? 'Finnhub request failed.';
                continue;
            }

            $data = $r['data'];
            if (isset($data['error'])) {
                $lastError = is_scalar($data['error']) ? (string)$data['error'] : json_encode($data['error']);
                continue;
            }
            $price = $data['c'] ?? null;
            if (is_numeric($price) && (float)$price > 0) {
                return [
                    'success' => true,
                    'provider' => 'finnhub',
                    'source' => 'finnhub_quote',
                    'provider_symbol' => $providerSymbol,
                    'price' => (float)$price,
                    'bid' => isset($data['b']) && is_numeric($data['b']) ? (float)$data['b'] : null,
                    'ask' => isset($data['a']) && is_numeric($data['a']) ? (float)$data['a'] : null,
                    'quote_time' => !empty($data['t']) && is_numeric($data['t']) ? gmdate('Y-m-d H:i:s', (int)$data['t']) : gmdate('Y-m-d H:i:s'),
                    'raw' => $data,
                ];
            }

            $lastError = 'No numeric current price in Finnhub quote response for ' . $providerSymbol . '.';
        }

        return self::fail('finnhub', $lastError ?: 'Finnhub quote failed.', $symbol);
    }

    private static function alphaVantageQuote(string $symbol): array
    {
        $key = self::apiKey('alphavantage') ?: self::apiKey('alpha_vantage');
        if ($key === '') {
            return self::fail('alphavantage', 'Alpha Vantage key missing.', $symbol);
        }

        [$from, $to] = self::currencyPair($symbol);
        if ($from === '' || $to === '') {
            return self::fail('alphavantage', 'Could not map symbol to currency pair.', $symbol);
        }

        self::recordProviderUsage('alphavantage', 1, 'currency_exchange_rate', $symbol);
        $url = 'https://www.alphavantage.co/query?' . http_build_query([
            'function' => 'CURRENCY_EXCHANGE_RATE',
            'from_currency' => $from,
            'to_currency' => $to,
            'apikey' => $key,
        ]);

        $r = self::httpJson($url, 10);
        if (empty($r['ok'])) {
            return self::fail('alphavantage', $r['error'] ?? 'Alpha Vantage request failed.', $symbol);
        }

        $data = $r['data'];
        if (isset($data['Note']) || isset($data['Information'])) {
            return self::fail('alphavantage', (string)($data['Note'] ?? $data['Information']), $symbol, $data);
        }

        $rate = $data['Realtime Currency Exchange Rate']['5. Exchange Rate'] ?? null;
        if (!is_numeric($rate)) {
            return self::fail('alphavantage', 'No exchange rate in Alpha Vantage response.', $symbol, $data);
        }

        return [
            'success' => true,
            'provider' => 'alphavantage',
            'source' => 'currency_exchange_rate',
            'provider_symbol' => $from . '/' . $to,
            'price' => (float)$rate,
            'quote_time' => gmdate('Y-m-d H:i:s'),
            'raw' => $data,
        ];
    }

    private static function metalsApiQuote(string $symbol): array
    {
        $key = self::apiKey('metalsapi') ?: self::apiKey('metals-api');
        if ($key === '') {
            return self::fail('metalsapi', 'Metals-API key missing.', $symbol);
        }

        $metal = self::metalCode($symbol);
        if ($metal === '') {
            return self::fail('metalsapi', 'Symbol is not a supported precious metal.', $symbol);
        }

        self::recordProviderUsage('metalsapi', 1, 'metalsapi_latest', $symbol);
        $url = 'https://api.metals-api.com/v1/latest?' . http_build_query([
            'access_key' => $key,
            'base' => 'USD',
            'symbols' => $metal,
        ]);

        $r = self::httpJson($url, 10);
        if (empty($r['ok'])) {
            return self::fail('metalsapi', $r['error'] ?? 'Metals-API request failed.', $symbol);
        }

        $data = $r['data'];
        $rate = $data['rates'][$metal] ?? null;
        if (!is_numeric($rate) || (float)$rate <= 0) {
            return self::fail('metalsapi', 'No usable metal rate returned.', $symbol, $data);
        }

        return [
            'success' => true,
            'provider' => 'metalsapi',
            'source' => 'latest_metals_rate',
            'provider_symbol' => $metal,
            'price' => 1 / (float)$rate,
            'quote_time' => gmdate('Y-m-d H:i:s'),
            'raw' => $data,
        ];
    }

    private static function metalPriceApiQuote(string $symbol): array
    {
        $key = self::apiKey('metalpriceapi') ?: self::apiKey('metalprice-api');
        if ($key === '') {
            return self::fail('metalpriceapi', 'MetalpriceAPI key missing.', $symbol);
        }

        $metal = self::metalCode($symbol);
        if ($metal === '') {
            return self::fail('metalpriceapi', 'Symbol is not a supported precious metal.', $symbol);
        }

        self::recordProviderUsage('metalpriceapi', 1, 'metalpriceapi_latest', $symbol);
        $url = 'https://api.metalpriceapi.com/v1/latest?' . http_build_query([
            'api_key' => $key,
            'base' => 'USD',
            'currencies' => $metal,
        ]);

        $r = self::httpJson($url, 10);
        if (empty($r['ok'])) {
            return self::fail('metalpriceapi', $r['error'] ?? 'MetalpriceAPI request failed.', $symbol);
        }

        $data = $r['data'];
        $rate = $data['rates'][$metal] ?? null;
        if (!is_numeric($rate) || (float)$rate <= 0) {
            return self::fail('metalpriceapi', 'No usable metal rate returned.', $symbol, $data);
        }

        return [
            'success' => true,
            'provider' => 'metalpriceapi',
            'source' => 'latest_metalpriceapi_rate',
            'provider_symbol' => $metal,
            'price' => 1 / (float)$rate,
            'quote_time' => gmdate('Y-m-d H:i:s'),
            'raw' => $data,
        ];
    }

    private static function providerOrder(string $assetType): array
    {
        $assetType = strtoupper($assetType);
        $key = 'live_quote_provider_order_' . strtolower($assetType);
        $configured = trim(self::setting($key, ''));

        if ($configured !== '') {
            return array_values(array_filter(array_map('trim', explode(',', $configured))));
        }

        if ($assetType === 'CRYPTO') {
            return ['binance'];
        }

        if ($assetType === 'METAL') {
            $order = ['goldapiio', 'gold-api', 'twelvedata', 'metalsapi', 'metalpriceapi', 'alphavantage'];
            if (self::settingEnabled('live_quote_fmp_metals_enabled', false)) {
                $order[] = 'fmp';
            }
            return $order;
        }

        if ($assetType === 'FOREX') {
            return ['fmp', 'twelvedata', 'finnhub', 'alphavantage'];
        }

        if (in_array($assetType, ['INDEX', 'INDICES', 'STOCK'], true)) {
            return ['finnhub', 'fmp', 'twelvedata', 'alphavantage'];
        }

        return ['fmp', 'twelvedata', 'finnhub', 'alphavantage'];
    }

    private static function providerQuoteEntries(string $provider, string $assetType, string $symbol): array
    {
        $provider = self::normalizeProvider($provider);

        if (in_array($provider, ['binance', 'gold-api', 'goldapi_free', 'goldapi_public'], true)) {
            return [[
                'provider' => $provider,
                'row' => [
                    'id' => 0,
                    'provider' => $provider,
                    'api_key' => '',
                    'is_active' => 1,
                    'supports_quote' => 1,
                    'daily_limit' => 0,
                    'used_today' => 0,
                ],
            ]];
        }

        if (class_exists('ApiCreditGuard') && method_exists('ApiCreditGuard', 'providerRows')) {
            $rows = ApiCreditGuard::providerRows($provider, 'quote', 1, true);
            if ($rows) {
                return array_map(static function ($row) use ($provider) {
                    return ['provider' => $provider, 'row' => $row];
                }, $rows);
            }
        }

        $key = self::apiKey($provider);
        if ($key !== '') {
            return [[
                'provider' => $provider,
                'row' => [
                    'id' => 0,
                    'provider' => $provider,
                    'api_key' => $key,
                    'is_active' => 1,
                    'supports_quote' => 1,
                ],
            ]];
        }

        return [[
            'provider' => $provider,
            'row' => ['id' => 0, 'provider' => $provider, 'api_key' => '', 'is_active' => 0],
        ]];
    }

    private static function providerAllowedForAsset(string $provider, string $assetType, string $symbol): bool
    {
        if ($assetType === 'CRYPTO') {
            return $provider === 'binance';
        }

        if (in_array($provider, ['goldapiio', 'goldapi', 'gold-api', 'goldapi_free', 'goldapi_public', 'metalsapi', 'metals-api', 'metalpriceapi', 'metalprice-api'], true)) {
            return $assetType === 'METAL';
        }

        if ($assetType === 'METAL' && $provider === 'fmp') {
            return self::settingEnabled('live_quote_fmp_metals_enabled', false);
        }

        if ($assetType === 'METAL' && $provider === 'finnhub') {
            return self::settingEnabled('live_quote_finnhub_metals_enabled', false);
        }

        return true;
    }

    private static function providerGuard(string $provider, array $providerRow = []): array
    {
        $provider = self::normalizeProvider($provider);

        if (in_array($provider, ['binance', 'gold-api', 'goldapi_free', 'goldapi_public'], true)) {
            return ['allowed' => true, 'reason' => 'Public/free quote provider.'];
        }

        if (class_exists('ApiCreditGuard')) {
            if ($providerRow && method_exists('ApiCreditGuard', 'shouldUseProviderRow')) {
                $guard = ApiCreditGuard::shouldUseProviderRow($providerRow, 'quote', 1);
                if (isset($guard['allowed'])) {
                    return $guard;
                }
            }

            $guard = ApiCreditGuard::shouldUseProvider($provider, 'quote', 1);
            if (isset($guard['allowed'])) {
                return $guard;
            }
        }

        if (self::apiKey($provider) === '') {
            return ['allowed' => false, 'reason' => strtoupper($provider) . ' API key missing.'];
        }

        return ['allowed' => true, 'reason' => 'Provider allowed.'];
    }

    private static function cachedQuote(string $symbol, int $ttl): array
    {
        $symbol = self::normalizeSymbol($symbol);
        $ttl = max(1, (int)$ttl);

        /*
         * IMPORTANT:
         * market_quotes.created_at is generated by MySQL CURRENT_TIMESTAMP.
         * On many cPanel servers MySQL runs in the server timezone, not UTC.
         * Comparing that column with UTC_TIMESTAMP() makes fresh rows look old
         * when the DB timezone is behind UTC, so the alert worker can call a
         * paid quote provider every minute. Use NOW() so the cache comparison
         * uses the same clock/timezone as created_at.
         */
        try {
            $stmt = db()->prepare("
                SELECT *
                FROM market_quotes
                WHERE symbol = ?
                  AND created_at >= DATE_SUB(NOW(), INTERVAL ? SECOND)
                ORDER BY created_at DESC, id DESC
                LIMIT 1
            ");
            $stmt->execute([$symbol, $ttl]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if (!$row) {
                return [];
            }

            return [
                'success' => true,
                'symbol' => $row['symbol'],
                'asset_type' => $row['asset_type'],
                'provider' => $row['provider'],
                'source' => 'market_quotes_cache',
                'original_source' => $row['source'],
                'provider_symbol' => $row['provider_symbol'],
                'price' => (float)$row['price'],
                'bid' => $row['bid'] !== null ? (float)$row['bid'] : null,
                'ask' => $row['ask'] !== null ? (float)$row['ask'] : null,
                'quote_time' => $row['quote_time'] ?: $row['created_at'],
                'timestamp' => $row['quote_time'] ?: $row['created_at'],
                'cached' => true,
                'cache_age_seconds' => self::quoteAgeSeconds((string)$row['created_at']),
            ];
        } catch (Throwable $e) {
            return [];
        }
    }

    private static function quoteAgeSeconds(string $createdAt): ?int
    {
        $createdAt = trim($createdAt);
        if ($createdAt === '') {
            return null;
        }

        try {
            $stmt = db()->prepare("SELECT TIMESTAMPDIFF(SECOND, ?, NOW())");
            $stmt->execute([$createdAt]);
            $age = $stmt->fetchColumn();
            return is_numeric($age) ? max(0, (int)$age) : null;
        } catch (Throwable $e) {
            return null;
        }
    }

    private static function recordQuote(string $symbol, string $assetType, array $quote): void
    {
        try {
            $symbol = self::normalizeSymbol($symbol);
            $provider = (string)($quote['provider'] ?? '');
            $source = (string)($quote['source'] ?? '');
            $minSeconds = max(0, min(1800, (int)self::setting('market_quotes_snapshot_min_seconds', '180')));

            /*
             * One quote snapshot per symbol inside the snapshot window is enough
             * for cache + audit purposes. Checking only provider/source allowed
             * the same symbol to be saved repeatedly when fallback providers or
             * source labels changed. This also supports all assets: forex, metals,
             * crypto, indices, stocks and other configured symbols.
             */
            if ($minSeconds > 0) {
                $check = db()->prepare("
                    SELECT id
                    FROM market_quotes
                    WHERE symbol = ?
                      AND created_at >= DATE_SUB(NOW(), INTERVAL ? SECOND)
                    ORDER BY created_at DESC, id DESC
                    LIMIT 1
                ");
                $check->execute([$symbol, $minSeconds]);
                if ($check->fetchColumn()) {
                    return;
                }
            }

            $stmt = db()->prepare("
                INSERT INTO market_quotes
                    (symbol, asset_type, provider, source, provider_symbol, price, bid, ask, quote_time, raw_json)
                VALUES
                    (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ");
            $stmt->execute([
                $symbol,
                $assetType,
                $provider,
                $source,
                (string)($quote['provider_symbol'] ?? ''),
                (float)$quote['price'],
                isset($quote['bid']) && is_numeric($quote['bid']) ? (float)$quote['bid'] : null,
                isset($quote['ask']) && is_numeric($quote['ask']) ? (float)$quote['ask'] : null,
                (string)($quote['quote_time'] ?? gmdate('Y-m-d H:i:s')),
                json_encode($quote['raw'] ?? $quote, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            ]);
        } catch (Throwable $e) {
        }
    }

    private static function ttlSeconds(string $assetType): int
    {
        $assetType = strtoupper($assetType);
        $defaults = [
            'CRYPTO' => 20,
            'METAL' => 120,
            'FOREX' => 120,
            'INDEX' => 180,
            'INDICES' => 180,
            'STOCK' => 180,
            'OTHER' => 180,
        ];

        $key = 'live_quote_cache_ttl_' . strtolower($assetType);
        $value = (int)self::setting($key, (string)($defaults[$assetType] ?? 30));

        return max(15, min(900, $value));
    }

    private static function assetType(string $symbol): string
    {
        if (class_exists('ApiCreditGuard')) {
            return ApiCreditGuard::guessAssetType($symbol);
        }

        $symbol = self::normalizeSymbol($symbol);
        if (preg_match('/^(BTC|ETH|SOL|BNB|XRP|ADA|DOGE|AVAX|LINK|LTC|BCH|DOT|MATIC|TRX|ATOM|NEAR|APT|OP|ARB)/', $symbol)) {
            return 'CRYPTO';
        }
        if (in_array(substr($symbol, 0, 3), ['XAU', 'XAG', 'XPT', 'XPD'], true)) {
            return 'METAL';
        }
        if (preg_match('/^[A-Z]{6}$/', $symbol)) {
            return 'FOREX';
        }
        if (preg_match('/^(NAS|US30|DJI|SPX|US100|US500|GER40|UK100|JP225)/', $symbol)) {
            return 'INDEX';
        }

        return 'OTHER';
    }

    private static function normalizeProvider(string $provider): string
    {
        $provider = strtolower(trim($provider));
        $map = [
            'financialmodelingprep' => 'fmp',
            'financial_modeling_prep' => 'fmp',
            'twelve_data' => 'twelvedata',
            'twelve-data' => 'twelvedata',
            'alpha-vantage' => 'alphavantage',
            'alpha_vantage' => 'alphavantage',
            'goldapi.io' => 'goldapiio',
            'goldapi' => 'goldapiio',
            'gold-api.com' => 'gold-api',
        ];

        return $map[$provider] ?? $provider;
    }

    private static function normalizeSymbol(string $symbol): string
    {
        return strtoupper(str_replace(['/', '-', '_', ' '], '', trim($symbol)));
    }

    private static function binanceSymbol(string $symbol): string
    {
        $symbol = self::normalizeSymbol($symbol);
        if (class_exists('ApiCreditGuard')) {
            return ApiCreditGuard::symbolProviderSymbol($symbol, [], 'binance');
        }
        if (preg_match('/USD$/', $symbol)) {
            return preg_replace('/USD$/', 'USDT', $symbol);
        }
        return $symbol;
    }

    private static function metalCode(string $symbol): string
    {
        $symbol = self::normalizeSymbol($symbol);
        if (strpos($symbol, 'XAU') === 0) return 'XAU';
        if (strpos($symbol, 'XAG') === 0) return 'XAG';
        if (strpos($symbol, 'XPT') === 0) return 'XPT';
        if (strpos($symbol, 'XPD') === 0) return 'XPD';
        return '';
    }

    private static function twelveDataSymbol(string $symbol): string
    {
        $symbol = self::normalizeSymbol($symbol);
        if (preg_match('/^[A-Z]{6}$/', $symbol)) {
            return substr($symbol, 0, 3) . '/' . substr($symbol, 3, 3);
        }
        if (substr($symbol, -3) === 'USD' && strlen($symbol) > 3) {
            return substr($symbol, 0, -3) . '/USD';
        }
        return $symbol;
    }

    private static function currencyPair(string $symbol): array
    {
        $symbol = self::normalizeSymbol($symbol);
        if (preg_match('/^[A-Z]{6}$/', $symbol)) {
            return [substr($symbol, 0, 3), substr($symbol, 3, 3)];
        }
        if (substr($symbol, -3) === 'USD' && strlen($symbol) > 3) {
            return [substr($symbol, 0, -3), 'USD'];
        }
        return ['', ''];
    }

    private static function fmpSymbolCandidates(string $symbol): array
    {
        $symbol = self::normalizeSymbol($symbol);
        $known = [
            'XAUUSD' => ['ZGUSD', 'XAUUSD', 'GCUSD'],
            'XAGUSD' => ['ZIUSD', 'SIUSD', 'XAGUSD', 'XAGUSD.FOREX'],
            'XPTUSD' => ['PLUSD', 'XPTUSD'],
            'XPDUSD' => ['PAUSD', 'XPDUSD'],
        ];

        return array_values(array_unique(array_merge($known[$symbol] ?? [], [$symbol])));
    }

    private static function finnhubSymbolCandidates(string $symbol, string $assetType): array
    {
        $symbol = self::normalizeSymbol($symbol);

        if ($assetType === 'FOREX' && preg_match('/^[A-Z]{6}$/', $symbol)) {
            $base = substr($symbol, 0, 3);
            $quote = substr($symbol, 3, 3);
            return [
                'OANDA:' . $base . '_' . $quote,
                'FXCM:' . $base . '/' . $quote,
                'FOREXCOM:' . $symbol,
                $base . '/' . $quote,
                $symbol,
            ];
        }

        if ($assetType === 'METAL') {
            $metal = substr($symbol, 0, 3);
            return [
                'OANDA:' . $metal . '_USD',
                'FOREXCOM:' . $metal . 'USD',
                $metal . '/USD',
                $symbol,
            ];
        }

        return [$symbol];
    }

    private static function apiKey(string $provider): string
    {
        $provider = self::normalizeProvider($provider);

        if (!empty(self::$activeProviderRows[$provider]['api_key'])) {
            return trim((string)self::$activeProviderRows[$provider]['api_key']);
        }

        if (class_exists('ApiCreditGuard')) {
            if (method_exists('ApiCreditGuard', 'providerRow')) {
                $row = ApiCreditGuard::providerRow($provider, 'quote', 1);
                if ($row) {
                    self::$activeProviderRows[$provider] = $row;
                    $key = trim((string)($row['api_key'] ?? ''));
                    if ($key !== '') {
                        return $key;
                    }
                }
            }

            $key = trim((string)ApiCreditGuard::getApiKey($provider));
            if ($key !== '') {
                return $key;
            }
        }

        try {
            $stmt = db()->prepare("
                SELECT api_key
                FROM api_settings
                WHERE LOWER(provider) = LOWER(?)
                  AND COALESCE(is_active, 1) = 1
                ORDER BY COALESCE(priority_order, 999999), id
                LIMIT 1
            ");
            $stmt->execute([$provider]);
            return trim((string)$stmt->fetchColumn());
        } catch (Throwable $e) {
            return '';
        }
    }

    private static function setting(string $key, string $default = ''): string
    {
        try {
            $stmt = db()->prepare("SELECT setting_value FROM system_settings WHERE setting_key = ? LIMIT 1");
            $stmt->execute([$key]);
            $value = $stmt->fetchColumn();
            return $value !== false ? (string)$value : $default;
        } catch (Throwable $e) {
            return $default;
        }
    }

    private static function settingEnabled(string $key, bool $default = false): bool
    {
        $value = strtolower(trim(self::setting($key, $default ? '1' : '0')));
        return in_array($value, ['1', 'true', 'yes', 'on', 'enabled'], true);
    }

    private static function httpJson(string $url, int $timeout = 10, array $headers = []): array
    {
        $headers = array_values(array_unique(array_merge(['Accept: application/json', 'User-Agent: ITA-LiveQuoteService/1.1'], $headers)));
        $httpCode = 0;

        if (function_exists('curl_init')) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_CONNECTTIMEOUT => min(5, $timeout),
                CURLOPT_TIMEOUT => $timeout,
                CURLOPT_SSL_VERIFYPEER => true,
                CURLOPT_HTTPHEADER => $headers,
            ]);
            $body = curl_exec($ch);
            $err = curl_error($ch);
            $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($body === false || trim((string)$body) === '') {
                return ['ok' => false, 'error' => $err ?: ('Empty HTTP response' . ($httpCode ? ' (HTTP ' . $httpCode . ')' : '') . '.')];
            }
        } else {
            $context = stream_context_create([
                'http' => [
                    'header' => implode("\r\n", $headers) . "\r\n",
                    'timeout' => $timeout,
                    'ignore_errors' => true,
                ],
            ]);
            $body = @file_get_contents($url, false, $context);
            if (isset($http_response_header) && is_array($http_response_header)) {
                foreach ($http_response_header as $headerLine) {
                    if (preg_match('/^HTTP\/\S+\s+(\d+)/', $headerLine, $m)) {
                        $httpCode = (int)$m[1];
                        break;
                    }
                }
            }
            if ($body === false || trim((string)$body) === '') {
                return ['ok' => false, 'error' => 'Empty HTTP response' . ($httpCode ? ' (HTTP ' . $httpCode . ')' : '') . '.'];
            }
        }

        $data = json_decode((string)$body, true);
        if (!is_array($data)) {
            return ['ok' => false, 'error' => ($httpCode ? 'HTTP ' . $httpCode . ': ' : '') . 'Invalid JSON response. Preview: ' . substr(trim((string)$body), 0, 220), 'raw' => substr((string)$body, 0, 500)];
        }

        if ($httpCode >= 400) {
            $message = $data['message'] ?? $data['error'] ?? ('HTTP ' . $httpCode . ' response.');
            return ['ok' => false, 'error' => is_scalar($message) ? (string)$message : json_encode($message), 'raw' => $data, 'http_code' => $httpCode];
        }

        return ['ok' => true, 'data' => $data, 'http_code' => $httpCode];
    }

    private static function fail(string $provider, string $error, string $symbol = '', $raw = null): array
    {
        return [
            'success' => false,
            'symbol' => $symbol,
            'provider' => $provider,
            'source' => 'failed',
            'price' => null,
            'timestamp' => gmdate('Y-m-d H:i:s'),
            'error' => $error,
            'raw' => $raw,
        ];
    }

    private static function providerLimitDetected(string $error, $raw = null): bool
    {
        $text = strtolower($error . ' ' . (is_scalar($raw) ? (string)$raw : json_encode($raw)));
        return strpos($text, 'limit reach') !== false
            || strpos($text, 'rate limit') !== false
            || strpos($text, 'daily limit') !== false
            || strpos($text, 'api limit') !== false
            || strpos($text, 'upgrade your plan') !== false
            || strpos($text, 'please upgrade') !== false;
    }

    private static function recordProviderUsage(string $provider, int $credits = 1, string $endpoint = 'live_quote_attempt', ?string $symbol = null, ?string $timeframe = null, array $providerRow = []): void
    {
        if (!class_exists('ApiCreditGuard')) {
            return;
        }

        $provider = self::normalizeProvider($provider);
        $row = $providerRow ?: (self::$activeProviderRows[$provider] ?? []);
        if ($row && method_exists('ApiCreditGuard', 'recordUsageForRow')) {
            ApiCreditGuard::recordUsageForRow($row, $credits, $endpoint, $symbol, $timeframe);
            return;
        }

        ApiCreditGuard::recordUsage($provider, $credits, $endpoint, $symbol, $timeframe);
    }

    private static function recordProviderSuccess(string $provider, array $providerRow = [], string $endpoint = 'live_quote', ?string $symbol = null, ?string $timeframe = null, int $responseMs = 0): void
    {
        if (!class_exists('ApiCreditGuard')) {
            return;
        }

        $provider = self::normalizeProvider($provider);
        $row = $providerRow ?: (self::$activeProviderRows[$provider] ?? []);
        if ($row && method_exists('ApiCreditGuard', 'recordSuccessForRow')) {
            ApiCreditGuard::recordSuccessForRow($row, $endpoint, $symbol, $timeframe, $responseMs);
            return;
        }

        ApiCreditGuard::recordSuccess($provider, $endpoint, $symbol, $timeframe, $responseMs);
    }

    private static function recordProviderFailure(string $provider, string $error, array $providerRow = [], string $endpoint = 'live_quote', ?string $symbol = null, ?string $timeframe = null, int $responseMs = 0): void
    {
        if (!class_exists('ApiCreditGuard') || in_array(self::normalizeProvider($provider), ['binance', 'gold-api', 'goldapi_free', 'goldapi_public'], true)) {
            return;
        }

        $provider = self::normalizeProvider($provider);
        $row = $providerRow ?: (self::$activeProviderRows[$provider] ?? []);
        if ($row && method_exists('ApiCreditGuard', 'recordFailureForRow')) {
            ApiCreditGuard::recordFailureForRow($row, $error, $endpoint, $symbol, $timeframe, $responseMs);
            return;
        }

        ApiCreditGuard::recordFailure($provider, $error, $endpoint, $symbol, $timeframe, $responseMs);
    }

    private static function recordProviderLimit(string $provider, string $error, array $providerRow = [], string $endpoint = 'live_quote', ?string $symbol = null, ?string $timeframe = null, int $responseMs = 0): void
    {
        if (class_exists('ApiCreditGuard') && method_exists('ApiCreditGuard', 'recordRateLimitedForRow')) {
            $provider = self::normalizeProvider($provider);
            $row = $providerRow ?: (self::$activeProviderRows[$provider] ?? []);
            if ($row) {
                ApiCreditGuard::recordRateLimitedForRow($row, $error, 60, $endpoint, $symbol, $timeframe, $responseMs);
                return;
            }
        }

        self::recordProviderFailure($provider, $error, $providerRow, $endpoint, $symbol, $timeframe, $responseMs);
    }
}
