<?php

class SmcAnalyzer
{
    public static function detectSwings(array $candles): array
    {
        $swings = [];
        $count = count($candles);

        if ($count < 10) {
            return self::fallbackSwings($candles);
        }

        for ($i = 2; $i < $count - 2; $i++) {
            $high = (float)$candles[$i]['high_price'];
            $low = (float)$candles[$i]['low_price'];

            if (
                $high >= (float)$candles[$i - 1]['high_price'] &&
                $high >= (float)$candles[$i - 2]['high_price'] &&
                $high >= (float)$candles[$i + 1]['high_price'] &&
                $high >= (float)$candles[$i + 2]['high_price']
            ) {
                $swings[] = [
                    'type' => 'HIGH',
                    'price' => $high,
                    'time' => $candles[$i]['open_time'],
                ];
            }

            if (
                $low <= (float)$candles[$i - 1]['low_price'] &&
                $low <= (float)$candles[$i - 2]['low_price'] &&
                $low <= (float)$candles[$i + 1]['low_price'] &&
                $low <= (float)$candles[$i + 2]['low_price']
            ) {
                $swings[] = [
                    'type' => 'LOW',
                    'price' => $low,
                    'time' => $candles[$i]['open_time'],
                ];
            }
        }

        if (count($swings) < 2) {
            return self::fallbackSwings($candles);
        }

        return self::cleanSwings($swings);
    }

    private static function fallbackSwings(array $candles): array
    {
        if (empty($candles)) {
            return [];
        }

        $recent = array_slice($candles, -80);
        $high = null;
        $low = null;

        foreach ($recent as $candle) {
            if ($high === null || (float)$candle['high_price'] > (float)$high['price']) {
                $high = [
                    'type' => 'HIGH',
                    'price' => (float)$candle['high_price'],
                    'time' => $candle['open_time'],
                ];
            }

            if ($low === null || (float)$candle['low_price'] < (float)$low['price']) {
                $low = [
                    'type' => 'LOW',
                    'price' => (float)$candle['low_price'],
                    'time' => $candle['open_time'],
                ];
            }
        }

        $result = array_values(array_filter([$low, $high]));

        usort($result, function ($a, $b) {
            return strtotime($a['time']) <=> strtotime($b['time']);
        });

        return $result;
    }

    private static function cleanSwings(array $swings): array
    {
        $cleaned = [];

        foreach ($swings as $swing) {
            if (empty($cleaned)) {
                $cleaned[] = $swing;
                continue;
            }

            $last = end($cleaned);

            if ($last['type'] === $swing['type']) {
                if ($swing['type'] === 'HIGH' && (float)$swing['price'] > (float)$last['price']) {
                    $cleaned[count($cleaned) - 1] = $swing;
                }

                if ($swing['type'] === 'LOW' && (float)$swing['price'] < (float)$last['price']) {
                    $cleaned[count($cleaned) - 1] = $swing;
                }
            } else {
                $cleaned[] = $swing;
            }
        }

        return $cleaned;
    }

    public static function detectStructureEvents(array $swings): array
    {
        $events = [];

        if (count($swings) < 2) {
            return $events;
        }

        $lastHigh = null;
        $lastLow = null;
        $bias = 'NEUTRAL';

        foreach ($swings as $swing) {
            if ($swing['type'] === 'HIGH') {
                if ($lastHigh && (float)$swing['price'] > (float)$lastHigh['price']) {
                    $events[] = [
                        'type' => $bias === 'BEARISH' ? 'CHoCH' : 'BOS',
                        'direction' => 'BULLISH',
                        'break_price' => (float)$lastHigh['price'],
                        'swing_price' => (float)$swing['price'],
                        'time' => $swing['time'],
                    ];

                    $bias = 'BULLISH';
                }

                $lastHigh = $swing;
            }

            if ($swing['type'] === 'LOW') {
                if ($lastLow && (float)$swing['price'] < (float)$lastLow['price']) {
                    $events[] = [
                        'type' => $bias === 'BULLISH' ? 'CHoCH' : 'BOS',
                        'direction' => 'BEARISH',
                        'break_price' => (float)$lastLow['price'],
                        'swing_price' => (float)$swing['price'],
                        'time' => $swing['time'],
                    ];

                    $bias = 'BEARISH';
                }

                $lastLow = $swing;
            }
        }

        return $events;
    }

    public static function getBias(array $events): string
    {
        if (!empty($events)) {
            $last = end($events);
            $direction = strtoupper((string)($last['direction'] ?? ''));

            if ($direction === 'BULLISH') {
                return 'BUY';
            }

            if ($direction === 'BEARISH') {
                return 'SELL';
            }
        }

        return 'WAIT';
    }

    public static function getLiquidityTargets(array $swings): array
    {
        $highs = array_values(array_filter($swings, fn($s) => ($s['type'] ?? '') === 'HIGH'));
        $lows = array_values(array_filter($swings, fn($s) => ($s['type'] ?? '') === 'LOW'));

        return [
            'buy_side' => !empty($highs) ? (float)end($highs)['price'] : null,
            'sell_side' => !empty($lows) ? (float)end($lows)['price'] : null,
        ];
    }

    public static function detectOrderBlocks(array $candles, array $events): array
    {
        if (class_exists('OrderBlockService') && method_exists('OrderBlockService', 'detect')) {
            try {
                $blocks = OrderBlockService::detect($candles, $events);
                if (!empty($blocks)) {
                    return $blocks;
                }
            } catch (Throwable $e) {
                // Fall back to internal detection.
            }
        }

        $blocks = [];

        if (count($candles) < 10) {
            return [];
        }

        foreach ($events as $event) {
            $index = self::findCandleIndexByTime($candles, $event['time']);
            if ($index === null) {
                continue;
            }

            if (($event['direction'] ?? '') === 'BULLISH') {
                $ob = self::findLastBearishCandleBefore($candles, $index);
                if ($ob) {
                    $blocks[] = self::makeOrderBlock($ob, 'BULLISH_OB', 'BUY', $event, $candles);
                }
            }

            if (($event['direction'] ?? '') === 'BEARISH') {
                $ob = self::findLastBullishCandleBefore($candles, $index);
                if ($ob) {
                    $blocks[] = self::makeOrderBlock($ob, 'BEARISH_OB', 'SELL', $event, $candles);
                }
            }
        }

        if (empty($blocks)) {
            $blocks = self::fallbackOrderBlocks($candles);
        }

        return self::removeDuplicateOrderBlocks($blocks);
    }

    public static function getActiveOrderBlock(array $orderBlocks, string $bias): ?array
    {
        if (class_exists('OrderBlockService') && method_exists('OrderBlockService', 'getActiveOrderBlock')) {
            try {
                $block = OrderBlockService::getActiveOrderBlock($orderBlocks, $bias);
                if ($block) {
                    return $block;
                }
            } catch (Throwable $e) {
                // Fall back.
            }
        }

        if (empty($orderBlocks)) {
            return null;
        }

        $filtered = array_values(array_filter($orderBlocks, function ($ob) use ($bias) {
            if (($ob['mitigated'] ?? false) === true) {
                return false;
            }

            if ($bias === 'WAIT') {
                return true;
            }

            return ($ob['direction'] ?? '') === $bias;
        }));

        if (empty($filtered)) {
            $filtered = $orderBlocks;
        }

        usort($filtered, function ($a, $b) {
            $scoreA = (int)($a['quality'] ?? 1);
            $scoreB = (int)($b['quality'] ?? 1);

            if ($scoreA === $scoreB) {
                return strtotime($b['time'] ?? 'now') <=> strtotime($a['time'] ?? 'now');
            }

            return $scoreB <=> $scoreA;
        });

        return $filtered[0] ?? null;
    }

    private static function fallbackOrderBlocks(array $candles): array
    {
        $blocks = [];
        $recent = array_slice($candles, -50);

        for ($i = 1; $i < count($recent); $i++) {
            $prev = $recent[$i - 1];
            $curr = $recent[$i];

            $prevOpen = (float)$prev['open_price'];
            $prevClose = (float)$prev['close_price'];
            $prevHigh = (float)$prev['high_price'];
            $prevLow = (float)$prev['low_price'];

            $currOpen = (float)$curr['open_price'];
            $currClose = (float)$curr['close_price'];
            $currHigh = (float)$curr['high_price'];
            $currLow = (float)$curr['low_price'];
            $currRange = max($currHigh - $currLow, 0.00000001);
            $currBody = abs($currClose - $currOpen);
            $hasDisplacement = $currBody >= ($currRange * 0.45);

            if ($prevClose < $prevOpen && $currClose > $currOpen && $hasDisplacement) {
                $blocks[] = [
                    'type' => 'BULLISH_OB',
                    'direction' => 'BUY',
                    'time' => $prev['open_time'],
                    'zone_low' => $prevLow,
                    'zone_high' => $prevHigh,
                    'midpoint' => ($prevLow + $prevHigh) / 2,
                    'quality' => 2,
                    'mitigated' => false,
                    'linked_event' => 'Fallback bullish displacement',
                    'event_time' => $curr['open_time'],
                    'narrative' => 'Fallback bullish institutional order block.',
                ];
            }

            if ($prevClose > $prevOpen && $currClose < $currOpen && $hasDisplacement) {
                $blocks[] = [
                    'type' => 'BEARISH_OB',
                    'direction' => 'SELL',
                    'time' => $prev['open_time'],
                    'zone_low' => $prevLow,
                    'zone_high' => $prevHigh,
                    'midpoint' => ($prevLow + $prevHigh) / 2,
                    'quality' => 2,
                    'mitigated' => false,
                    'linked_event' => 'Fallback bearish displacement',
                    'event_time' => $curr['open_time'],
                    'narrative' => 'Fallback bearish institutional order block.',
                ];
            }
        }

        return $blocks;
    }

    private static function makeOrderBlock(array $candle, string $type, string $direction, array $event, array $candles = []): array
    {
        $zoneLow = (float)$candle['low_price'];
        $zoneHigh = (float)$candle['high_price'];
        $body = abs((float)$candle['close_price'] - (float)$candle['open_price']);
        $range = max($zoneHigh - $zoneLow, 0.00000001);
        $quality = 1;

        if ($body >= ($range * 0.5)) {
            $quality += 1;
        }

        if (($event['type'] ?? '') === 'BOS') {
            $quality += 1;
        }

        if (($event['type'] ?? '') === 'CHoCH') {
            $quality += 2;
        }

        return [
            'type' => $type,
            'direction' => $direction,
            'time' => $candle['open_time'],
            'zone_low' => $zoneLow,
            'zone_high' => $zoneHigh,
            'midpoint' => ($zoneLow + $zoneHigh) / 2,
            'body_size' => $body,
            'range_size' => $range,
            'quality' => min(5, $quality),
            'mitigated' => false,
            'linked_event' => ($event['type'] ?? '') . ' ' . ($event['direction'] ?? ''),
            'event_time' => $event['time'] ?? null,
            'narrative' => $direction === 'BUY'
                ? 'Bullish institutional order block.'
                : 'Bearish institutional order block.',
        ];
    }

    public static function detectFVG(array $candles): array
    {
        if (class_exists('FVGService') && method_exists('FVGService', 'detect')) {
            try {
                return FVGService::detect($candles);
            } catch (Throwable $e) {
                // Fall back.
            }
        }

        $zones = [];

        if (count($candles) < 3) {
            return $zones;
        }

        for ($i = 0; $i < count($candles) - 2; $i++) {
            $c1 = $candles[$i];
            $c2 = $candles[$i + 1];
            $c3 = $candles[$i + 2];

            $c1High = (float)$c1['high_price'];
            $c1Low = (float)$c1['low_price'];
            $c2Open = (float)$c2['open_price'];
            $c2Close = (float)$c2['close_price'];
            $c3High = (float)$c3['high_price'];
            $c3Low = (float)$c3['low_price'];
            $impulse = abs($c2Close - $c2Open);

            if ($c1High < $c3Low) {
                $gapSize = $c3Low - $c1High;
                $zones[] = [
                    'type' => 'BULLISH_FVG',
                    'direction' => 'BUY',
                    'low' => $c1High,
                    'high' => $c3Low,
                    'midpoint' => ($c1High + $c3Low) / 2,
                    'gap_size' => $gapSize,
                    'impulse_strength' => $impulse,
                    'quality' => self::classifyFvgQuality($gapSize, $impulse),
                    'filled' => self::isFvgFilled($candles, $i + 3, $c1High, $c3Low, 'BUY'),
                    'time' => $c2['open_time'],
                    'narrative' => 'Bullish imbalance created by displacement.',
                ];
            }

            if ($c1Low > $c3High) {
                $gapSize = $c1Low - $c3High;
                $zones[] = [
                    'type' => 'BEARISH_FVG',
                    'direction' => 'SELL',
                    'low' => $c3High,
                    'high' => $c1Low,
                    'midpoint' => ($c3High + $c1Low) / 2,
                    'gap_size' => $gapSize,
                    'impulse_strength' => $impulse,
                    'quality' => self::classifyFvgQuality($gapSize, $impulse),
                    'filled' => self::isFvgFilled($candles, $i + 3, $c3High, $c1Low, 'SELL'),
                    'time' => $c2['open_time'],
                    'narrative' => 'Bearish imbalance created by displacement.',
                ];
            }
        }

        return $zones;
    }

    public static function getActiveFVG(array $fvgZones, string $bias): ?array
    {
        if (class_exists('FVGService') && method_exists('FVGService', 'getActiveFVG')) {
            try {
                $fvg = FVGService::getActiveFVG($fvgZones, $bias);
                if ($fvg) {
                    return $fvg;
                }
            } catch (Throwable $e) {
                // Fall back.
            }
        }

        if (empty($fvgZones)) {
            return null;
        }

        $filtered = array_values(array_filter($fvgZones, function ($fvg) use ($bias) {
            if (($fvg['filled'] ?? false) === true) {
                return false;
            }

            if ($bias === 'WAIT') {
                return true;
            }

            return ($fvg['direction'] ?? '') === $bias;
        }));

        if (empty($filtered)) {
            $filtered = array_values(array_filter($fvgZones, fn($fvg) => $bias === 'WAIT' || ($fvg['direction'] ?? '') === $bias));
        }

        if (empty($filtered)) {
            return null;
        }

        usort($filtered, function ($a, $b) {
            $scoreA = (int)($a['quality'] ?? 1);
            $scoreB = (int)($b['quality'] ?? 1);

            if ($scoreA === $scoreB) {
                return strtotime($b['time'] ?? 'now') <=> strtotime($a['time'] ?? 'now');
            }

            return $scoreB <=> $scoreA;
        });

        return $filtered[0] ?? null;
    }

    public static function buildTradePlan(
        array $candles,
        string $bias,
        array $liquidity,
        ?array $activeOrderBlock,
        ?array $activeFVG,
        array $symbol
    ): array {
        if (empty($candles)) {
            return [
                'status' => 'NO DATA',
                'display_status' => 'No Data',
                'message' => 'No candle data available.',
                'direction' => 'WAIT',
            ];
        }

        $symbolCode = $symbol['symbol'] ?? '';
        $decimals = isset($symbol['decimals']) ? (int)$symbol['decimals'] : ($symbolCode === 'XAUUSD' ? 2 : 5);
        $lastCandle = end($candles);
        $currentPrice = (float)$lastCandle['close_price'];

        if ($bias === 'WAIT') {
            $bias = self::inferBiasFromCandles($candles);
        }

        if (!$activeOrderBlock) {
            $fallbackBlocks = self::fallbackOrderBlocks($candles);
            $activeOrderBlock = self::getActiveOrderBlock($fallbackBlocks, $bias);
        }

        if (!$activeOrderBlock) {
            return [
                'status' => 'STANDBY',
                'display_status' => 'No Institutional Zone',
                'message' => 'No reliable institutional order block has formed yet.',
                'direction' => $bias,
                'current_price' => $currentPrice,
            ];
        }

        $entryLow = (float)$activeOrderBlock['zone_low'];
        $entryHigh = (float)$activeOrderBlock['zone_high'];

        if ($entryLow > $entryHigh) {
            [$entryLow, $entryHigh] = [$entryHigh, $entryLow];
        }

        $entrySource = $activeOrderBlock['linked_event'] ?? 'Order Block';
        $hasFvgConfluence = false;

        if ($activeFVG && ($activeFVG['direction'] ?? '') === $bias) {
            $overlapLow = max($entryLow, (float)$activeFVG['low']);
            $overlapHigh = min($entryHigh, (float)$activeFVG['high']);

            if ($overlapLow <= $overlapHigh) {
                $entryLow = $overlapLow;
                $entryHigh = $overlapHigh;
                $entrySource = 'Order Block + FVG Confluence';
                $hasFvgConfluence = true;
            }
        }

        $insideZone = ($insideZone);
        $entryPrice = $insideZone
            ? $currentPrice
            : ($bias === 'BUY'
                ? ($currentPrice > $entryHigh ? $entryHigh : $entryLow)
                : ($currentPrice < $entryLow ? $entryLow : $entryHigh));

        $baseStop = self::buildInstitutionalStopLoss($bias, $entryLow, $entryHigh, $candles, $symbolCode, $decimals);

        if ($bias === 'BUY') {
            $stopLoss = min($baseStop, $entryLow, $entryPrice - self::minimumStopDistance($symbolCode));
        } else {
            $stopLoss = max($baseStop, $entryHigh, $entryPrice + self::minimumStopDistance($symbolCode));
        }

        $risk = max(abs($entryPrice - $stopLoss), self::minimumStopDistance($symbolCode));

        if ($bias === 'BUY') {
            $tp1 = $entryPrice + ($risk * 1.5);
            $tp2 = $entryPrice + ($risk * 2.5);
            $tp3 = $entryPrice + ($risk * 4.0);
            $takeProfit = $tp2;
            $primaryLiquidity = $liquidity['buy_side'] ?? null;

            if ($insideZone) {
                $status = 'EXECUTION_READY';
                $displayStatus = 'Execution Ready';
                $message = 'Price is inside the institutional BUY execution zone.';
            } elseif ($currentPrice > $entryHigh) {
                $status = 'PULLBACK_NEEDED';
                $displayStatus = 'Pullback Needed';
                $message = 'Price is above the BUY zone. Wait for mitigation/pullback into the zone.';
            } elseif ($currentPrice < $stopLoss) {
                $status = 'INVALID';
                $displayStatus = 'Invalidated';
                $message = 'Price has moved below the institutional invalidation level.';
            } else {
                $status = 'MONITOR';
                $displayStatus = 'Monitor';
                $message = 'Price is approaching the BUY execution zone.';
            }
        } else {
            $tp1 = $entryPrice - ($risk * 1.5);
            $tp2 = $entryPrice - ($risk * 2.5);
            $tp3 = $entryPrice - ($risk * 4.0);
            $takeProfit = $tp2;
            $primaryLiquidity = $liquidity['sell_side'] ?? null;

            if ($insideZone) {
                $status = 'EXECUTION_READY';
                $displayStatus = 'Execution Ready';
                $message = 'Price is inside the institutional SELL execution zone.';
            } elseif ($currentPrice < $entryLow) {
                $status = 'PULLBACK_NEEDED';
                $displayStatus = 'Pullback Needed';
                $message = 'Price is below the SELL zone. Wait for mitigation/pullback into the zone.';
            } elseif ($currentPrice > $stopLoss) {
                $status = 'INVALID';
                $displayStatus = 'Invalidated';
                $message = 'Price has moved above the institutional invalidation level.';
            } else {
                $status = 'MONITOR';
                $displayStatus = 'Monitor';
                $message = 'Price is approaching the SELL execution zone.';
            }
        }

        $tp1 = round($tp1, $decimals);
        $tp2 = round($tp2, $decimals);
        $tp3 = round($tp3, $decimals);
        $takeProfit = round($takeProfit, $decimals);
        $entryLow = round($entryLow, $decimals);
        $entryHigh = round($entryHigh, $decimals);
        $entryPrice = round($entryPrice, $decimals);
        $stopLoss = round($stopLoss, $decimals);
        $reward = abs($takeProfit - $entryPrice);
        $rr = $risk > 0 ? round($reward / $risk, 2) : 0;

        return [
            'status' => $status,
            'display_status' => $displayStatus,
            'message' => $message,
            'direction' => $bias,
            'current_price' => $currentPrice,
            'entry_low' => $entryLow,
            'entry_high' => $entryHigh,
            'entry_price' => $entryPrice,
            'stop_loss' => $stopLoss,
            'take_profit' => $takeProfit,
            'tp1' => $tp1,
            'tp2' => $tp2,
            'tp3' => $tp3,
            'risk' => $risk,
            'reward' => $reward,
            'rr' => $rr,
            'entry_source' => $entrySource,
            'has_fvg_confluence' => $hasFvgConfluence,
            'primary_liquidity_target' => $primaryLiquidity,
            'order_block_quality' => $activeOrderBlock['quality'] ?? null,
            'fvg_quality' => $activeFVG['quality'] ?? null,
        ];
    }

    public static function enhanceTradePlan(array $plan): array
    {
        /*
        |--------------------------------------------------------------------------
        | Balanced SMC Confluence Scoring
        |--------------------------------------------------------------------------
        | Technical structure must remain the primary driver.
        | OB, FVG, mitigation, RR and execution location are additive.
        | Missing one confirmation should reduce confidence, not kill the setup.
        |--------------------------------------------------------------------------
        */

        if (!isset($plan['entry_price'], $plan['stop_loss'], $plan['tp1'], $plan['tp2'], $plan['tp3'])) {
            $plan['score'] = 0;
            $plan['quality'] = 'LOW';
            $plan['confidence'] = 'VERY LOW';
            $plan['execution_mode'] = $plan['execution_mode'] ?? 'NO_SETUP';

            return $plan;
        }

        $score = 0.0;
        $reasons = [];

        $status = strtoupper((string)($plan['status'] ?? ''));
        $entrySource = strtoupper((string)($plan['entry_source'] ?? ''));
        $rr = (float)($plan['rr'] ?? 0);
        $obQuality = (int)($plan['order_block_quality'] ?? 0);
        $fvgQuality = (int)($plan['fvg_quality'] ?? 0);

        /*
        |--------------------------------------------------------------------------
        | 1. Base Institutional Zone
        |--------------------------------------------------------------------------
        */

        if (!empty($plan['entry_source'])) {
            $score += 2.0;
            $reasons[] = 'Institutional entry zone identified';
        }

        if (str_contains($entrySource, 'ORDER BLOCK')) {
            $score += 1.0;
            $reasons[] = 'Order block foundation';
        }

        if (str_contains($entrySource, 'FVG')) {
            $score += 1.0;
            $reasons[] = 'Fair value gap involvement';
        }

        /*
        |--------------------------------------------------------------------------
        | 2. FVG Confluence
        |--------------------------------------------------------------------------
        */

        if (!empty($plan['has_fvg_confluence'])) {
            $score += 1.5;
            $reasons[] = 'OB + FVG confluence';
        }

        if ($fvgQuality >= 4) {
            $score += 0.75;
            $reasons[] = 'High-quality FVG';
        } elseif ($fvgQuality >= 2) {
            $score += 0.35;
            $reasons[] = 'Moderate FVG quality';
        }

        /*
        |--------------------------------------------------------------------------
        | 3. Order Block Quality
        |--------------------------------------------------------------------------
        */

        if ($obQuality >= 4) {
            $score += 1.0;
            $reasons[] = 'High-quality order block';
        } elseif ($obQuality >= 2) {
            $score += 0.5;
            $reasons[] = 'Usable order block quality';
        }

        /*
        |--------------------------------------------------------------------------
        | 4. Execution Readiness
        |--------------------------------------------------------------------------
        */

        if (in_array($status, ['EXECUTION_READY', 'READY'], true)) {
            $score += 1.5;
            $reasons[] = 'Price is inside execution zone';
        } elseif (in_array($status, ['MONITOR', 'PULLBACK_NEEDED', 'WAIT_FOR_MITIGATION'], true)) {
            $score += 0.75;
            $reasons[] = 'Setup is developing / waiting for mitigation';
        } elseif ($status === 'INVALID') {
            $score -= 2.0;
            $reasons[] = 'Setup invalidated by price';
        }

        /*
        |--------------------------------------------------------------------------
        | 5. Risk Reward
        |--------------------------------------------------------------------------
        */

        if ($rr >= 3.0) {
            $score += 1.5;
            $reasons[] = 'Excellent risk/reward';
        } elseif ($rr >= 2.0) {
            $score += 1.0;
            $reasons[] = 'Good risk/reward';
        } elseif ($rr >= 1.5) {
            $score += 0.5;
            $reasons[] = 'Acceptable risk/reward';
        } elseif ($rr > 0 && $rr < 1.2) {
            $score -= 1.0;
            $reasons[] = 'Weak risk/reward';
        }

        /*
        |--------------------------------------------------------------------------
        | 6. Floor Protection
        |--------------------------------------------------------------------------
        | If a complete trade plan exists, it should not return 0/10 unless
        | the setup is genuinely invalid or no zone exists.
        |--------------------------------------------------------------------------
        */

        $hasCompleteSetup = isset(
            $plan['entry_low'],
            $plan['entry_high'],
            $plan['entry_price'],
            $plan['stop_loss'],
            $plan['tp1'],
            $plan['tp2'],
            $plan['tp3']
        );

        if ($hasCompleteSetup && $status !== 'INVALID') {
            $score = max($score, 3.0);
        }

        if ($status === 'EXECUTION_READY' && $score < 5.0) {
            $score = 5.0;
        }

        if (!empty($plan['has_fvg_confluence']) && $status !== 'INVALID' && $score < 5.0) {
            $score = 5.0;
        }

        $score = round(max(0, min(10, $score)), 2);

        $plan['score'] = $score;
        $plan['confluence_reasons'] = $reasons;

        /*
        |--------------------------------------------------------------------------
        | Human Quality Labels
        |--------------------------------------------------------------------------
        */

        if ($score >= 8) {
            $plan['quality'] = 'HIGH';
            $plan['confidence'] = 'HIGH';
        } elseif ($score >= 6) {
            $plan['quality'] = 'MEDIUM';
            $plan['confidence'] = 'MEDIUM';
        } elseif ($score >= 4) {
            $plan['quality'] = 'LOW';
            $plan['confidence'] = 'LOW';
        } else {
            $plan['quality'] = 'LOW';
            $plan['confidence'] = 'VERY LOW';
        }

        /*
        |--------------------------------------------------------------------------
        | Execution State Protection
        |--------------------------------------------------------------------------
        */

        if (($plan['rr'] ?? 0) < 1.2 && in_array(($plan['status'] ?? ''), ['READY', 'EXECUTION_READY'], true)) {
            $plan['status'] = 'SELECTIVE_EXECUTION';
            $plan['display_status'] = 'Selective Execution';
            $plan['message'] = 'Price is in the zone, but risk/reward is weak. Use caution.';
        }

        if ($status !== 'INVALID' && $score >= 4 && in_array($status, ['STANDBY', 'WAIT'], true)) {
            $plan['status'] = 'MONITOR';
            $plan['display_status'] = 'Monitor';
        }

        return $plan;
    }

    private static function buildInstitutionalStopLoss(
        string $direction,
        float $entryLow,
        float $entryHigh,
        array $candles,
        string $symbol,
        int $decimals
    ): float {
        if (class_exists('VolatilityService') && method_exists('VolatilityService', 'buildStopLoss')) {
            try {
                return (float)VolatilityService::buildStopLoss($direction, $entryLow, $entryHigh, $candles, $symbol);
            } catch (Throwable $e) {
                // Fall back below.
            }
        }

        $minimum = self::minimumStopDistance($symbol);
        $atr = self::calculateATR($candles, 14);
        $distance = max($minimum, $atr * 1.5);

        if ($direction === 'BUY') {
            return round($entryLow - $distance, $decimals);
        }

        return round($entryHigh + $distance, $decimals);
    }

    private static function minimumStopDistance(string $symbol): float
    {
        return match ($symbol) {
            'XAUUSD' => 3.0,
            'USDJPY', 'EURJPY', 'GBPJPY' => 0.20,
            'GBPUSD' => 0.0025,
            default => 0.0020,
        };
    }

    private static function calculateATR(array $candles, int $period = 14): float
    {
        if (count($candles) < $period + 1) {
            return 0;
        }

        $trs = [];

        for ($i = 1; $i < count($candles); $i++) {
            $high = (float)$candles[$i]['high_price'];
            $low = (float)$candles[$i]['low_price'];
            $prevClose = (float)$candles[$i - 1]['close_price'];

            $trs[] = max(
                $high - $low,
                abs($high - $prevClose),
                abs($low - $prevClose)
            );
        }

        $recent = array_slice($trs, -$period);

        return empty($recent) ? 0 : array_sum($recent) / count($recent);
    }

    private static function inferBiasFromCandles(array $candles): string
    {
        if (count($candles) < 3) {
            return 'WAIT';
        }

        $recent = array_slice($candles, -10);
        $first = (float)$recent[0]['close_price'];
        $last = (float)end($recent)['close_price'];

        if ($last > $first) {
            return 'BUY';
        }

        if ($last < $first) {
            return 'SELL';
        }

        return 'WAIT';
    }

    private static function classifyFvgQuality(float $gapSize, float $impulse): int
    {
        $score = 1;

        if ($gapSize > 0.0010) {
            $score += 1;
        }

        if ($gapSize > 0.0020) {
            $score += 1;
        }

        if ($impulse > 0.0015) {
            $score += 1;
        }

        if ($impulse > 0.0030) {
            $score += 1;
        }

        return min(5, $score);
    }

    private static function isFvgFilled(array $candles, int $startIndex, float $low, float $high, string $direction): bool
    {
        for ($i = $startIndex; $i < count($candles); $i++) {
            $candleLow = (float)$candles[$i]['low_price'];
            $candleHigh = (float)$candles[$i]['high_price'];

            if ($direction === 'BUY' && $candleLow <= $high) {
                return true;
            }

            if ($direction === 'SELL' && $candleHigh >= $low) {
                return true;
            }
        }

        return false;
    }

    private static function findCandleIndexByTime(array $candles, string $time): ?int
    {
        foreach ($candles as $index => $candle) {
            if (($candle['open_time'] ?? '') === $time) {
                return $index;
            }
        }

        return null;
    }

    private static function findLastBearishCandleBefore(array $candles, int $index): ?array
    {
        for ($i = $index - 1; $i >= max(0, $index - 20); $i--) {
            if ((float)$candles[$i]['close_price'] < (float)$candles[$i]['open_price']) {
                return $candles[$i];
            }
        }

        return null;
    }

    private static function findLastBullishCandleBefore(array $candles, int $index): ?array
    {
        for ($i = $index - 1; $i >= max(0, $index - 20); $i--) {
            if ((float)$candles[$i]['close_price'] > (float)$candles[$i]['open_price']) {
                return $candles[$i];
            }
        }

        return null;
    }

    private static function removeDuplicateOrderBlocks(array $blocks): array
    {
        $unique = [];
        $seen = [];

        foreach ($blocks as $block) {
            $key =
                ($block['type'] ?? '') . '_' .
                ($block['time'] ?? '') . '_' .
                round((float)($block['zone_low'] ?? 0), 5) . '_' .
                round((float)($block['zone_high'] ?? 0), 5);

            if (!isset($seen[$key])) {
                $unique[] = $block;
                $seen[$key] = true;
            }
        }

        return $unique;
    }
}
