<?php

require_once __DIR__ . '/CandleService.php';
require_once __DIR__ . '/SmcAnalyzer.php';
require_once __DIR__ . '/TradingStyleService.php';

class MarketStructureService
{
    public static function analyze(string $symbol, string $timeframe, string $styleKey = 'intraday', array $newsRisk = [], array $geoRisk = []): array
    {
        $style = TradingStyleService::getStyle($styleKey);
        $executionCandles = CandleService::getCandles($symbol, $timeframe, 250);
        $contextTimeframe = self::getHigherTimeframe($timeframe);
        $contextCandles = CandleService::getCandles($symbol, $contextTimeframe, 250);

        if (empty($executionCandles)) {
            return self::emptyResult($symbol, $timeframe, $contextTimeframe, $style);
        }

        $lastCandle = end($executionCandles);
        $currentPrice = (float)($lastCandle['close_price'] ?? 0);

        $swings = SmcAnalyzer::detectSwings($executionCandles);
        $structureEvents = SmcAnalyzer::detectStructureEvents($swings);
        $swingExtremes = self::latestSwingHighLow($swings);
        if (!empty($structureEvents)) {
            $lastIndex = count($structureEvents) - 1;
            $structureEvents[$lastIndex]['swing_high'] = $swingExtremes['swing_high'];
            $structureEvents[$lastIndex]['swing_low'] = $swingExtremes['swing_low'];
        }
        $executionBias = SmcAnalyzer::getBias($structureEvents);
        if ($executionBias === 'WAIT') $executionBias = self::inferMomentumBias($executionCandles);

        $contextSwings = SmcAnalyzer::detectSwings($contextCandles);
        $contextEvents = SmcAnalyzer::detectStructureEvents($contextSwings);
        $contextBias = SmcAnalyzer::getBias($contextEvents);
        if ($contextBias === 'WAIT') $contextBias = self::inferMomentumBias($contextCandles);

        $aligned = in_array($executionBias, ['BUY', 'SELL'], true) && $executionBias === $contextBias;

        $rangeCandles = !empty($contextCandles) ? $contextCandles : $executionCandles;
        $dealingRange = self::buildDealingRange($rangeCandles, $style);
        $priceLocation = self::getPriceLocation($currentPrice, $dealingRange);
        $rangePosition = self::rangePosition($currentPrice, $dealingRange['range_high'] ?? null, $dealingRange['range_low'] ?? null);

        $liquidity = self::buildLiquidityMap($swings, $executionCandles, $executionBias);
        $liquidityEvent = self::detectLiquidityEvent($executionCandles, $liquidity, $executionBias);
        $displacement = self::detectDisplacement($executionCandles, $executionBias);

        $orderBlocks = SmcAnalyzer::detectOrderBlocks($executionCandles, $structureEvents);
        $latestOrderBlock = self::latestZoneByTime($orderBlocks);
        $activeOrderBlock = self::selectActiveOrderBlock($symbol, $orderBlocks, $executionBias, $currentPrice, $style);

        $fvgZones = SmcAnalyzer::detectFVG($executionCandles);
        $latestFVG = self::latestZoneByTime($fvgZones);
        $activeFVG = self::selectActiveFVG($symbol, $fvgZones, $executionBias, $currentPrice, $style);

        $atr = self::calculateATR($executionCandles, 14);
        $volatility = self::classifyVolatility($symbol, $atr);

        $candidates = self::buildExecutionCandidates(
            $symbol,
            $executionCandles,
            $executionBias,
            $contextBias,
            $currentPrice,
            $style,
            $activeOrderBlock,
            $activeFVG,
            $latestOrderBlock,
            $latestFVG,
            $priceLocation,
            $liquidityEvent,
            $displacement,
            !empty($structureEvents),
            $aligned,
            $volatility,
            $newsRisk,
            $geoRisk
        );

        $bestCandidate = self::selectBestCandidate($candidates);
        $tradePlan = self::buildTradePlanFromCandidate(
            $symbol,
            $style,
            $bestCandidate,
            $executionBias,
            $currentPrice,
            $newsRisk,
            $geoRisk,
            $volatility
        );

        $sessionIntelligence = self::sessionIntelligence();
        $symbolPersonality = self::symbolPersonality($symbol);
        $alignmentStrength = self::alignmentStrength(
            $executionBias,
            $contextBias,
            $aligned,
            !empty($structureEvents),
            $displacement,
            $liquidityEvent
        );

        $tradePlan = self::applyPhase2ScoreModifiers(
            $tradePlan,
            $sessionIntelligence,
            $symbolPersonality,
            $alignmentStrength
        );

        $locationQuality = self::locationQuality(
            $executionBias,
            $priceLocation,
            $aligned,
            $liquidityEvent,
            $displacement
        );

        $tradePlan = self::applyLocationAwareTradeAdjustment($tradePlan, $locationQuality);

        $tradePlan = self::phase3ConfirmationLayer(
            $executionCandles ?? $candles,
            $executionBias,
            $bestCandidate,
            $currentPrice,
            (float)$atr,
            $tradePlan
        );

        $executionMode = self::executionMode($tradePlan, $bestCandidate ?? [], $newsRisk, $geoRisk, $volatility);
        $setupLifecycle = self::setupLifecycle($tradePlan, $bestCandidate, $currentPrice);
        $rankedSetups = self::rankCandidates($candidates);

        if (empty($tradePlan['execution_mode']) || !in_array($tradePlan['execution_mode'], ['REDUCED_RR', 'LOCATION_CAUTION'], true)) {
            $tradePlan['execution_mode'] = $executionMode['mode'];
            $tradePlan['execution_guidance'] = $executionMode['guidance'];
        }
        $tradePlan['lifecycle_stage'] = $setupLifecycle['stage'];
        $tradePlan['lifecycle_guidance'] = $setupLifecycle['guidance'];
        $tradePlan['lifecycle_progress'] = $setupLifecycle['progress'];
        $tradePlan['alignment_strength'] = $alignmentStrength;
        $tradePlan['session_name'] = $sessionIntelligence['name'];
        $tradePlan['symbol_model'] = $symbolPersonality['name'];

        $decision = self::buildDecision($tradePlan, $executionBias, $contextBias, $priceLocation, $aligned, $volatility, $newsRisk, $geoRisk);

        $institutionalBias = [
            'htf_bias' => $contextBias,
            'ltf_bias' => $executionBias,
            'overall_bias' => $aligned ? $executionBias : 'MIXED',
            'aligned' => $aligned,
            'location' => $priceLocation,
            'confidence' => $decision['confidence'] ?? 'LOW',
            'institutional_state' => $decision['institutional_state'] ?? 'PULLBACK / TRANSITION',
            'narrative' => self::buildNarrative($symbol, $timeframe, $contextTimeframe, $executionBias, $contextBias, $priceLocation, $tradePlan, $decision, $liquidityEvent, $displacement)
        ];

        return [
            'symbol' => $symbol,
            'timeframe' => $timeframe,
            'context_timeframe' => $contextTimeframe,
            'current_price' => $currentPrice,
            'execution_bias' => $executionBias,
            'context_bias' => $contextBias,
            'aligned' => $aligned,
            'swings' => $swings,
            'structure_events' => $structureEvents,
            'latest_event' => !empty($structureEvents) ? end($structureEvents) : null,
            'context_swings' => $contextSwings,
            'context_events' => $contextEvents,
            'context_latest_event' => !empty($contextEvents) ? end($contextEvents) : null,
            'dealing_range' => $dealingRange,
            'price_location' => $priceLocation,
            'range_position' => $rangePosition,
            'array_advantage' => self::arrayAdvantage($executionBias, $priceLocation),
            'liquidity' => $liquidity,
            'liquidity_event' => $liquidityEvent,
            'displacement' => $displacement,
            'order_blocks' => $orderBlocks,
            'active_order_block' => $activeOrderBlock,
            'latest_order_block' => $latestOrderBlock,
            'fvg_zones' => $fvgZones,
            'active_fvg' => $activeFVG,
            'latest_fvg' => $latestFVG,
            'fvg_ob_confluence' => self::zonesOverlap($activeFVG, $activeOrderBlock),
            'confluence_zone' => self::confluenceZone($activeFVG, $activeOrderBlock),
            'candidates' => $candidates,
            'best_candidate' => $bestCandidate,
            'ranked_setups' => $rankedSetups ?? [],
            'execution_mode' => $executionMode ?? [],
            'setup_lifecycle' => $setupLifecycle ?? [],
            'session_intelligence' => $sessionIntelligence ?? [],
            'symbol_personality' => $symbolPersonality ?? [],
            'alignment_strength' => $alignmentStrength ?? 0,
            'location_quality' => $locationQuality ?? [],
            'phase3' => $tradePlan['phase3'] ?? [],
            'atr' => $atr,
            'volatility' => $volatility,
            'trading_style' => $style,
            'institutional_bias' => $institutionalBias,
            'trade_plan' => $tradePlan,
            'decision' => $decision,
            'narrative' => $institutionalBias['narrative']
        ];
    }

    private static function buildExecutionCandidates(
        string $symbol,
        array $candles,
        string $bias,
        string $contextBias,
        float $currentPrice,
        array $style,
        ?array $activeOrderBlock,
        ?array $activeFVG,
        ?array $latestOrderBlock,
        ?array $latestFVG,
        string $priceLocation,
        array $liquidityEvent,
        array $displacement,
        bool $hasStructureShift,
        bool $aligned,
        string $volatility,
        array $newsRisk,
        array $geoRisk
    ): array {
        $candidates = [];

        if (!in_array($bias, ['BUY', 'SELL'], true)) {
            return [];
        }

        if ($activeOrderBlock && (($activeOrderBlock['direction'] ?? '') === $bias)) {
            $candidates[] = self::scoreCandidate($symbol, $style, [
                'type' => 'ORDER_BLOCK',
                'source' => 'Institutional Order Block',
                'direction' => $bias,
                'entry_low' => (float)$activeOrderBlock['zone_low'],
                'entry_high' => (float)$activeOrderBlock['zone_high'],
                'time' => $activeOrderBlock['time'] ?? null,
                'poi_quality' => (int)($activeOrderBlock['quality'] ?? 2),
            ], $currentPrice, $contextBias, $priceLocation, $liquidityEvent, $displacement, $hasStructureShift, $aligned, $volatility, $newsRisk, $geoRisk);
        }

        if ($activeFVG && (($activeFVG['direction'] ?? '') === $bias)) {
            $candidates[] = self::scoreCandidate($symbol, $style, [
                'type' => 'FVG',
                'source' => 'Fair Value Gap Mitigation',
                'direction' => $bias,
                'entry_low' => (float)$activeFVG['low'],
                'entry_high' => (float)$activeFVG['high'],
                'time' => $activeFVG['time'] ?? null,
                'poi_quality' => (int)($activeFVG['quality'] ?? 2),
            ], $currentPrice, $contextBias, $priceLocation, $liquidityEvent, $displacement, $hasStructureShift, $aligned, $volatility, $newsRisk, $geoRisk);
        }

        if ($activeOrderBlock && $activeFVG && self::zonesOverlap($activeFVG, $activeOrderBlock)) {
            $zone = self::confluenceZone($activeFVG, $activeOrderBlock);
            if ($zone) {
                $candidates[] = self::scoreCandidate($symbol, $style, [
                    'type' => 'OB_FVG_CONFLUENCE',
                    'source' => 'OB + FVG Confluence Zone',
                    'direction' => $bias,
                    'entry_low' => (float)$zone['low'],
                    'entry_high' => (float)$zone['high'],
                    'time' => $activeFVG['time'] ?? ($activeOrderBlock['time'] ?? null),
                    'poi_quality' => 5,
                ], $currentPrice, $contextBias, $priceLocation, $liquidityEvent, $displacement, $hasStructureShift, $aligned, $volatility, $newsRisk, $geoRisk);
            }
        }


        if (!$activeOrderBlock && $latestOrderBlock && (($latestOrderBlock['direction'] ?? '') === $bias)) {
            $candidates[] = self::scoreCandidate($symbol, $style, [
                'type' => 'ORDER_BLOCK',
                'source' => 'Latest Institutional Order Block',
                'direction' => $bias,
                'entry_low' => (float)$latestOrderBlock['zone_low'],
                'entry_high' => (float)$latestOrderBlock['zone_high'],
                'time' => $latestOrderBlock['time'] ?? null,
                'poi_quality' => (int)($latestOrderBlock['quality'] ?? 1),
            ], $currentPrice, $contextBias, $priceLocation, $liquidityEvent, $displacement, $hasStructureShift, $aligned, $volatility, $newsRisk, $geoRisk);
        }

        if (!$activeFVG && $latestFVG && (($latestFVG['direction'] ?? '') === $bias)) {
            $candidates[] = self::scoreCandidate($symbol, $style, [
                'type' => 'FVG',
                'source' => 'Latest Fair Value Gap',
                'direction' => $bias,
                'entry_low' => (float)$latestFVG['low'],
                'entry_high' => (float)$latestFVG['high'],
                'time' => $latestFVG['time'] ?? null,
                'poi_quality' => (int)($latestFVG['quality'] ?? 1),
            ], $currentPrice, $contextBias, $priceLocation, $liquidityEvent, $displacement, $hasStructureShift, $aligned, $volatility, $newsRisk, $geoRisk);
        }

        $micro = self::buildMicroPullbackZone($candles, $bias, $style);
        if ($micro) {
            $candidates[] = self::scoreCandidate($symbol, $style, $micro, $currentPrice, $contextBias, $priceLocation, $liquidityEvent, $displacement, $hasStructureShift, $aligned, $volatility, $newsRisk, $geoRisk);
        }

        return array_values(array_filter($candidates));
    }

    private static function scoreCandidate(
        string $symbol,
        array $style,
        array $candidate,
        float $currentPrice,
        string $contextBias,
        string $priceLocation,
        array $liquidityEvent,
        array $displacement,
        bool $hasStructureShift,
        bool $aligned,
        string $volatility,
        array $newsRisk,
        array $geoRisk
    ): array {
        $direction = $candidate['direction'] ?? 'WAIT';
        $low = (float)($candidate['entry_low'] ?? 0);
        $high = (float)($candidate['entry_high'] ?? 0);
        if ($low <= 0 || $high <= 0 || !in_array($direction, ['BUY', 'SELL'], true)) return [];
        if ($low > $high) { $tmp = $low; $low = $high; $high = $tmp; }

        $zoneMid = ($low + $high) / 2;
        $distance = self::distanceFromZone($currentPrice, $low, $high);
        $maxDistance = TradingStyleService::maxEntryDistance($symbol, $style);
        $tooFar = $distance > $maxDistance;

        $score = 0;
        $reasons = [];

        if ($aligned) { $score += 2; $reasons[] = 'HTF and execution bias aligned'; }
        elseif ($contextBias === $direction) { $score += 1; $reasons[] = 'HTF bias supports direction'; }
        else { $score -= 2; $reasons[] = 'HTF bias not aligned'; }

        $arrayFavorable = self::arrayIsFavorable($direction, $priceLocation);
        $arrayNeutral = in_array($priceLocation, ['EQUILIBRIUM','SLIGHT PREMIUM','SLIGHT DISCOUNT'], true);

        if ($arrayFavorable) {
            $score += 2;
            $reasons[] = 'Price is in favorable dealing range';
        } elseif ($arrayNeutral) {
            $score += 1;
            $reasons[] = 'Price is close to equilibrium / transition area';
        } else {
            // Buying deep premium or selling deep discount is lower-probability unless
            // there is a confirmed sweep/displacement continuation. Penalize strongly.
            $score -= 1;
            $reasons[] = 'Price is not in ideal premium/discount';
        }

        $liquidityConfirmed = !empty($liquidityEvent['confirmed']);
        $displacementConfirmed = !empty($displacement['confirmed']);
        $extremeUnfavorableArray =
            ($direction === 'BUY' && $priceLocation === 'EXTREME PREMIUM') ||
            ($direction === 'SELL' && $priceLocation === 'EXTREME DISCOUNT');

        if ($extremeUnfavorableArray && (!$liquidityConfirmed || !$displacementConfirmed)) {
            $score -= 1;
            $reasons[] = 'Extreme premium/discount requires liquidity sweep and displacement confirmation';
        }

        if ($liquidityConfirmed) { $score += 2; $reasons[] = $liquidityEvent['narrative']; }
        if ($displacementConfirmed) { $score += 2; $reasons[] = $displacement['narrative']; }
        if ($hasStructureShift) { $score += 2; $reasons[] = 'BOS/CHoCH structure confirmation present'; }

        $type = $candidate['type'] ?? '';
        if ($type === 'OB_FVG_CONFLUENCE') { $score += 3; $reasons[] = 'OB and FVG overlap'; }
        elseif ($type === 'ORDER_BLOCK') { $score += 2; $reasons[] = 'Order block POI present'; }
        elseif ($type === 'FVG') { $score += 2; $reasons[] = 'FVG mitigation POI present'; }
        elseif ($type === 'MICRO_PULLBACK') { $score += 1; $reasons[] = 'Recent pullback zone for active continuation'; }

        if (!$tooFar) {
            $score += 2;
            $reasons[] = 'Entry zone is close enough for selected style';
        } else {
            // Too far should create Watchlist/Pullback Needed, not kill the setup.
            $score -= 1;
            $reasons[] = 'Entry zone is not nearby yet for selected style';
        }

        if ($volatility === 'EXTREME') { $score -= 2; $reasons[] = 'Extreme volatility risk'; }
        elseif ($volatility === 'HIGH') { $score -= 1; $reasons[] = 'High volatility risk'; }
        else { $score += 1; $reasons[] = 'Volatility acceptable'; }

        // Macro/news/geopolitical intelligence must not invalidate technical setup scoring.
        // Upcoming news is handled later as execution awareness, not as a setup killer.
        if (!empty($newsRisk['has_news'])) {
            $reasons[] = 'Scheduled high-impact news awareness active';
        }

        $geoRiskLevel = strtoupper((string)($geoRisk['risk'] ?? ''));
        if ($geoRiskLevel === 'HIGH' || $geoRiskLevel === 'EXTREME') {
            $score -= 1;
            $reasons[] = 'Geopolitical risk requires reduced exposure';
        } elseif ($geoRiskLevel === 'MEDIUM') {
            $reasons[] = 'Medium geopolitical risk noted';
        }

        $candidate['entry_low'] = $low;
        $candidate['entry_high'] = $high;
        $candidate['entry_price'] = $zoneMid;
        $candidate['distance_from_price'] = $distance;
        $candidate['midpoint_distance_from_price'] = abs($currentPrice - $zoneMid);
        $candidate['max_distance'] = $maxDistance;
        $candidate['too_far'] = $tooFar;
        $candidate['array_favorable'] = $arrayFavorable;
        $candidate['array_neutral'] = $arrayNeutral;
        $candidate['price_location'] = $priceLocation;
        $candidate['liquidity_sweep_confirmed'] = $liquidityConfirmed;
        $candidate['displacement_confirmed'] = $displacementConfirmed;
        $candidate['extreme_unfavorable_array'] = $extremeUnfavorableArray;
        // If a technical POI exists, preserve a non-zero technical score unless invalid.
        if (in_array(($candidate['type'] ?? ''), ['ORDER_BLOCK', 'FVG', 'OB_FVG_CONFLUENCE', 'MICRO_PULLBACK'], true)) {
            $score = max($score, 3);
        }

        if (!empty($candidate['array_favorable']) || !empty($candidate['array_neutral'])) {
            $score = max($score, 4);
        }

        $candidate['score'] = max(0, min(15, $score));
        $candidate['raw_score'] = $score;
        $candidate['reasons'] = $reasons;

        return $candidate;
    }

    private static function selectBestCandidate(array $candidates): ?array
    {
        if (empty($candidates)) return null;
        usort($candidates, function ($a, $b) {
            if (($a['score'] ?? 0) === ($b['score'] ?? 0)) {
                return ($a['distance_from_price'] ?? 999999) <=> ($b['distance_from_price'] ?? 999999);
            }
            return ($b['score'] ?? 0) <=> ($a['score'] ?? 0);
        });
        return $candidates[0];
    }



    private static function dynamicStopLossDistance(
        string $symbol,
        string $direction,
        float $entryPrice,
        float $entryLow,
        float $entryHigh,
        float $baseSlDistance,
        float $currentPrice,
        array $candidate,
        string $volatility
    ): array {
        /*
        |--------------------------------------------------------------------------
        | Dynamic Institutional SL Model
        |--------------------------------------------------------------------------
        | The DB/style SL is the base risk model.
        | Final SL must respect:
        | - style minimum,
        | - zone size,
        | - current execution location,
        | - volatility,
        | - structure buffer.
        |--------------------------------------------------------------------------
        */

        $zoneSize = abs($entryHigh - $entryLow);
        $executionSpread = abs($entryPrice - ($direction === 'BUY' ? $entryLow : $entryHigh));

        $volatilityMultiplier = 1.0;
        if ($volatility === 'EXTREME') {
            $volatilityMultiplier = 1.75;
        } elseif ($volatility === 'HIGH') {
            $volatilityMultiplier = 1.35;
        }

        $structureDistance = $zoneSize + ($baseSlDistance * 0.5);
        $executionDistance = $executionSpread + $baseSlDistance;

        $finalDistance = max(
            $baseSlDistance,
            $structureDistance,
            $executionDistance
        );

        $finalDistance *= $volatilityMultiplier;

        /*
        |--------------------------------------------------------------------------
        | Do not let dynamic SL become absurdly wide.
        | This prevents the engine from producing unusable SLs when a very large
        | historical zone is selected.
        |--------------------------------------------------------------------------
        */

        $capMultiplier = strtoupper($symbol) === 'XAUUSD' ? 6.0 : 4.0;
        $maxAllowed = $baseSlDistance * $capMultiplier;

        $finalDistance = min($finalDistance, $maxAllowed);

        return [
            'base_distance' => $baseSlDistance,
            'final_distance' => $finalDistance,
            'zone_size' => $zoneSize,
            'structure_distance' => $structureDistance,
            'execution_distance' => $executionDistance,
            'volatility_multiplier' => $volatilityMultiplier,
            'reason' => self::dynamicSlReason($baseSlDistance, $finalDistance, $volatility, $zoneSize),
        ];
    }

    private static function dynamicSlReason(float $baseDistance, float $finalDistance, string $volatility, float $zoneSize): string
    {
        if ($finalDistance <= ($baseDistance * 1.05)) {
            return 'Base style SL is sufficient for the current structure.';
        }

        $parts = [];

        if ($zoneSize > $baseDistance) {
            $parts[] = 'institutional zone is wider than the base SL';
        }

        if ($volatility === 'EXTREME' || $volatility === 'HIGH') {
            $parts[] = strtolower($volatility) . ' volatility requires extra buffer';
        }

        if (empty($parts)) {
            $parts[] = 'execution location requires additional structure buffer';
        }

        return 'Dynamic SL widened because ' . implode(' and ', $parts) . '.';
    }


    private static function determineZoneInteraction(
        string $direction,
        float $currentPrice,
        float $entryLow,
        float $entryHigh,
        float $slDistance,
        array $candidate,
        array $style,
        string $symbol
    ): array {
        if ($entryLow > $entryHigh) {
            [$entryLow, $entryHigh] = [$entryHigh, $entryLow];
        }

        $insideZone = ($currentPrice >= $entryLow && $currentPrice <= $entryHigh);
        $distanceFromZone = self::distanceFromZone($currentPrice, $entryLow, $entryHigh);
        $maxEntryDistance = (float)($candidate['max_distance'] ?? TradingStyleService::maxEntryDistance($symbol, $style));

        $hasLiquidity = !empty($candidate['liquidity_sweep_confirmed']);
        $hasDisplacement = !empty($candidate['displacement_confirmed']);

        /*
        |--------------------------------------------------------------------------
        | Zone interaction stages
        |--------------------------------------------------------------------------
        | The institutional zone is not changed by current price.
        | This only determines whether the zone is executable, watched, missed,
        | or invalidated.
        |--------------------------------------------------------------------------
        */

        if ($insideZone) {
            if ($hasLiquidity && $hasDisplacement) {
                return [
                    'state' => 'REACTION_CONFIRMED',
                    'status' => 'EXECUTION_READY',
                    'display' => 'Execution Ready',
                    'entry_price' => $currentPrice,
                    'message' => 'Price is inside the institutional ' . $direction . ' zone with liquidity and displacement confirmation.',
                    'distance_from_entry' => 0.0,
                    'inside_zone' => true,
                ];
            }

            return [
                'state' => 'ZONE_TAPPED',
                'status' => 'WAIT_FOR_CONFIRMATION',
                'display' => 'Wait for Confirmation',
                'entry_price' => $currentPrice,
                'message' => 'Price is inside the institutional ' . $direction . ' zone. Wait for lower-timeframe confirmation before execution.',
                'distance_from_entry' => 0.0,
                'inside_zone' => true,
            ];
        }

        if ($direction === 'BUY') {
            if ($currentPrice > $entryHigh) {
                return [
                    'state' => 'WAITING_FOR_PULLBACK',
                    'status' => 'WAIT_FOR_MITIGATION',
                    'display' => 'Wait for Mitigation',
                    'entry_price' => $entryHigh,
                    'message' => 'SMC confluence supports BUY, but price is above the execution zone. Wait for pullback into the institutional zone.',
                    'distance_from_entry' => abs($currentPrice - $entryHigh),
                    'inside_zone' => false,
                ];
            }

            if ($currentPrice < ($entryLow - $slDistance)) {
                return [
                    'state' => 'INVALIDATED',
                    'status' => 'INVALID',
                    'display' => 'Invalidated',
                    'entry_price' => $entryLow,
                    'message' => 'Price has traded below the BUY invalidation area. Wait for a fresh institutional zone.',
                    'distance_from_entry' => abs($currentPrice - $entryLow),
                    'inside_zone' => false,
                ];
            }

            return [
                'state' => 'APPROACHING_ZONE',
                'status' => 'WATCHLIST',
                'display' => 'Watchlist',
                'entry_price' => $entryLow,
                'message' => 'SMC confluence supports BUY, but price has not reached the execution zone yet.',
                'distance_from_entry' => abs($currentPrice - $entryLow),
                'inside_zone' => false,
            ];
        }

        if ($direction === 'SELL') {
            if ($currentPrice < $entryLow) {
                return [
                    'state' => 'WAITING_FOR_PULLBACK',
                    'status' => 'WAIT_FOR_MITIGATION',
                    'display' => 'Wait for Mitigation',
                    'entry_price' => $entryLow,
                    'message' => 'SMC confluence supports SELL, but price is below the execution zone. Wait for pullback into the institutional zone.',
                    'distance_from_entry' => abs($currentPrice - $entryLow),
                    'inside_zone' => false,
                ];
            }

            if ($currentPrice > ($entryHigh + $slDistance)) {
                return [
                    'state' => 'INVALIDATED',
                    'status' => 'INVALID',
                    'display' => 'Invalidated',
                    'entry_price' => $entryHigh,
                    'message' => 'Price has traded above the SELL invalidation area. Wait for a fresh institutional zone.',
                    'distance_from_entry' => abs($currentPrice - $entryHigh),
                    'inside_zone' => false,
                ];
            }

            return [
                'state' => 'APPROACHING_ZONE',
                'status' => 'WATCHLIST',
                'display' => 'Watchlist',
                'entry_price' => $entryHigh,
                'message' => 'SMC confluence supports SELL, but price has not reached the execution zone yet.',
                'distance_from_entry' => abs($currentPrice - $entryHigh),
                'inside_zone' => false,
            ];
        }

        return [
            'state' => 'NO_VALID_SETUP',
            'status' => 'NO_VALID_SETUP',
            'display' => 'No Valid Setup',
            'entry_price' => null,
            'message' => 'No valid institutional execution state is available.',
            'distance_from_entry' => null,
            'inside_zone' => false,
        ];
    }

    private static function executionEntryLabel(array $interaction): string
    {
        $state = strtoupper((string)($interaction['state'] ?? ''));

        return match ($state) {
            'REACTION_CONFIRMED' => 'Market execution after confirmation',
            'ZONE_TAPPED' => 'Confirm before execution',
            'WAITING_FOR_PULLBACK' => 'Zone boundary - wait for mitigation',
            'APPROACHING_ZONE' => 'Pending zone entry',
            'INVALIDATED' => 'Invalidated',
            default => 'Pending confirmation',
        };
    }


    private static function buildTradePlanFromCandidate(string $symbol, array $style, ?array $candidate, string $bias, float $currentPrice, array $newsRisk, array $geoRisk, string $volatility): array
    {
        $style = TradingStyleService::normalize($style);
        $slDistance = TradingStyleService::stopDistance($symbol, $style);
        $tp = TradingStyleService::tpMultipliers($symbol, $style);
        $rrLabel = TradingStyleService::rrLabel($symbol, $style);
        $slPips = TradingStyleService::stopPips($symbol, $style);

        if (!$candidate || ($candidate['score'] ?? 0) < 3) {
            $message = 'No clear institutional execution zone has formed yet. Monitor fresh liquidity sweep, displacement, or mitigation.';
            return [
                'status' => 'NO_VALID_SETUP',
                'display_status' => 'No Valid Setup',
                'message' => $message,
                'direction' => in_array($bias, ['BUY','SELL'], true) ? $bias : 'WAIT',
                'bias_context' => in_array($bias, ['BUY','SELL'], true) ? $bias : 'WAIT',
                'current_price' => $currentPrice,
                'score' => $candidate['score'] ?? 0,
                'quality' => 'VERY LOW',
                'entry_source' => $candidate['source'] ?? 'N/A',
                'style_key' => $style['style_key'],
                'style_name' => $style['style_name'],
                'sl_pips' => $slPips,
                'sl_distance' => $slDistance,
                'sl_distance_label' => TradingStyleService::distanceLabel($symbol, $slDistance),
                'base_sl_distance' => $slDistance,
                'base_sl_distance_label' => TradingStyleService::distanceLabel($symbol, $slDistance),
                'dynamic_sl_distance' => $slDistance,
                'dynamic_sl_distance_label' => TradingStyleService::distanceLabel($symbol, $slDistance),
                'rr_label' => $rrLabel,
                'execution_state' => 'NO_VALID_SETUP',
                'entry_label' => 'No executable zone',
                'reasons' => $candidate['reasons'] ?? [],
            ];
        }

        $direction = $candidate['direction'];
        $entryLow = (float)$candidate['entry_low'];
        $entryHigh = (float)$candidate['entry_high'];

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

        /*
        |--------------------------------------------------------------------------
        | Preserve true institutional zone
        |--------------------------------------------------------------------------
        | entry_low / entry_high remain the candle-derived POI.
        | suggested entry depends on interaction state.
        |--------------------------------------------------------------------------
        */

        $interaction = self::determineZoneInteraction(
            $direction,
            $currentPrice,
            $entryLow,
            $entryHigh,
            $slDistance,
            $candidate,
            $style,
            $symbol
        );

        $entryPrice = $interaction['entry_price'] ?? (($entryLow + $entryHigh) / 2);

        /*
        |--------------------------------------------------------------------------
        | Stop loss and targets must be calculated from the suggested entry,
        | but invalidation must respect the zone.
        |--------------------------------------------------------------------------
        */

        $dynamicSl = self::dynamicStopLossDistance(
            $symbol,
            $direction,
            $entryPrice,
            $entryLow,
            $entryHigh,
            $slDistance,
            $currentPrice,
            $candidate,
            $volatility
        );

        $finalSlDistance = (float)$dynamicSl['final_distance'];

        if ($direction === 'BUY') {
            $stopLoss = $entryPrice - $finalSlDistance;
            $tp1 = $entryPrice + ($finalSlDistance * $tp['tp1']);
            $tp2 = $entryPrice + ($finalSlDistance * $tp['tp2']);
            $tp3 = $entryPrice + ($finalSlDistance * $tp['tp3']);
        } else {
            $stopLoss = $entryPrice + $finalSlDistance;
            $tp1 = $entryPrice - ($finalSlDistance * $tp['tp1']);
            $tp2 = $entryPrice - ($finalSlDistance * $tp['tp2']);
            $tp3 = $entryPrice - ($finalSlDistance * $tp['tp3']);
        }

        $status = $interaction['status'];
        $display = $interaction['display'];
        $message = $interaction['message'];

        /*
        |--------------------------------------------------------------------------
        | Chasing protection
        |--------------------------------------------------------------------------
        | If price is away from the zone, this is not executable.
        | The suggested entry is the zone boundary and the message must say wait.
        |--------------------------------------------------------------------------
        */

        $distanceFromEntry = $interaction['distance_from_entry'];
        $maxEntryDistance = (float)($candidate['max_distance'] ?? TradingStyleService::maxEntryDistance($symbol, $style));

        if (
            !($interaction['inside_zone'] ?? false) &&
            $distanceFromEntry !== null &&
            $distanceFromEntry > ($maxEntryDistance * 1.5) &&
            !in_array($status, ['INVALID', 'NO_VALID_SETUP'], true)
        ) {
            $status = 'MISSED_ENTRY';
            $display = 'Missed Entry';
            $message = 'The institutional entry zone is no longer close enough for clean execution. Wait for a fresh pullback or a new zone.';
        }

        /*
        |--------------------------------------------------------------------------
        | Institutional caution gate
        |--------------------------------------------------------------------------
        */

        $unfavorableArray = empty($candidate['array_favorable']) && empty($candidate['array_neutral']);
        $needsExtraConfirmation = $unfavorableArray && (empty($candidate['liquidity_sweep_confirmed']) || empty($candidate['displacement_confirmed']));

        if ($needsExtraConfirmation && in_array($status, ['EXECUTION_READY', 'SELECTIVE_EXECUTION', 'WAIT_FOR_MITIGATION', 'WATCHLIST'], true)) {
            $status = 'WAIT_FOR_CONFIRMATION';
            $display = 'Wait for Confirmation';
            $message = 'Directional bias exists, but price is currently positioned in an unfavorable institutional dealing range. Wait for liquidity sweep, displacement, and lower-timeframe confirmation before execution.';
        }

        if (!empty($newsRisk['has_news'])) {
            if ($status === 'EXECUTION_READY') {
                $status = 'SELECTIVE_EXECUTION';
                $display = 'News Caution';
            }
            $message .= ' High-impact news is near; avoid entering during the release window.';
        }

        $geoRiskLevel = strtoupper((string)($geoRisk['risk'] ?? ''));
        if ($geoRiskLevel === 'HIGH' || $geoRiskLevel === 'EXTREME') {
            if ($status === 'EXECUTION_READY') {
                $status = 'SELECTIVE_EXECUTION';
                $display = 'Reduced Risk Execution';
            }
            $message .= ' Geopolitical risk is elevated; reduce exposure and require confirmation.';
        }

        if ($volatility === 'EXTREME' && $status === 'EXECUTION_READY') {
            $status = 'SELECTIVE_EXECUTION';
            $display = 'Reduced Risk Execution';
            $message = 'SMC setup is valid, but volatility is extreme. Use reduced risk and confirmation.';
        }

        $score = (float)($candidate['score'] ?? 0);
        $quality = $score >= 9 ? 'HIGH' : ($score >= 6 ? 'MEDIUM' : ($score >= 3 ? 'LOW' : 'VERY LOW'));

        $displayDirection = ($status === 'NO_VALID_SETUP')
            ? 'WAIT'
            : $direction;

        return [
            'status' => $status,
            'display_status' => $display,
            'message' => $message,
            'direction' => $displayDirection,
            'bias_context' => $direction,
            'current_price' => $currentPrice,

            // True institutional POI from candles
            'entry_low' => $entryLow,
            'entry_high' => $entryHigh,

            // Suggested execution price based on zone interaction state
            'entry_price' => $entryPrice,
            'entry_label' => self::executionEntryLabel($interaction),
            'execution_state' => $interaction['state'],
            'inside_zone' => (bool)($interaction['inside_zone'] ?? false),

            'stop_loss' => $stopLoss,
            'tp1' => $tp1,
            'tp2' => $tp2,
            'tp3' => $tp3,
            'take_profit' => $tp2,
            'risk' => abs($entryPrice - $stopLoss),
            'reward' => abs($tp2 - $entryPrice),
            'base_sl_distance' => $slDistance,
            'base_sl_distance_label' => TradingStyleService::distanceLabel($symbol, $slDistance),
            'dynamic_sl_distance' => $finalSlDistance,
            'dynamic_sl_distance_label' => TradingStyleService::distanceLabel($symbol, $finalSlDistance),
            'dynamic_sl_reason' => $dynamicSl['reason'] ?? '',
            'sl_volatility_multiplier' => $dynamicSl['volatility_multiplier'] ?? 1.0,
            'sl_zone_size' => $dynamicSl['zone_size'] ?? 0,
            'rr' => $tp['tp3'],
            'rr_label' => $rrLabel,
            'tp_rr' => $tp,
            'score' => round($score, 2),
            'quality' => $quality,
            'entry_source' => $candidate['source'],
            'candidate_type' => $candidate['type'],
            'has_fvg_confluence' => in_array($candidate['type'], ['FVG','OB_FVG_CONFLUENCE'], true),
            'style_key' => $style['style_key'],
            'style_name' => $style['style_name'],
            'sl_pips' => $slPips,
            'sl_distance' => $finalSlDistance,
            'sl_distance_label' => TradingStyleService::distanceLabel($symbol, $finalSlDistance),
            'distance_from_price' => $distanceFromEntry,
            'max_distance' => $candidate['max_distance'] ?? null,
            'zone_midpoint' => ($entryLow + $entryHigh) / 2,
            'reasons' => $candidate['reasons'] ?? [],
        ];
    }

    private static function buildDecision(array $tradePlan, string $executionBias, string $contextBias, string $priceLocation, bool $aligned, string $volatility, array $newsRisk = [], array $geoRisk = []): array
    {
        $status = $tradePlan['status'] ?? 'NO_VALID_SETUP';
        $direction = $tradePlan['direction'] ?? 'WAIT';
        $score = (float)($tradePlan['score'] ?? 0);

        $label = 'No Valid Setup';
        $action = 'WAIT';
        $tone = 'neutral';
        $confidence = 'VERY LOW';
        $summary = $tradePlan['message'] ?? 'Wait for clearer institutional confirmation.';

        if (in_array($status, ['EXECUTION_READY','SELECTIVE_EXECUTION'], true)) {
            $label = $status === 'SELECTIVE_EXECUTION' ? 'Reduced Risk Execution' : 'Execution Ready';
            $action = $direction;
            $tone = $direction === 'BUY' ? 'bullish' : 'bearish';
            $confidence = $score >= 8 ? 'HIGH' : 'MEDIUM';
            $summary = $tradePlan['message'];
        } elseif ($status === 'WAIT_FOR_MITIGATION') {
            $label = 'Wait for Mitigation';
            $action = 'WAIT';
            $tone = 'caution';
            $confidence = 'MEDIUM';
        } elseif ($status === 'WAIT_FOR_CONFIRMATION') {
            $label = 'Wait for Confirmation';
            $action = 'WAIT';
            $tone = 'caution';
            $confidence = 'LOW';
            $summary = $tradePlan['message'] ?? 'Directional bias exists, but the setup needs liquidity sweep, displacement, and confirmation before execution.';
        } elseif ($status === 'WATCHLIST') {
            $label = 'Watchlist';
            $action = 'WAIT';
            $tone = 'neutral';
            $confidence = 'LOW';
        } elseif ($status === 'MISSED_ENTRY') {
            $label = 'Missed Entry';
            $action = 'WAIT';
            $tone = 'caution';
            $confidence = 'LOW';
            $summary = $tradePlan['message'] ?? 'The entry has moved away from current price. Wait for a fresh setup.';
        }

        if (!empty($newsRisk['has_news']) && in_array($status, ['EXECUTION_READY','SELECTIVE_EXECUTION','WAIT_FOR_MITIGATION','WATCHLIST'], true)) {
            $label = $status === 'EXECUTION_READY' ? 'Execution Ready - News Caution' : $label;
            $tone = $tone === 'neutral' ? 'caution' : $tone;
            $summary = trim($summary . ' News caution is active; avoid entering during the release window.');
        }

        return [
            'label' => $label,
            'action' => $action,
            'tone' => $tone,
            'summary' => $summary,
            'confidence' => $confidence,
            'price_location' => $priceLocation,
            'institutional_state' => $aligned ? 'VALID TREND STRUCTURE' : 'PULLBACK / TRANSITION'
        ];
    }

    private static function buildMicroPullbackZone(array $candles, string $bias, array $style): ?array
    {
        if (count($candles) < 10 || !in_array($bias, ['BUY','SELL'], true)) return null;
        $recent = array_slice($candles, -12);
        $high = max(array_map(function ($c) { return (float)$c['high_price']; }, $recent));
        $low = min(array_map(function ($c) { return (float)$c['low_price']; }, $recent));
        $range = max($high - $low, 0.00001);

        if ($bias === 'BUY') {
            $entryLow = $low + ($range * 0.382);
            $entryHigh = $low + ($range * 0.618);
        } else {
            $entryLow = $high - ($range * 0.618);
            $entryHigh = $high - ($range * 0.382);
        }

        return [
            'type' => 'MICRO_PULLBACK',
            'source' => 'Structure + Liquidity Pullback Zone',
            'direction' => $bias,
            'entry_low' => min($entryLow, $entryHigh),
            'entry_high' => max($entryLow, $entryHigh),
            'time' => end($recent)['open_time'] ?? null,
            'poi_quality' => 1,
        ];
    }

    private static function detectLiquidityEvent(array $candles, array $liquidity, string $bias): array
    {
        if (count($candles) < 8) return ['confirmed' => false, 'narrative' => 'Not enough candles to confirm liquidity sweep.'];
        $recent = array_slice($candles, -5);
        $prev = array_slice($candles, -25, 20);
        $prevHigh = max(array_map(function ($c) { return (float)$c['high_price']; }, $prev));
        $prevLow = min(array_map(function ($c) { return (float)$c['low_price']; }, $prev));
        $last = end($candles);
        $lastClose = (float)$last['close_price'];

        foreach ($recent as $c) {
            $high = (float)$c['high_price'];
            $low = (float)$c['low_price'];
            if ($bias === 'BUY' && $low < $prevLow && $lastClose > $prevLow) {
                return ['confirmed' => true, 'type' => 'SELL_SIDE_SWEEP', 'narrative' => 'Sell-side liquidity sweep detected before bullish delivery.'];
            }
            if ($bias === 'SELL' && $high > $prevHigh && $lastClose < $prevHigh) {
                return ['confirmed' => true, 'type' => 'BUY_SIDE_SWEEP', 'narrative' => 'Buy-side liquidity sweep detected before bearish delivery.'];
            }
        }

        return ['confirmed' => false, 'type' => 'NONE', 'narrative' => 'No confirmed liquidity sweep yet.'];
    }

    private static function detectDisplacement(array $candles, string $bias): array
    {
        if (count($candles) < 10) return ['confirmed' => false, 'narrative' => 'Not enough candles to confirm displacement.'];
        $recent = array_slice($candles, -10);
        $bodies = [];
        foreach ($recent as $c) $bodies[] = abs((float)$c['close_price'] - (float)$c['open_price']);
        $avgBody = array_sum($bodies) / max(count($bodies), 1);
        $last = end($candles);
        $body = abs((float)$last['close_price'] - (float)$last['open_price']);
        $bull = (float)$last['close_price'] > (float)$last['open_price'];
        $bear = (float)$last['close_price'] < (float)$last['open_price'];

        if ($body >= ($avgBody * 1.4) && (($bias === 'BUY' && $bull) || ($bias === 'SELL' && $bear))) {
            return ['confirmed' => true, 'narrative' => 'Displacement candle confirms directional delivery.'];
        }
        return ['confirmed' => false, 'narrative' => 'No strong displacement confirmation yet.'];
    }

    private static function distanceFromZone(float $price, float $low, float $high): float
    {
        if ($price >= $low && $price <= $high) return 0.0;
        if ($price < $low) return $low - $price;
        return $price - $high;
    }

    private static function arrayIsFavorable(string $bias, string $location): bool
    {
        if ($bias === 'BUY') return in_array($location, ['DISCOUNT','EXTREME DISCOUNT','SLIGHT DISCOUNT'], true);
        if ($bias === 'SELL') return in_array($location, ['PREMIUM','EXTREME PREMIUM','SLIGHT PREMIUM'], true);
        return false;
    }

    private static function getHigherTimeframe(string $timeframe): string
    {
        $tf = strtoupper($timeframe);
        if ($tf === '1M') return '5M';
        if ($tf === '5M') return '15M';
        if ($tf === '15M') return '1H';
        if ($tf === '30M') return '4H';
        if ($tf === '1H') return '4H';
        if ($tf === '4H') return '1D';
        if ($tf === '1D') return '1W';
        if ($tf === '1W') return '1MN';
        return '4H';
    }

    private static function inferMomentumBias(array $candles): string
    {
        if (count($candles) < 5) 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 buildDealingRange(array $candles, array $style): array
    {
        if (empty($candles)) return ['range_high' => null, 'range_low' => null, 'equilibrium' => null, 'range_size' => 0];
        $key = strtolower($style['style_key'] ?? 'intraday');
        $lookback = $key === 'scalping' ? 24 : ($key === 'swing' ? 120 : ($key === 'auto' ? 60 : 48));
        $recent = array_slice($candles, -$lookback);
        $high = max(array_map(function ($c) { return (float)$c['high_price']; }, $recent));
        $low = min(array_map(function ($c) { return (float)$c['low_price']; }, $recent));
        $eq = ($high + $low) / 2;
        return [
            'range_high' => $high,
            'range_low' => $low,
            'equilibrium' => $eq,
            'premium_zone' => ['low' => $eq, 'high' => $high],
            'discount_zone' => ['low' => $low, 'high' => $eq],
            'range_size' => abs($high - $low),
            'lookback' => $lookback,
        ];
    }

    private static function getPriceLocation(float $price, array $range): string
    {
        $high = $range['range_high'] ?? null;
        $low = $range['range_low'] ?? null;

        if ($high === null || $low === null || $high <= $low) {
            return 'UNKNOWN';
        }

        $position = self::rangePosition($price, $high, $low);

        if ($position === null) {
            return 'UNKNOWN';
        }

        /*
        |--------------------------------------------------------------------------
        | Institutional premium / discount classification
        |--------------------------------------------------------------------------
        | 50% is equilibrium. A price slightly below 50% should not be displayed
        | as plain equilibrium because traders need to know whether price is on the
        | discount or premium side of the dealing range.
        */

        if ($position >= 75) {
            return 'EXTREME PREMIUM';
        }

        if ($position >= 55) {
            return 'PREMIUM';
        }

        if ($position > 50 && $position < 55) {
            return 'SLIGHT PREMIUM';
        }

        if ($position >= 48 && $position <= 52) {
            return 'EQUILIBRIUM';
        }

        if ($position > 45 && $position < 49.5) {
            return 'SLIGHT DISCOUNT';
        }

        if ($position <= 25) {
            return 'EXTREME DISCOUNT';
        }

        return 'DISCOUNT';
    }

    private static function rangePosition(?float $price, ?float $high, ?float $low): ?float
    {
        if ($price === null || $high === null || $low === null || $high <= $low) return null;
        return round((($price - $low) / ($high - $low)) * 100, 2);
    }

    private static function buildLiquidityMap(array $swings, array $candles, string $bias): array
    {
        $currentPrice = null;
        if (!empty($candles)) {
            $last = end($candles);
            $currentPrice = isset($last['close_price']) ? (float)$last['close_price'] : null;
        }

        $recentCandles = array_slice($candles, -80);
        $recentHigh = !empty($recentCandles) ? max(array_map(function ($c) { return (float)$c['high_price']; }, $recentCandles)) : null;
        $recentLow  = !empty($recentCandles) ? min(array_map(function ($c) { return (float)$c['low_price']; }, $recentCandles)) : null;

        $swingHighs = array_values(array_filter($swings, function ($s) use ($currentPrice) {
            return ($s['type'] ?? '') === 'HIGH' && ($currentPrice === null || (float)$s['price'] >= $currentPrice);
        }));
        $swingLows = array_values(array_filter($swings, function ($s) use ($currentPrice) {
            return ($s['type'] ?? '') === 'LOW' && ($currentPrice === null || (float)$s['price'] <= $currentPrice);
        }));

        usort($swingHighs, function ($a, $b) use ($currentPrice) {
            if ($currentPrice === null) return strtotime($b['time'] ?? 'now') <=> strtotime($a['time'] ?? 'now');
            return abs((float)$a['price'] - $currentPrice) <=> abs((float)$b['price'] - $currentPrice);
        });
        usort($swingLows, function ($a, $b) use ($currentPrice) {
            if ($currentPrice === null) return strtotime($b['time'] ?? 'now') <=> strtotime($a['time'] ?? 'now');
            return abs((float)$a['price'] - $currentPrice) <=> abs((float)$b['price'] - $currentPrice);
        });

        $buySide = !empty($swingHighs) ? (float)$swingHighs[0]['price'] : $recentHigh;
        $sellSide = !empty($swingLows) ? (float)$swingLows[0]['price'] : $recentLow;

        $sweep = self::detectRecentSweep($candles, $buySide, $sellSide);
        $inducement = self::detectInducement($candles, $bias, $currentPrice, $buySide, $sellSide);

        $drawSide = 'UNKNOWN';
        $drawTarget = null;
        if ($bias === 'BUY') {
            $drawSide = 'BUY SIDE';
            $drawTarget = $buySide;
        } elseif ($bias === 'SELL') {
            $drawSide = 'SELL SIDE';
            $drawTarget = $sellSide;
        }

        $buySideDistance = ($currentPrice !== null && $buySide !== null) ? abs($buySide - $currentPrice) : null;
        $sellSideDistance = ($currentPrice !== null && $sellSide !== null) ? abs($currentPrice - $sellSide) : null;
        $drawDistance = ($currentPrice !== null && $drawTarget !== null) ? abs($drawTarget - $currentPrice) : null;

        return [
            'buy_side' => $buySide,
            'sell_side' => $sellSide,
            'nearest_buy_side' => $buySide,
            'nearest_sell_side' => $sellSide,
            'buy_side_distance' => $buySideDistance,
            'sell_side_distance' => $sellSideDistance,
            'recent_high' => $recentHigh,
            'recent_low' => $recentLow,
            'sweep' => $sweep,
            'inducement' => $inducement,
            'draw_on_liquidity' => [
                'side' => $drawSide,
                'target' => $drawTarget,
                'distance' => $drawDistance,
                'narrative' => $drawSide === 'BUY SIDE'
                    ? 'Primary draw is toward nearest buy-side liquidity above price.'
                    : ($drawSide === 'SELL SIDE' ? 'Primary draw is toward nearest sell-side liquidity below price.' : 'No clear draw on liquidity.')
            ],
            'summary' => 'Liquidity map uses nearest unbroken swing liquidity plus recent range extremes, not only the last swing.'
        ];
    }

    private static function detectRecentSweep(array $candles, ?float $buySide, ?float $sellSide): array
    {
        if (count($candles) < 8) {
            return ['confirmed' => false, 'type' => 'NONE', 'narrative' => 'Not enough candles to confirm liquidity sweep.'];
        }

        $recent = array_slice($candles, -5);
        $last = end($candles);
        $lastClose = (float)($last['close_price'] ?? 0);

        foreach ($recent as $c) {
            $high = (float)$c['high_price'];
            $low = (float)$c['low_price'];

            if ($buySide !== null && $high > $buySide && $lastClose < $buySide) {
                return ['confirmed' => true, 'type' => 'BUY_SIDE_SWEEP', 'narrative' => 'Buy-side liquidity was swept and price closed back below it.'];
            }

            if ($sellSide !== null && $low < $sellSide && $lastClose > $sellSide) {
                return ['confirmed' => true, 'type' => 'SELL_SIDE_SWEEP', 'narrative' => 'Sell-side liquidity was swept and price closed back above it.'];
            }
        }

        return ['confirmed' => false, 'type' => 'NONE', 'narrative' => 'No recent liquidity sweep confirmed.'];
    }

    private static function detectInducement(array $candles, string $bias, ?float $currentPrice, ?float $buySide, ?float $sellSide): array
    {
        if ($currentPrice === null || count($candles) < 12) {
            return ['confirmed' => false, 'narrative' => 'Not enough data to identify inducement.'];
        }

        $recent = array_slice($candles, -12);
        $minorHigh = max(array_map(function ($c) { return (float)$c['high_price']; }, $recent));
        $minorLow = min(array_map(function ($c) { return (float)$c['low_price']; }, $recent));

        if ($bias === 'BUY' && $sellSide !== null && $minorLow > $sellSide && $minorLow < $currentPrice) {
            return ['confirmed' => true, 'level' => $minorLow, 'narrative' => 'Possible sell-side inducement below recent minor lows before bullish continuation.'];
        }

        if ($bias === 'SELL' && $buySide !== null && $minorHigh < $buySide && $minorHigh > $currentPrice) {
            return ['confirmed' => true, 'level' => $minorHigh, 'narrative' => 'Possible buy-side inducement above recent minor highs before bearish continuation.'];
        }

        return ['confirmed' => false, 'narrative' => 'No clear inducement level near current price.'];
    }

    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 classifyVolatility(string $symbol, float $atr): string
    {
        $symbol = strtoupper($symbol);
        if ($symbol === 'XAUUSD') {
            if ($atr >= 8) return 'EXTREME'; if ($atr >= 4) return 'HIGH'; if ($atr >= 1.5) return 'NORMAL'; return 'LOW';
        }
        if (strpos($symbol, 'JPY') !== false) {
            if ($atr >= 0.80) return 'EXTREME'; if ($atr >= 0.40) return 'HIGH'; if ($atr >= 0.12) return 'NORMAL'; return 'LOW';
        }
        if ($atr >= 0.0080) return 'EXTREME'; if ($atr >= 0.0040) return 'HIGH'; if ($atr >= 0.0012) return 'NORMAL'; return 'LOW';
    }

    private static function arrayAdvantage(string $bias, string $location): array
    {
        $favorable = self::arrayIsFavorable($bias, $location);
        $message = $favorable ? ($bias . ' setup has favorable institutional pricing.') : ($bias . ' setup is not in ideal premium/discount pricing.');
        if (!in_array($bias, ['BUY','SELL'], true)) $message = 'Neutral institutional pricing.';
        return ['favorable' => $favorable, 'message' => $message];
    }

    private static function zonesOverlap(?array $fvg, ?array $ob): bool
    {
        if (!$fvg || !$ob) return false;
        $low = max((float)$fvg['low'], (float)$ob['zone_low']);
        $high = min((float)$fvg['high'], (float)$ob['zone_high']);
        return $low <= $high;
    }

    private static function confluenceZone(?array $fvg, ?array $ob): ?array
    {
        if (!self::zonesOverlap($fvg, $ob)) return null;
        return [
            'low' => max((float)$fvg['low'], (float)$ob['zone_low']),
            'high' => min((float)$fvg['high'], (float)$ob['zone_high']),
            'narrative' => 'Order Block and FVG overlap confirmed.'
        ];
    }

    private static function latestZoneByTime(array $zones): ?array
    {
        if (empty($zones)) return null;
        usort($zones, function ($a, $b) {
            return strtotime($b['time'] ?? '1970-01-01') <=> strtotime($a['time'] ?? '1970-01-01');
        });
        return $zones[0] ?? null;
    }

    private static function selectActiveOrderBlock(string $symbol, array $orderBlocks, string $bias, float $currentPrice, array $style): ?array
    {
        if (empty($orderBlocks)) return null;

        $maxDistance = TradingStyleService::maxEntryDistance($symbol, $style);
        $filtered = array_values(array_filter($orderBlocks, function ($ob) use ($bias) {
            if (($ob['mitigated'] ?? false) === true) return false;
            return $bias === 'WAIT' || ($ob['direction'] ?? '') === $bias;
        }));

        if (empty($filtered)) {
            $filtered = array_values(array_filter($orderBlocks, function ($ob) use ($bias) {
                return $bias === 'WAIT' || ($ob['direction'] ?? '') === $bias;
            }));
        }

        if (empty($filtered)) return null;

        usort($filtered, function ($a, $b) use ($currentPrice, $maxDistance) {
            $aLow = (float)($a['zone_low'] ?? 0); $aHigh = (float)($a['zone_high'] ?? 0);
            $bLow = (float)($b['zone_low'] ?? 0); $bHigh = (float)($b['zone_high'] ?? 0);
            $daZone = self::distanceFromZone($currentPrice, $aLow, $aHigh);
            $dbZone = self::distanceFromZone($currentPrice, $bLow, $bHigh);
            $daMid = self::midpointDistance($currentPrice, $aLow, $aHigh);
            $dbMid = self::midpointDistance($currentPrice, $bLow, $bHigh);
            $aNear = $daZone <= $maxDistance ? 1 : 0;
            $bNear = $dbZone <= $maxDistance ? 1 : 0;
            if ($aNear !== $bNear) return $bNear <=> $aNear;

            // Prefer the nearest useful POI first. If price is inside two wide zones,
            // midpoint distance prevents an old wide OB from always winning with 0.00.
            if (abs($daZone - $dbZone) > 0.0000001) return $daZone <=> $dbZone;
            if (abs($daMid - $dbMid) > 0.0000001) return $daMid <=> $dbMid;

            $qa = (int)($a['quality'] ?? 1); $qb = (int)($b['quality'] ?? 1);
            if ($qa !== $qb) return $qb <=> $qa;
            return strtotime($b['time'] ?? '1970-01-01') <=> strtotime($a['time'] ?? '1970-01-01');
        });

        $chosen = $filtered[0] ?? null;
        if ($chosen) {
            $low = (float)$chosen['zone_low'];
            $high = (float)$chosen['zone_high'];
            $chosen['zone_distance_from_price'] = self::distanceFromZone($currentPrice, $low, $high);
            $chosen['midpoint_distance_from_price'] = self::midpointDistance($currentPrice, $low, $high);
            $chosen['display_distance'] = $chosen['midpoint_distance_from_price'];
            $chosen['selection_note'] = 'Selected by proximity to current price, freshness, quality, and bias alignment.';
        }
        return $chosen;
    }

    private static function selectActiveFVG(string $symbol, array $fvgZones, string $bias, float $currentPrice, array $style): ?array
    {
        if (empty($fvgZones)) return null;

        $maxDistance = TradingStyleService::maxEntryDistance($symbol, $style);

        // For dashboard display, the most useful active imbalance is the nearest
        // unfilled imbalance near price. Candidate building still requires direction
        // alignment before it can become an execution source.
        $filtered = array_values(array_filter($fvgZones, function ($fvg) {
            return ($fvg['filled'] ?? false) !== true;
        }));

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

        usort($filtered, function ($a, $b) use ($currentPrice, $maxDistance, $bias) {
            $aLow = (float)($a['low'] ?? 0); $aHigh = (float)($a['high'] ?? 0);
            $bLow = (float)($b['low'] ?? 0); $bHigh = (float)($b['high'] ?? 0);
            $daZone = self::distanceFromZone($currentPrice, $aLow, $aHigh);
            $dbZone = self::distanceFromZone($currentPrice, $bLow, $bHigh);
            $daMid = self::midpointDistance($currentPrice, $aLow, $aHigh);
            $dbMid = self::midpointDistance($currentPrice, $bLow, $bHigh);
            $aNear = $daZone <= $maxDistance ? 1 : 0;
            $bNear = $dbZone <= $maxDistance ? 1 : 0;
            if ($aNear !== $bNear) return $bNear <=> $aNear;

            // Direction-aligned imbalances are slightly preferred only when distance is similar.
            $aAligned = ($bias === 'WAIT' || ($a['direction'] ?? '') === $bias) ? 1 : 0;
            $bAligned = ($bias === 'WAIT' || ($b['direction'] ?? '') === $bias) ? 1 : 0;
            if (abs($daZone - $dbZone) < 0.0000001 && $aAligned !== $bAligned) return $bAligned <=> $aAligned;

            if (abs($daZone - $dbZone) > 0.0000001) return $daZone <=> $dbZone;
            if (abs($daMid - $dbMid) > 0.0000001) return $daMid <=> $dbMid;
            $qa = (int)($a['quality'] ?? 1); $qb = (int)($b['quality'] ?? 1);
            if ($qa !== $qb) return $qb <=> $qa;
            return strtotime($b['time'] ?? '1970-01-01') <=> strtotime($a['time'] ?? '1970-01-01');
        });

        $chosen = $filtered[0] ?? null;
        if ($chosen) {
            $low = (float)$chosen['low'];
            $high = (float)$chosen['high'];
            $chosen['zone_distance_from_price'] = self::distanceFromZone($currentPrice, $low, $high);
            $chosen['midpoint_distance_from_price'] = self::midpointDistance($currentPrice, $low, $high);
            $chosen['display_distance'] = $chosen['midpoint_distance_from_price'];
            $chosen['selection_note'] = 'Selected by proximity to current price, freshness, quality, and imbalance status.';
        }
        return $chosen;
    }

    private static function midpointDistance(float $price, float $low, float $high): float
    {
        if ($low > $high) { $tmp = $low; $low = $high; $high = $tmp; }
        return abs($price - (($low + $high) / 2));
    }

    private static function latestSwingHighLow(array $swings): array
    {
        $swingHigh = null;
        $swingLow = null;

        for ($i = count($swings) - 1; $i >= 0; $i--) {
            if ($swingHigh === null && ($swings[$i]['type'] ?? '') === 'HIGH') {
                $swingHigh = (float)$swings[$i]['price'];
            }

            if ($swingLow === null && ($swings[$i]['type'] ?? '') === 'LOW') {
                $swingLow = (float)$swings[$i]['price'];
            }

            if ($swingHigh !== null && $swingLow !== null) {
                break;
            }
        }

        return [
            'swing_high' => $swingHigh,
            'swing_low' => $swingLow,
        ];
    }

    private static function buildNarrative(string $symbol, string $timeframe, string $contextTimeframe, string $executionBias, string $contextBias, string $priceLocation, array $tradePlan, array $decision, array $liquidityEvent, array $displacement): string
    {
        $parts = [];
        $parts[] = $symbol . ' ' . $timeframe . ' is using ' . $contextTimeframe . ' higher-timeframe context.';

        if ($executionBias === $contextBias && in_array($executionBias, ['BUY', 'SELL'], true)) {
            $parts[] = 'Higher-timeframe and execution structure are aligned to ' . $executionBias . '.';
        } else {
            $parts[] = 'Execution bias is ' . $executionBias . ' while higher-timeframe bias is ' . $contextBias . ', so confirmation quality matters.';
        }

        if ($priceLocation === 'EXTREME PREMIUM' && $executionBias === 'BUY') {
            $parts[] = 'Price is trading in extreme premium; fresh longs require a sweep, displacement, or retracement into a better institutional zone.';
        } elseif ($priceLocation === 'EXTREME DISCOUNT' && $executionBias === 'SELL') {
            $parts[] = 'Price is trading in extreme discount; fresh shorts require a sweep, displacement, or retracement into a better institutional zone.';
        } else {
            $parts[] = 'Price location is ' . $priceLocation . '.';
        }

        if (!empty($liquidityEvent['confirmed'])) {
            $parts[] = $liquidityEvent['narrative'] ?? 'Liquidity event confirmed.';
        } else {
            $parts[] = 'No confirmed liquidity sweep yet.';
        }

        if (!empty($displacement['confirmed'])) {
            $parts[] = $displacement['narrative'] ?? 'Displacement confirmed.';
        } else {
            $parts[] = 'No strong displacement confirmation yet.';
        }

        if (!empty($tradePlan['entry_source']) && ($tradePlan['entry_source'] ?? 'N/A') !== 'N/A') {
            $parts[] = 'Entry source: ' . $tradePlan['entry_source'] . '.';
        }

        if (!empty($tradePlan['execution_mode'])) {
            $parts[] = 'Execution mode: ' . $tradePlan['execution_mode'] . '.';
        }

        if (!empty($tradePlan['execution_state'])) {
            $parts[] = 'Execution state: ' . str_replace('_', ' ', $tradePlan['execution_state']) . '.';
        }

        if (!empty($tradePlan['lifecycle_stage'])) {
            $parts[] = 'Setup lifecycle: ' . $tradePlan['lifecycle_stage'] . '.';
        }

        if (($tradePlan['status'] ?? '') === 'SELECTIVE_EXECUTION') {
            $parts[] = 'Volatility is extreme, so reduced risk and extra confirmation are required.';
        } elseif (($tradePlan['status'] ?? '') === 'NO_VALID_SETUP') {
            $parts[] = 'No executable setup is available until price forms a nearer validated zone.';
        } elseif (($tradePlan['status'] ?? '') === 'WAIT_FOR_CONFIRMATION') {
            $parts[] = 'Wait for lower-timeframe confirmation before execution.';
        } elseif (!empty($tradePlan['message'])) {
            $parts[] = $tradePlan['message'];
        }

        $clean = [];
        foreach ($parts as $part) {
            $part = trim((string)$part);
            if ($part === '') continue;
            $key = strtolower(preg_replace('/\s+/', ' ', $part));
            $clean[$key] = $part;
        }

        return implode(' ', array_values($clean));
    }




    private static function recentCandles(array $candles, int $count = 8): array
    {
        return array_slice($candles, -$count);
    }

    private static function candleBody(array $candle): float
    {
        return abs((float)($candle['close_price'] ?? 0) - (float)($candle['open_price'] ?? 0));
    }

    private static function candleRange(array $candle): float
    {
        return max(0.00000001, (float)($candle['high_price'] ?? 0) - (float)($candle['low_price'] ?? 0));
    }

    private static function candleDirection(array $candle): string
    {
        $open = (float)($candle['open_price'] ?? 0);
        $close = (float)($candle['close_price'] ?? 0);

        if ($close > $open) return 'BUY';
        if ($close < $open) return 'SELL';

        return 'NEUTRAL';
    }

    private static function detectMitigationState(array $candles, ?array $candidate, float $currentPrice): array
    {
        if (!$candidate || empty($candidate['entry_low']) || empty($candidate['entry_high'])) {
            return [
                'state' => 'NO_ZONE',
                'confirmed' => false,
                'partial' => false,
                'label' => 'No mitigation zone',
                'message' => 'No valid mitigation zone is available yet.',
                'score_modifier' => -0.75,
            ];
        }

        $low = min((float)$candidate['entry_low'], (float)$candidate['entry_high']);
        $high = max((float)$candidate['entry_low'], (float)$candidate['entry_high']);
        $zoneSize = max(0.00000001, $high - $low);
        $proximityBuffer = $zoneSize * 0.15;
        $recent = self::recentCandles($candles, 12);

        $insideNow = ($currentPrice >= $low && $currentPrice <= $high);
        $aboveZone = $currentPrice > $high;
        $belowZone = $currentPrice < $low;
        $nearZone = (!$insideNow && abs($currentPrice - ($aboveZone ? $high : $low)) <= $proximityBuffer);

        $touched = false;
        $closedInside = false;
        $rejected = false;
        $direction = strtoupper((string)($candidate['direction'] ?? 'WAIT'));

        foreach ($recent as $candle) {
            $cLow = (float)($candle['low_price'] ?? 0);
            $cHigh = (float)($candle['high_price'] ?? 0);
            $cClose = (float)($candle['close_price'] ?? 0);

            if ($cHigh >= $low && $cLow <= $high) {
                $touched = true;
            }

            if ($cClose >= $low && $cClose <= $high) {
                $closedInside = true;
            }
        }

        $last = !empty($recent) ? end($recent) : null;

        if ($last) {
            $lastDirection = self::candleDirection($last);
            $close = (float)($last['close_price'] ?? 0);

            if ($direction === 'BUY' && $touched && $lastDirection === 'BUY' && $close > $low) {
                $rejected = true;
            }

            if ($direction === 'SELL' && $touched && $lastDirection === 'SELL' && $close < $high) {
                $rejected = true;
            }
        }

        if ($rejected) {
            return [
                'state' => 'MITIGATION_REACTION',
                'confirmed' => true,
                'partial' => true,
                'label' => 'Mitigation Reaction',
                'message' => 'Price has mitigated the institutional zone and reacted in the expected direction.',
                'score_modifier' => 1.25,
            ];
        }

        if ($insideNow) {
            return [
                'state' => 'INSIDE_MITIGATION',
                'confirmed' => false,
                'partial' => true,
                'label' => 'Inside Mitigation Zone',
                'message' => 'Price is currently inside the mitigation zone. Wait for rejection, displacement, or candle trigger.',
                'score_modifier' => 0.35,
            ];
        }

        if ($nearZone) {
            return [
                'state' => 'APPROACHING_ZONE',
                'confirmed' => false,
                'partial' => false,
                'label' => 'Approaching Mitigation Zone',
                'message' => $aboveZone
                    ? 'Price is slightly above the institutional zone. Wait for mitigation re-entry.'
                    : 'Price is slightly below the institutional zone. Wait for mitigation re-entry.',
                'score_modifier' => -0.10,
            ];
        }

        if ($touched || $closedInside) {
            return [
                'state' => 'ZONE_TAPPED',
                'confirmed' => false,
                'partial' => true,
                'label' => 'Zone Previously Tapped',
                'message' => 'Price has interacted with the zone before, but current price is not inside it now.',
                'score_modifier' => 0.10,
            ];
        }

        return [
            'state' => 'AWAITING_MITIGATION',
            'confirmed' => false,
            'partial' => false,
            'label' => 'Awaiting Mitigation',
            'message' => 'Price has not properly mitigated the institutional zone yet.',
            'score_modifier' => -0.75,
        ];
    }

    private static function detectDisplacementConfirmation(array $candles, string $direction, float $atr = 0): array
    {
        $recent = self::recentCandles($candles, 6);

        if (count($recent) < 3) {
            return [
                'confirmed' => false,
                'label' => 'No displacement confirmation',
                'message' => 'Not enough candles to confirm displacement.',
                'score_modifier' => 0,
            ];
        }

        $last = end($recent);
        $body = self::candleBody($last);
        $range = self::candleRange($last);
        $bodyRatio = $range > 0 ? ($body / $range) : 0;
        $lastDirection = self::candleDirection($last);

        $avgBody = 0;
        foreach (array_slice($recent, 0, -1) as $candle) {
            $avgBody += self::candleBody($candle);
        }
        $avgBody = $avgBody / max(1, count($recent) - 1);

        $atrCheck = $atr > 0 ? $body >= ($atr * 0.25) : true;
        $bodyExpansion = $avgBody > 0 ? $body >= ($avgBody * 1.35) : true;

        if ($lastDirection === $direction && $bodyRatio >= 0.55 && $bodyExpansion && $atrCheck) {
            return [
                'confirmed' => true,
                'label' => 'Displacement Confirmed',
                'message' => 'A strong directional candle confirms institutional displacement.',
                'score_modifier' => 1.5,
                'body_ratio' => round($bodyRatio * 100, 1),
            ];
        }

        return [
            'confirmed' => false,
            'label' => 'Displacement Pending',
            'message' => 'No strong displacement candle has confirmed execution yet.',
            'score_modifier' => -0.25,
            'body_ratio' => round($bodyRatio * 100, 1),
        ];
    }

    private static function detectLiveLiquiditySweep(array $candles, string $direction): array
    {
        $recent = self::recentCandles($candles, 12);

        if (count($recent) < 5) {
            return [
                'confirmed' => false,
                'label' => 'No sweep confirmation',
                'message' => 'Not enough candles to confirm liquidity sweep.',
                'score_modifier' => 0,
            ];
        }

        $last = end($recent);
        $previous = array_slice($recent, 0, -1);

        $prevHigh = max(array_map(fn($c) => (float)($c['high_price'] ?? 0), $previous));
        $prevLow = min(array_map(fn($c) => (float)($c['low_price'] ?? 0), $previous));

        $lastHigh = (float)($last['high_price'] ?? 0);
        $lastLow = (float)($last['low_price'] ?? 0);
        $lastClose = (float)($last['close_price'] ?? 0);

        if ($direction === 'BUY' && $lastLow < $prevLow && $lastClose > $prevLow) {
            return [
                'confirmed' => true,
                'side' => 'SELL_SIDE',
                'label' => 'Sell-side Liquidity Swept',
                'message' => 'Sell-side liquidity was swept and price closed back above it.',
                'score_modifier' => 1.25,
                'level' => $prevLow,
            ];
        }

        if ($direction === 'SELL' && $lastHigh > $prevHigh && $lastClose < $prevHigh) {
            return [
                'confirmed' => true,
                'side' => 'BUY_SIDE',
                'label' => 'Buy-side Liquidity Swept',
                'message' => 'Buy-side liquidity was swept and price closed back below it.',
                'score_modifier' => 1.25,
                'level' => $prevHigh,
            ];
        }

        return [
            'confirmed' => false,
            'side' => null,
            'label' => 'Liquidity Sweep Pending',
            'message' => 'No fresh liquidity sweep confirmation detected.',
            'score_modifier' => 0,
            'level' => null,
        ];
    }

    private static function detectCandleConfirmation(array $candles, string $direction, ?array $candidate): array
    {
        $recent = self::recentCandles($candles, 4);

        if (count($recent) < 2) {
            return [
                'confirmed' => false,
                'label' => 'Confirmation pending',
                'message' => 'Not enough candles to confirm entry.',
                'score_modifier' => 0,
            ];
        }

        $last = end($recent);
        $prev = $recent[count($recent) - 2];

        $lastDirection = self::candleDirection($last);
        $bodyRatio = self::candleBody($last) / self::candleRange($last);

        $lastClose = (float)($last['close_price'] ?? 0);
        $prevHigh = (float)($prev['high_price'] ?? 0);
        $prevLow = (float)($prev['low_price'] ?? 0);

        if ($direction === 'BUY' && $lastDirection === 'BUY' && $lastClose > $prevHigh && $bodyRatio >= 0.45) {
            return [
                'confirmed' => true,
                'label' => 'Bullish confirmation',
                'message' => 'Bullish confirmation candle closed above the previous high.',
                'score_modifier' => 1.25,
            ];
        }

        if ($direction === 'SELL' && $lastDirection === 'SELL' && $lastClose < $prevLow && $bodyRatio >= 0.45) {
            return [
                'confirmed' => true,
                'label' => 'Bearish confirmation',
                'message' => 'Bearish confirmation candle closed below the previous low.',
                'score_modifier' => 1.25,
            ];
        }

        return [
            'confirmed' => false,
            'label' => 'Confirmation pending',
            'message' => 'No candle confirmation has validated execution yet.',
            'score_modifier' => -0.25,
        ];
    }

    private static function swingBasedDealingRange(array $candles, array $structureEvents = []): array
    {
        $recent = array_slice($candles, -120);
        if (count($recent) < 20) {
            return [];
        }

        $swingHigh = null;
        $swingLow = null;

        foreach ($recent as $candle) {
            $high = (float)($candle['high_price'] ?? 0);
            $low = (float)($candle['low_price'] ?? 0);

            if ($swingHigh === null || $high > $swingHigh) {
                $swingHigh = $high;
            }

            if ($swingLow === null || $low < $swingLow) {
                $swingLow = $low;
            }
        }

        if ($swingHigh === null || $swingLow === null || $swingHigh <= $swingLow) {
            return [];
        }

        return [
            'range_high' => $swingHigh,
            'range_low' => $swingLow,
            'equilibrium' => ($swingHigh + $swingLow) / 2,
            'source' => 'swing_dealing_range',
        ];
    }


    private static function institutionalScoreCap(array $tradePlan): array
    {
        $confirmationCount = (int)($tradePlan['confirmation_count'] ?? 0);
        $partialCount = (int)($tradePlan['partial_confirmation_count'] ?? 0);
        $stage = strtoupper((string)($tradePlan['confirmation_stage'] ?? 'CONFIRMATION_PENDING'));
        $status = strtoupper((string)($tradePlan['status'] ?? ''));
        $locationClass = strtoupper((string)($tradePlan['location_execution_class'] ?? ''));
        $probability = (float)($tradePlan['probability_score'] ?? 0);
        $score = (float)($tradePlan['score'] ?? 0);

        /*
        |--------------------------------------------------------------------------
        | Institutional scoring caps
        |--------------------------------------------------------------------------
        | A setup can be structurally attractive, but it cannot be rated A+/10
        | until execution confirmation exists.
        |--------------------------------------------------------------------------
        */

        $cap = 10.0;

        if (in_array($status, ['INVALID', 'MISSED_ENTRY', 'NO_VALID_SETUP', 'NO_DATA', 'NO DATA'], true)) {
            $cap = 3.0;
        } elseif ($confirmationCount === 0 && $partialCount === 0) {
            $cap = 5.8;
        } elseif ($confirmationCount === 0 && $partialCount > 0) {
            $cap = 6.8;
        } elseif ($confirmationCount === 1) {
            $cap = 7.4;
        } elseif ($confirmationCount === 2) {
            $cap = 8.4;
        } elseif ($confirmationCount >= 3) {
            $cap = 9.4;
        }

        if ($locationClass === 'CAUTION') {
            $cap = min($cap, 5.8);
        } elseif ($locationClass === 'REDUCED_RR') {
            $cap = min($cap, 7.2);
        }

        if ($stage !== 'EXECUTION_CONFIRMED') {
            $cap = min($cap, 7.2);
        }

        $score = min($score, $cap);

        $confidence = 'LOW';
        if ($score >= 8.5 && $confirmationCount >= 3 && $probability >= 75) {
            $confidence = 'VERY HIGH';
        } elseif ($score >= 7.2 && $confirmationCount >= 2 && $probability >= 65) {
            $confidence = 'HIGH';
        } elseif ($score >= 5.2 || $partialCount > 0) {
            $confidence = 'MEDIUM';
        } elseif ($score >= 4.0) {
            $confidence = 'LOW';
        } else {
            $confidence = 'VERY LOW';
        }

        $readiness = 'Watchlist';
        if (in_array($status, ['INVALID', 'MISSED_ENTRY', 'NO_VALID_SETUP', 'NO_DATA', 'NO DATA'], true)) {
            $readiness = 'Waitlist';
        } elseif ($stage === 'EXECUTION_CONFIRMED' && $score >= 8.5) {
            $readiness = 'A Setup';
        } elseif ($confirmationCount >= 2 && $score >= 7.0) {
            $readiness = 'B+ Setup';
        } elseif ($confirmationCount >= 1 || $partialCount > 0) {
            $readiness = 'Developing Setup';
        } else {
            $readiness = 'Watchlist';
        }

        $tradePlan['score'] = round(max(0, min(10, $score)), 2);
        $tradePlan['institutional_score_cap'] = $cap;
        $tradePlan['institutional_confidence'] = $confidence;
        $tradePlan['institutional_readiness'] = $readiness;

        return $tradePlan;
    }


    private static function phase3ConfirmationLayer(
        array $candles,
        string $direction,
        ?array $candidate,
        float $currentPrice,
        float $atr,
        array $tradePlan
    ): array {
        $mitigation = self::detectMitigationState($candles, $candidate, $currentPrice);
        $displacement = self::detectDisplacementConfirmation($candles, $direction, $atr);
        $liquiditySweep = self::detectLiveLiquiditySweep($candles, $direction);
        $candleConfirmation = self::detectCandleConfirmation($candles, $direction, $candidate);

        /*
        |--------------------------------------------------------------------------
        | Balanced confirmation model
        |--------------------------------------------------------------------------
        | Confirmed checks count fully; partial mitigation counts as progress, not
        | execution confirmation. This prevents "inside zone" from contradicting
        | the current price and stops over-scoring setups with no trigger.
        |--------------------------------------------------------------------------
        */

        $confirmationCount = 0;
        $partialCount = 0;

        foreach ([$mitigation, $displacement, $liquiditySweep, $candleConfirmation] as $item) {
            if (!empty($item['confirmed'])) {
                $confirmationCount++;
            } elseif (!empty($item['partial'])) {
                $partialCount++;
            }
        }

        $rawModifier =
            (float)($mitigation['score_modifier'] ?? 0) +
            (float)($displacement['score_modifier'] ?? 0) +
            (float)($liquiditySweep['score_modifier'] ?? 0) +
            (float)($candleConfirmation['score_modifier'] ?? 0);

        // Penalize excellent-looking but unconfirmed zones so rankings do not become too perfect.
        $confirmationPenalty = 0;
        if ($confirmationCount === 0) {
            $confirmationPenalty = -1.75;
        } elseif ($confirmationCount === 1) {
            $confirmationPenalty = -0.75;
        } elseif ($confirmationCount === 2) {
            $confirmationPenalty = -0.15;
        }

        $scoreModifier = $rawModifier + $confirmationPenalty;

        $stage = 'CONFIRMATION_PENDING';
        $readiness = 'Wait for confirmation';
        $guidance = 'Execution zone exists, but confirmation is still incomplete.';
        $triggerState = 'PENDING_TRIGGER';
        $invalidationState = 'ACTIVE';

        if ($confirmationCount >= 3) {
            $stage = 'EXECUTION_CONFIRMED';
            $readiness = 'Execution Ready';
            $guidance = 'Mitigation, liquidity, and candle confirmation are aligned.';
            $triggerState = 'TRIGGERED';
        } elseif ($confirmationCount === 2) {
            $stage = 'PARTIAL_CONFIRMATION';
            $readiness = 'Selective Execution';
            $guidance = 'Some confirmation is present. Use reduced risk and wait for cleaner reaction if uncertain.';
            $triggerState = 'PARTIAL_TRIGGER';
        } elseif ($confirmationCount === 1 || $partialCount > 0) {
            $stage = 'EARLY_CONFIRMATION';
            $readiness = 'Developing Setup';
            $guidance = 'Early confirmation is forming, but the setup is not fully validated yet.';
            $triggerState = 'DEVELOPING';
        }

        if (in_array(strtoupper((string)($tradePlan['status'] ?? '')), ['INVALID', 'MISSED_ENTRY', 'NO_VALID_SETUP'], true)) {
            $invalidationState = 'INACTIVE';
        }

        $tradePlan['phase3'] = [
            'stage' => $stage,
            'readiness' => $readiness,
            'guidance' => $guidance,
            'score_modifier' => round($scoreModifier, 2),
            'confirmation_count' => $confirmationCount,
            'partial_count' => $partialCount,
            'trigger_state' => $triggerState,
            'invalidation_state' => $invalidationState,
            'mitigation' => $mitigation,
            'displacement' => $displacement,
            'liquidity_sweep' => $liquiditySweep,
            'candle_confirmation' => $candleConfirmation,
        ];

        if (!in_array(strtoupper((string)($tradePlan['status'] ?? '')), ['INVALID', 'MISSED_ENTRY', 'NO_VALID_SETUP'], true)) {
            $tradePlan['score'] = round(max(0, min(15, (float)($tradePlan['score'] ?? 0) + $scoreModifier)), 2);
        }

        /*
        |--------------------------------------------------------------------------
        | True execution trigger logic
        |--------------------------------------------------------------------------
        | A setup can be high quality while still waiting. Execution Ready requires
        | real confirmation, not merely alignment or a valid zone.
        |--------------------------------------------------------------------------
        */

        if ($stage === 'EXECUTION_CONFIRMED' && in_array(($tradePlan['status'] ?? ''), ['WAIT_FOR_CONFIRMATION', 'WATCHLIST', 'WAIT_FOR_MITIGATION'], true)) {
            $tradePlan['status'] = 'EXECUTION_READY';
            $tradePlan['display_status'] = 'Execution Ready';
            $tradePlan['message'] = 'Institutional confirmation is active. ' . $guidance;
        } elseif ($stage !== 'EXECUTION_CONFIRMED' && in_array(($tradePlan['status'] ?? ''), ['EXECUTION_READY'], true)) {
            $tradePlan['status'] = 'WAIT_FOR_CONFIRMATION';
            $tradePlan['display_status'] = 'Wait for Confirmation';
            $tradePlan['message'] = $guidance;
        }

        /*
        |--------------------------------------------------------------------------
        | Probabilistic confidence model
        |--------------------------------------------------------------------------
        */

        $baseScore = (float)($tradePlan['score'] ?? 0);
        $alignmentStrength = (float)($tradePlan['alignment_strength'] ?? 50);
        $locationQuality = (float)($tradePlan['location_quality_score'] ?? 50);

        $probability = (
            ($baseScore / 15) * 35 +
            ($alignmentStrength / 100) * 25 +
            ($locationQuality / 100) * 20 +
            ($confirmationCount / 4) * 20
        );

        $tradePlan['probability_score'] = round(max(0, min(100, $probability)), 1);
        $tradePlan['confirmation_stage'] = $stage;
        $tradePlan['confirmation_readiness'] = $readiness;
        $tradePlan['confirmation_guidance'] = $guidance;
        $tradePlan['confirmation_count'] = $confirmationCount;
        $tradePlan['partial_confirmation_count'] = $partialCount;
        $tradePlan['trigger_state'] = $triggerState;
        $tradePlan['invalidation_state'] = $invalidationState;

        return $tradePlan;
    }


    private static function locationQuality(
        string $direction,
        string $priceLocation,
        bool $aligned,
        array $liquidityEvent,
        array $displacement
    ): array {
        $direction = strtoupper($direction);
        $location = strtoupper($priceLocation);

        $hasLiquidity = !empty($liquidityEvent['confirmed']);
        $hasDisplacement = !empty($displacement['confirmed']);
        $hasContinuationEvidence = $aligned && ($hasLiquidity || $hasDisplacement);

        $score = 50;
        $label = 'Neutral Location';
        $state = 'NEUTRAL';
        $guidance = 'Price is around equilibrium. Wait for confirmation before execution.';
        $executionClass = 'WATCHLIST';

        if ($direction === 'BUY') {
            if (in_array($location, ['EXTREME DISCOUNT', 'DISCOUNT', 'SLIGHT DISCOUNT'], true)) {
                $score = $location === 'EXTREME DISCOUNT' ? 95 : 85;
                $label = 'High-Quality Buy Location';
                $state = 'IDEAL_BUY_LOCATION';
                $guidance = 'BUY idea is forming from discount. This is a stronger institutional location.';
                $executionClass = 'A_SETUP';
            } elseif ($location === 'EQUILIBRIUM') {
                $score = 55;
                $label = 'Neutral Buy Location';
                $state = 'NEUTRAL_BUY_LOCATION';
                $guidance = 'BUY idea is near equilibrium. Require confirmation before execution.';
                $executionClass = 'B_SETUP';
            } elseif (in_array($location, ['PREMIUM', 'EXTREME PREMIUM', 'SLIGHT PREMIUM'], true)) {
                if ($hasContinuationEvidence) {
                    $score = $location === 'EXTREME PREMIUM' ? 38 : 48;
                    $label = 'Momentum Buy - Reduced RR';
                    $state = 'MOMENTUM_BUY_REDUCED_RR';
                    $guidance = 'BUY bias is aligned, but price is above equilibrium. Treat as momentum continuation only and reduce target aggression.';
                    $executionClass = 'REDUCED_RR';
                } else {
                    $score = $location === 'EXTREME PREMIUM' ? 20 : 30;
                    $label = 'Late Buy Location';
                    $state = 'LATE_BUY_LOCATION';
                    $guidance = 'BUY setup is late because price is in premium. Wait for discount pullback or confirmed breakout continuation.';
                    $executionClass = 'CAUTION';
                }
            }
        }

        if ($direction === 'SELL') {
            if (in_array($location, ['EXTREME PREMIUM', 'PREMIUM', 'SLIGHT PREMIUM'], true)) {
                $score = $location === 'EXTREME PREMIUM' ? 95 : 85;
                $label = 'High-Quality Sell Location';
                $state = 'IDEAL_SELL_LOCATION';
                $guidance = 'SELL idea is forming from premium. This is a stronger institutional location.';
                $executionClass = 'A_SETUP';
            } elseif ($location === 'EQUILIBRIUM') {
                $score = 55;
                $label = 'Neutral Sell Location';
                $state = 'NEUTRAL_SELL_LOCATION';
                $guidance = 'SELL idea is near equilibrium. Require confirmation before execution.';
                $executionClass = 'B_SETUP';
            } elseif (in_array($location, ['DISCOUNT', 'EXTREME DISCOUNT', 'SLIGHT DISCOUNT'], true)) {
                if ($hasContinuationEvidence) {
                    $score = $location === 'EXTREME DISCOUNT' ? 38 : 48;
                    $label = 'Momentum Sell - Reduced RR';
                    $state = 'MOMENTUM_SELL_REDUCED_RR';
                    $guidance = 'SELL bias is aligned, but price is below equilibrium. Treat as momentum continuation only and reduce target aggression.';
                    $executionClass = 'REDUCED_RR';
                } else {
                    $score = $location === 'EXTREME DISCOUNT' ? 20 : 30;
                    $label = 'Late Sell Location';
                    $state = 'LATE_SELL_LOCATION';
                    $guidance = 'SELL setup is late because price is in discount. Wait for premium pullback or confirmed breakdown continuation.';
                    $executionClass = 'CAUTION';
                }
            }
        }

        return [
            'score' => $score,
            'label' => $label,
            'state' => $state,
            'guidance' => $guidance,
            'execution_class' => $executionClass,
        ];
    }

    private static function applyLocationAwareTradeAdjustment(array $tradePlan, array $locationQuality): array
    {
        $status = strtoupper((string)($tradePlan['status'] ?? ''));
        if (in_array($status, ['INVALID', 'MISSED_ENTRY', 'NO_VALID_SETUP', 'NO_DATA', 'NO DATA'], true)) {
            $tradePlan['location_quality'] = $locationQuality;
            return $tradePlan;
        }

        $score = (float)($tradePlan['score'] ?? 0);
        $class = strtoupper((string)($locationQuality['execution_class'] ?? 'WATCHLIST'));

        if ($class === 'A_SETUP') {
            $score += 1.0;
        } elseif ($class === 'B_SETUP') {
            $score += 0.25;
        } elseif ($class === 'REDUCED_RR') {
            $score -= 1.5;
            $tradePlan['display_status'] = 'Reduced RR';
            $tradePlan['execution_mode'] = 'REDUCED_RR';
            $tradePlan['execution_guidance'] = $locationQuality['guidance'];
            $tradePlan['message'] = trim(($tradePlan['message'] ?? '') . ' ' . $locationQuality['guidance']);
        } elseif ($class === 'CAUTION') {
            $score -= 2.5;
            if (in_array($status, ['EXECUTION_READY', 'SELECTIVE_EXECUTION'], true)) {
                $tradePlan['status'] = 'WAIT_FOR_CONFIRMATION';
            }
            $tradePlan['display_status'] = 'Location Caution';
            $tradePlan['execution_mode'] = 'LOCATION_CAUTION';
            $tradePlan['execution_guidance'] = $locationQuality['guidance'];
            $tradePlan['message'] = $locationQuality['guidance'];
        }

        $tradePlan['score'] = round(max(0, min(15, $score)), 2);
        $tradePlan['location_quality'] = $locationQuality;
        $tradePlan['location_quality_score'] = $locationQuality['score'];
        $tradePlan['location_quality_label'] = $locationQuality['label'];
        $tradePlan['location_quality_state'] = $locationQuality['state'];
        $tradePlan['location_guidance'] = $locationQuality['guidance'];
        $tradePlan['location_execution_class'] = $locationQuality['execution_class'];

        return $tradePlan;
    }


    private static function sessionIntelligence(): array
    {
        $hour = (int)date('G');

        if ($hour >= 0 && $hour < 7) {
            return ['name' => 'Asian Session', 'score_modifier' => -0.5, 'guidance' => 'Asian session can be slower. Prefer clean confirmations and avoid forcing entries.'];
        }

        if ($hour >= 7 && $hour < 10) {
            return ['name' => 'London Open', 'score_modifier' => 1.0, 'guidance' => 'London Open is active. Liquidity grabs and displacement confirmations carry stronger weight.'];
        }

        if ($hour >= 10 && $hour < 13) {
            return ['name' => 'London Session', 'score_modifier' => 0.5, 'guidance' => 'London session liquidity is active. Valid mitigation setups can perform well.'];
        }

        if ($hour >= 13 && $hour < 16) {
            return ['name' => 'New York Open', 'score_modifier' => 1.0, 'guidance' => 'New York Open is active. Expect volatility expansion; require confirmation before execution.'];
        }

        if ($hour >= 16 && $hour < 20) {
            return ['name' => 'New York Session', 'score_modifier' => 0.25, 'guidance' => 'New York session remains active. Manage risk around late-session reversals.'];
        }

        return ['name' => 'Late Session', 'score_modifier' => -0.5, 'guidance' => 'Late-session conditions can be choppy. Prefer reduced risk or wait for fresh liquidity.'];
    }

    private static function symbolPersonality(string $symbol): array
    {
        $symbol = strtoupper($symbol);

        if ($symbol === 'XAUUSD') {
            return ['name' => 'Gold Volatility Model', 'score_modifier' => 0.75, 'volatility_tolerance' => 'HIGH', 'guidance' => 'Gold requires wider execution tolerance, stronger displacement, and reduced risk during volatility spikes.'];
        }

        if (str_contains($symbol, 'JPY')) {
            return ['name' => 'JPY Volatility Model', 'score_modifier' => 0.25, 'volatility_tolerance' => 'MEDIUM', 'guidance' => 'JPY pairs can move quickly during risk sentiment shifts. Confirmation remains important.'];
        }

        return ['name' => 'Major FX Model', 'score_modifier' => 0.0, 'volatility_tolerance' => 'NORMAL', 'guidance' => 'Major FX pairs use standard mitigation and confirmation logic.'];
    }

    private static function alignmentStrength(string $executionBias, string $contextBias, bool $aligned, bool $hasStructureShift, array $displacement, array $liquidityEvent): int
    {
        $score = 35;

        if ($aligned && in_array($executionBias, ['BUY', 'SELL'], true)) {
            $score += 35;
        } elseif ($contextBias === $executionBias && in_array($executionBias, ['BUY', 'SELL'], true)) {
            $score += 20;
        }

        if ($hasStructureShift) $score += 10;
        if (!empty($displacement['confirmed'])) $score += 10;
        if (!empty($liquidityEvent['confirmed'])) $score += 10;

        return max(0, min(100, $score));
    }

    private static function setupLifecycle(array $tradePlan, ?array $candidate, float $currentPrice): array
    {
        $status = strtoupper((string)($tradePlan['status'] ?? ''));

        if (!$candidate || empty($tradePlan['entry_low']) || empty($tradePlan['entry_high'])) {
            return ['stage' => 'FORMING', 'guidance' => 'No complete execution zone is available yet.', 'progress' => 20];
        }

        $low = min((float)$tradePlan['entry_low'], (float)$tradePlan['entry_high']);
        $high = max((float)$tradePlan['entry_low'], (float)$tradePlan['entry_high']);

        if (in_array($status, ['INVALID', 'MISSED_ENTRY', 'NO_VALID_SETUP', 'NO_DATA', 'NO DATA'], true)) {
            return ['stage' => 'INVALIDATED', 'guidance' => 'Setup is invalid or unavailable. Wait for a new structure.', 'progress' => 10];
        }

        if ($currentPrice >= $low && $currentPrice <= $high) {
            return ['stage' => 'TRIGGER ZONE', 'guidance' => 'Price is inside the execution zone. Wait for confirmation candle before entry.', 'progress' => 85];
        }

        if (in_array($status, ['WAIT_FOR_MITIGATION', 'WATCHLIST', 'MONITOR'], true)) {
            return ['stage' => 'ARMED', 'guidance' => 'Setup is armed. Wait for price to mitigate the zone.', 'progress' => 65];
        }

        if ($status === 'SELECTIVE_EXECUTION') {
            return ['stage' => 'REDUCED RISK', 'guidance' => 'Setup is valid but conditions require reduced risk.', 'progress' => 75];
        }

        return ['stage' => 'FORMING', 'guidance' => 'Setup is forming. Wait for stronger confirmation.', 'progress' => 45];
    }

    private static function executionMode(array $tradePlan, array $candidate, array $newsRisk, array $geoRisk, string $volatility): array
    {
        $status = strtoupper((string)($tradePlan['status'] ?? ''));
        $type = strtoupper((string)($candidate['type'] ?? ($tradePlan['candidate_type'] ?? '')));
        $score = (float)($tradePlan['score'] ?? 0);
        $confirmationStage = strtoupper((string)($tradePlan['confirmation_stage'] ?? 'CONFIRMATION_PENDING'));

        $mode = 'Confirmation Pending';
        $guidance = 'Setup is visible, but execution requires mitigation, displacement, and candle confirmation.';

        if ($confirmationStage === 'EXECUTION_CONFIRMED' && $score >= 8) {
            $mode = 'Standard Execution';
            $guidance = 'Institutional confirmation is aligned. Execute only within your risk model.';
        } elseif ($confirmationStage === 'PARTIAL_CONFIRMATION') {
            $mode = 'Selective Execution';
            $guidance = 'Partial confirmation is present. Use reduced risk or wait for a cleaner trigger.';
        } elseif ($type === 'MICRO_PULLBACK') {
            $mode = 'Trend Continuation';
            $guidance = 'Continuation setup. Prefer confirmation in the direction of active structure.';
        } elseif ($type === 'OB_FVG_CONFLUENCE') {
            $mode = 'Institutional Confluence';
            $guidance = 'OB and FVG confluence detected. Require candle confirmation before execution.';
        }

        if (!empty($newsRisk['has_scheduled_event']) || !empty($newsRisk['has_news'])) {
            $mode = 'News Caution';
            $guidance = 'Setup remains visible, but avoid entering close to the scheduled news release.';
        }

        $geoRisk = strtoupper((string)($geoRisk['risk'] ?? 'LOW'));
        if ($geoRisk === 'HIGH' || $geoRisk === 'EXTREME') {
            $mode = 'Geopolitical Caution';
            $guidance = 'Geopolitical volatility is elevated. Use reduced exposure and require stronger confirmation.';
        }

        if ($volatility === 'EXTREME') {
            $mode = 'Volatility Caution';
            $guidance = 'Extreme volatility detected. Reduce risk and wait for clean confirmation.';
        }

        return ['mode' => $mode, 'guidance' => $guidance];
    }

    private static function rankCandidates(array $candidates): array
    {
        if (empty($candidates)) return [];

        usort($candidates, function ($a, $b) {
            if (($a['score'] ?? 0) === ($b['score'] ?? 0)) {
                return ($a['distance_from_price'] ?? 999999) <=> ($b['distance_from_price'] ?? 999999);
            }
            return ($b['score'] ?? 0) <=> ($a['score'] ?? 0);
        });

        $ranked = [];
        $rank = 1;

        foreach (array_slice($candidates, 0, 5) as $candidate) {
            $raw = (float)($candidate['score'] ?? 0);
            $distance = (float)($candidate['distance_from_price'] ?? 0);
            $distancePenalty = $distance > 0 ? min(1.4, $distance * 0.12) : 0.0;
            $freshnessPenalty = !empty($candidate['stale']) ? 0.8 : 0.0;
            $confirmationPenalty = empty($candidate['liquidity_sweep_confirmed']) && empty($candidate['displacement_confirmed']) ? 1.2 : 0.35;

            $displayScore = $raw - $distancePenalty - $freshnessPenalty - $confirmationPenalty - (($rank - 1) * 0.35);
            $displayScore = max(0, min(7.2, $displayScore));

            $ranked[] = [
                'rank' => $rank++,
                'type' => $candidate['type'] ?? 'UNKNOWN',
                'source' => $candidate['source'] ?? 'N/A',
                'score' => round($displayScore, 2),
                'raw_score' => round($raw, 2),
                'direction' => $candidate['direction'] ?? 'WAIT',
                'distance_from_price' => $candidate['distance_from_price'] ?? null,
                'reasons' => array_slice($candidate['reasons'] ?? [], 0, 4),
            ];
        }

        return $ranked;
    }

    private static function applyPhase2ScoreModifiers(array $tradePlan, array $session, array $personality, int $alignmentStrength): array
    {
        if (!isset($tradePlan['score'])) return $tradePlan;

        $status = strtoupper((string)($tradePlan['status'] ?? ''));
        if (in_array($status, ['INVALID', 'MISSED_ENTRY', 'NO_VALID_SETUP', 'NO_DATA', 'NO DATA'], true)) return $tradePlan;

        $score = (float)$tradePlan['score'];
        $score += (float)($session['score_modifier'] ?? 0);
        $score += (float)($personality['score_modifier'] ?? 0);

        if ($alignmentStrength >= 80) $score += 1.0;
        elseif ($alignmentStrength >= 65) $score += 0.5;
        elseif ($alignmentStrength < 45) $score -= 0.5;

        $tradePlan['score'] = round(max(0, min(15, $score)), 2);

        if ($tradePlan['score'] >= 12) $tradePlan['quality'] = 'HIGH';
        elseif ($tradePlan['score'] >= 8) $tradePlan['quality'] = 'MEDIUM';
        elseif ($tradePlan['score'] >= 5) $tradePlan['quality'] = 'LOW';
        else $tradePlan['quality'] = 'VERY LOW';

        return $tradePlan;
    }

    private static function emptyResult(string $symbol, string $timeframe, string $contextTimeframe, array $style): array
    {
        return [
            'symbol' => $symbol, 'timeframe' => $timeframe, 'context_timeframe' => $contextTimeframe, 'current_price' => null,
            'execution_bias' => 'WAIT', 'context_bias' => 'NEUTRAL', 'aligned' => false,
            'swings' => [], 'structure_events' => [], 'latest_event' => null, 'context_swings' => [], 'context_events' => [], 'context_latest_event' => null,
            'dealing_range' => [], 'price_location' => 'UNKNOWN', 'range_position' => null,
            'array_advantage' => ['favorable' => false, 'message' => 'No candle data.'],
            'liquidity' => ['buy_side' => null, 'sell_side' => null, 'summary' => 'No candle data.'],
            'liquidity_event' => ['confirmed' => false, 'narrative' => 'No candle data.'],
            'displacement' => ['confirmed' => false, 'narrative' => 'No candle data.'],
            'order_blocks' => [], 'active_order_block' => null, 'latest_order_block' => null,
            'fvg_zones' => [], 'active_fvg' => null, 'latest_fvg' => null, 'fvg_ob_confluence' => false, 'confluence_zone' => null,
            'candidates' => [], 'best_candidate' => null, 'ranked_setups' => [], 'execution_mode' => [], 'setup_lifecycle' => [], 'session_intelligence' => [], 'symbol_personality' => [], 'alignment_strength' => 0, 'location_quality' => [], 'phase3' => [], 'atr' => 0, 'volatility' => 'UNKNOWN', 'trading_style' => $style,
            'institutional_bias' => [],
            'trade_plan' => ['status' => 'NO DATA', 'display_status' => 'No Data', 'message' => 'No candle data available.', 'direction' => 'WAIT', 'style_name' => $style['style_name']],
            'decision' => ['label' => 'No Data', 'action' => 'WAIT', 'tone' => 'neutral', 'summary' => 'No candle data available.', 'confidence' => 'LOW'],
            'narrative' => 'No candle data available.'
        ];
    }
}
