"""Social Sentiment Index — aggregate sentiment from price behavior proxy.

Since no external social API is available, sentiment is derived entirely
from price action patterns in sparkline_in_7d (168 hourly data points):
  Momentum sentiment  — multi-period momentum direction and strength
  Volume sentiment    — rising volume + positive price = bullish
  Volatility sentiment — high vol = fear, low vol = complacency
  Crowd behavior      — chasing (FOMO) vs panic selling patterns

Score scale (0-100):
  0-20   Extreme Fear
  20-40  Fear
  40-60  Neutral
  60-80  Greed
  80-100 Extreme Greed

Data source: Asset model fields (current_price, sparkline_in_7d,
price_change_percentage_24h, total_volume, ath, atl, market_cap).
"""
from __future__ import annotations

import json
import math
import logging

logger = logging.getLogger(__name__)


def _safe_float(val, default: float = 0.0) -> float:
    """Safely convert a DB Numeric / Decimal to float."""
    if val is None:
        return default
    try:
        f = float(val)
        return default if (math.isnan(f) or math.isinf(f)) else f
    except (ValueError, TypeError):
        return default


class SocialSentimentService:
    """Derive market sentiment index from price behavior proxies."""

    # Component weights (sum to 100)
    WEIGHTS = {
        'momentum': 25,
        'volume': 20,
        'volatility': 25,
        'crowd_behavior': 20,
        'ath_atl_proximity': 10,
    }

    SENTIMENT_LABELS = [
        (20, 'Extreme Fear'),
        (40, 'Fear'),
        (60, 'Neutral'),
        (80, 'Greed'),
        (100, 'Extreme Greed'),
    ]

    # ──────────────────────────────────────────────────────────────────
    # Public API
    # ──────────────────────────────────────────────────────────────────

    def analyze(self, symbol, asset_type='crypto'):
        """Calculate sentiment index for a single asset.

        Returns sentiment score, label, component breakdown, contrarian
        signal, FOMO level, and panic level.
        """
        try:
            from app.models.asset import Asset

            asset = Asset.query.filter_by(
                symbol=symbol.upper(), is_active=True
            ).first()
            if not asset:
                return {'status': 'error', 'message': f'Asset {symbol} not found'}

            prices = self._parse_sparkline(asset)
            if len(prices) < 48:
                return {'status': 'error', 'message': 'Insufficient sparkline data'}

            current_price = _safe_float(asset.current_price)
            if current_price <= 0:
                current_price = prices[-1] if prices else 0
            if current_price <= 0:
                return {'status': 'error', 'message': 'No valid price data'}

            # Calculate components
            momentum = self._calculate_momentum_sentiment(prices)
            volume = self._calculate_volume_sentiment(prices)
            volatility = self._calculate_volatility_sentiment(prices)
            crowd = self._calculate_crowd_behavior(prices)
            ath_atl = self._calculate_ath_atl_sentiment(asset, current_price)

            # FOMO and panic signals
            fomo = self._detect_fomo_signals(prices, momentum, volume)
            panic = self._detect_panic_signals(prices, momentum, volume)

            # Composite sentiment
            composite = self._calculate_composite_sentiment(
                momentum, volume, volatility, crowd, ath_atl
            )

            # Contrarian score
            contrarian = self._calculate_contrarian_score(composite, fomo, panic)

            # Label
            sentiment_label = self._get_sentiment_label(composite)

            # Historical sentiment from sparkline windows
            historical = self._estimate_historical_sentiment(prices)

            return {
                'status': 'success',
                'data': {
                    'symbol': asset.symbol,
                    'name': asset.name,
                    'current_price': current_price,
                    'sentiment_score': composite,
                    'sentiment_label': sentiment_label,
                    'components': {
                        'momentum': {
                            'score': momentum['score'],
                            'weight': self.WEIGHTS['momentum'],
                            'detail': momentum['detail'],
                        },
                        'volume': {
                            'score': volume['score'],
                            'weight': self.WEIGHTS['volume'],
                            'detail': volume['detail'],
                        },
                        'volatility': {
                            'score': volatility['score'],
                            'weight': self.WEIGHTS['volatility'],
                            'detail': volatility['detail'],
                        },
                        'crowd_behavior': {
                            'score': crowd['score'],
                            'weight': self.WEIGHTS['crowd_behavior'],
                            'detail': crowd['detail'],
                        },
                        'ath_atl_proximity': {
                            'score': ath_atl['score'],
                            'weight': self.WEIGHTS['ath_atl_proximity'],
                            'detail': ath_atl['detail'],
                        },
                    },
                    'fomo_level': fomo,
                    'panic_level': panic,
                    'contrarian_signal': contrarian,
                    'historical_sentiment': historical,
                    'recommendations': self._generate_recommendations(
                        composite, sentiment_label, contrarian, fomo, panic
                    ),
                },
            }

        except Exception as e:
            logger.error(f'Sentiment analysis error for {symbol}: {e}')
            return {'status': 'error', 'message': str(e)}

    def scan_all(self, asset_type='crypto', limit=50):
        """Calculate sentiment for all active assets and find extremes.

        Returns ranked list plus market-wide sentiment index.
        """
        try:
            from app.models.asset import Asset

            assets = (
                Asset.query
                .filter_by(asset_type=asset_type, is_active=True)
                .order_by(Asset.market_cap_rank.asc())
                .limit(limit * 2)
                .all()
            )

            if not assets:
                return {'status': 'error', 'message': 'No assets found'}

            results = []
            all_sentiments = []

            for asset in assets:
                prices = self._parse_sparkline(asset)
                if len(prices) < 48:
                    continue

                current_price = _safe_float(asset.current_price)
                if current_price <= 0:
                    current_price = prices[-1] if prices else 0
                if current_price <= 0:
                    continue

                momentum = self._calculate_momentum_sentiment(prices)
                volume = self._calculate_volume_sentiment(prices)
                volatility = self._calculate_volatility_sentiment(prices)
                crowd = self._calculate_crowd_behavior(prices)
                ath_atl = self._calculate_ath_atl_sentiment(asset, current_price)

                fomo = self._detect_fomo_signals(prices, momentum, volume)
                panic = self._detect_panic_signals(prices, momentum, volume)

                composite = self._calculate_composite_sentiment(
                    momentum, volume, volatility, crowd, ath_atl
                )
                label = self._get_sentiment_label(composite)
                contrarian = self._calculate_contrarian_score(composite, fomo, panic)

                all_sentiments.append(composite)

                results.append({
                    'symbol': asset.symbol,
                    'name': asset.name,
                    'current_price': current_price,
                    'market_cap_rank': asset.market_cap_rank,
                    'sentiment_score': composite,
                    'sentiment_label': label,
                    'fomo_level': fomo['level'],
                    'panic_level': panic['level'],
                    'contrarian_score': contrarian['score'],
                    'contrarian_signal': contrarian['signal'],
                    'momentum_sentiment': momentum['score'],
                    'volatility_sentiment': volatility['score'],
                })

            results.sort(key=lambda x: x['sentiment_score'], reverse=True)
            results = results[:limit]

            # Market-wide sentiment
            if all_sentiments:
                market_sentiment = round(
                    sum(all_sentiments) / len(all_sentiments), 1
                )
            else:
                market_sentiment = 50

            market_label = self._get_sentiment_label(market_sentiment)

            extreme_fear = [r for r in results if r['sentiment_score'] < 20]
            extreme_greed = [r for r in results if r['sentiment_score'] > 80]
            contrarian_buys = [
                r for r in results
                if r['contrarian_signal'] == 'buy' and r['contrarian_score'] >= 60
            ]

            return {
                'status': 'success',
                'data': {
                    'items': results,
                    'total': len(results),
                    'market_sentiment': market_sentiment,
                    'market_sentiment_label': market_label,
                    'extreme_fear_count': len(extreme_fear),
                    'extreme_greed_count': len(extreme_greed),
                    'contrarian_buy_count': len(contrarian_buys),
                    'most_fearful': results[-1] if results else None,
                    'most_greedy': results[0] if results else None,
                    'asset_type': asset_type,
                    'recommendations': self._generate_market_recommendations(
                        market_sentiment, market_label,
                        extreme_fear, extreme_greed, contrarian_buys
                    ),
                },
            }

        except Exception as e:
            logger.error(f'Sentiment scan error: {e}')
            return {'status': 'error', 'message': str(e)}

    # ──────────────────────────────────────────────────────────────────
    # Sparkline parsing
    # ──────────────────────────────────────────────────────────────────

    @staticmethod
    def _parse_sparkline(asset) -> list:
        """Parse sparkline_in_7d via AssetProfile.profile_json."""
        try:
            from app.models.asset import AssetProfile
            profile = (
                AssetProfile.query
                .filter_by(asset_id=asset.id)
                .order_by(AssetProfile.fetched_at.desc())
                .first()
            )
            if not profile or not profile.profile_json:
                return []
            pj = profile.profile_json
            if isinstance(pj, str):
                pj = json.loads(pj)
            sparkline = pj.get('sparkline_in_7d', {})
            if isinstance(sparkline, dict):
                raw = sparkline.get('price', [])
            elif isinstance(sparkline, list):
                raw = sparkline
            else:
                return []
            prices = [float(p) for p in raw if p is not None]
            return prices if len(prices) >= 24 else []
        except Exception:
            return []

    # ──────────────────────────────────────────────────────────────────
    # Component: Momentum sentiment
    # ──────────────────────────────────────────────────────────────────

    @staticmethod
    def _calculate_momentum_sentiment(prices):
        """Derive sentiment from multi-period momentum.

        Positive momentum across multiple periods = bullish sentiment.
        Uses 6h, 24h, 72h, 168h windows.
        """
        if len(prices) < 48:
            return {'score': 50, 'detail': 'Insufficient data'}

        windows = []
        # 6h momentum
        if len(prices) >= 6 and prices[-6] != 0:
            ret_6h = (prices[-1] - prices[-6]) / prices[-6] * 100
            windows.append(('6h', ret_6h))

        # 24h momentum
        if len(prices) >= 24 and prices[-24] != 0:
            ret_24h = (prices[-1] - prices[-24]) / prices[-24] * 100
            windows.append(('24h', ret_24h))

        # 72h momentum (3 days)
        if len(prices) >= 72 and prices[-72] != 0:
            ret_72h = (prices[-1] - prices[-72]) / prices[-72] * 100
            windows.append(('72h', ret_72h))

        # 168h momentum (7 days)
        if prices[0] != 0:
            ret_7d = (prices[-1] - prices[0]) / prices[0] * 100
            windows.append(('7d', ret_7d))

        if not windows:
            return {'score': 50, 'detail': 'No momentum data'}

        # Weight: shorter periods get more weight
        weight_map = {'6h': 0.35, '24h': 0.30, '72h': 0.20, '7d': 0.15}
        weighted_ret = 0.0
        total_weight = 0.0
        positive_count = 0

        for label, ret in windows:
            w = weight_map.get(label, 0.2)
            weighted_ret += ret * w
            total_weight += w
            if ret > 0:
                positive_count += 1

        avg_ret = weighted_ret / total_weight if total_weight > 0 else 0

        # Map: -15% -> 0, 0% -> 50, +15% -> 100
        score = max(0, min(100, 50 + avg_ret * 3.33))

        alignment = positive_count / len(windows) * 100
        detail = (
            f'Weighted momentum: {avg_ret:.2f}%, '
            f'{alignment:.0f}% of timeframes positive'
        )

        return {'score': round(score, 1), 'detail': detail}

    # ──────────────────────────────────────────────────────────────────
    # Component: Volume sentiment
    # ──────────────────────────────────────────────────────────────────

    @staticmethod
    def _calculate_volume_sentiment(prices):
        """Derive sentiment from volume proxy patterns.

        Volume proxy: abs(price[i] - price[i-1]) * price[i]
        Rising volume + positive price = bullish.
        Rising volume + negative price = panic (fear).
        Low volume = indecision (neutral).
        """
        if len(prices) < 48:
            return {'score': 50, 'detail': 'Insufficient data'}

        volumes = []
        for i in range(1, len(prices)):
            vol = abs(prices[i] - prices[i - 1]) * prices[i]
            volumes.append(vol)

        if not volumes:
            return {'score': 50, 'detail': 'No volume data'}

        # Recent vs older volume
        mid = len(volumes) // 2
        recent_vol = sum(volumes[mid:]) / len(volumes[mid:])
        older_vol = sum(volumes[:mid]) / len(volumes[:mid])

        vol_trend = (
            (recent_vol - older_vol) / older_vol * 100
        ) if older_vol > 0 else 0

        # Recent price direction
        recent_prices = prices[-(len(prices) // 4):]
        if len(recent_prices) >= 2 and recent_prices[0] != 0:
            price_direction = (
                (recent_prices[-1] - recent_prices[0]) / recent_prices[0] * 100
            )
        else:
            price_direction = 0

        # Volume up + price up = greed; Volume up + price down = fear
        if vol_trend > 10 and price_direction > 1:
            score = 60 + min(30, vol_trend * 0.3 + price_direction * 2)
        elif vol_trend > 10 and price_direction < -1:
            score = 40 - min(30, vol_trend * 0.2 + abs(price_direction) * 2)
        elif vol_trend < -10:
            score = 45  # Low volume = apathy / complacency
        else:
            score = 50

        score = max(0, min(100, score))

        if vol_trend > 20:
            trend_label = 'surging'
        elif vol_trend > 5:
            trend_label = 'rising'
        elif vol_trend < -20:
            trend_label = 'collapsing'
        elif vol_trend < -5:
            trend_label = 'declining'
        else:
            trend_label = 'stable'

        detail = (
            f'Volume {trend_label} ({vol_trend:+.1f}%), '
            f'price direction {price_direction:+.2f}%'
        )

        return {'score': round(score, 1), 'detail': detail}

    # ──────────────────────────────────────────────────────────────────
    # Component: Volatility sentiment
    # ──────────────────────────────────────────────────────────────────

    @staticmethod
    def _calculate_volatility_sentiment(prices):
        """Derive sentiment from volatility regime.

        High volatility = fear/uncertainty.
        Low volatility = complacency/stability.
        Normal volatility = neutral.
        """
        if len(prices) < 48:
            return {'score': 50, 'detail': 'Insufficient data'}

        returns = []
        for i in range(1, len(prices)):
            if prices[i - 1] != 0:
                ret = (prices[i] - prices[i - 1]) / prices[i - 1]
                returns.append(ret)

        if len(returns) < 24:
            return {'score': 50, 'detail': 'Insufficient return data'}

        # Full-period volatility
        mean_ret = sum(returns) / len(returns)
        variance = sum((r - mean_ret) ** 2 for r in returns) / len(returns)
        full_vol = math.sqrt(variance) if variance > 0 else 0

        # Recent 24h volatility
        recent_returns = returns[-24:]
        recent_mean = sum(recent_returns) / len(recent_returns)
        recent_var = sum(
            (r - recent_mean) ** 2 for r in recent_returns
        ) / len(recent_returns)
        recent_vol = math.sqrt(recent_var) if recent_var > 0 else 0

        # Ratio
        vol_ratio = recent_vol / full_vol if full_vol > 0 else 1.0

        # Map: high ratio = fear (low score), low ratio = complacency (high score)
        if vol_ratio <= 0.6:
            score = 75 + min(20, (0.6 - vol_ratio) * 50)
        elif vol_ratio <= 0.9:
            score = 55 + (0.9 - vol_ratio) * 66
        elif vol_ratio <= 1.2:
            score = 40 + (1.2 - vol_ratio) * 50
        elif vol_ratio <= 1.8:
            score = 20 + (1.8 - vol_ratio) * 33
        else:
            score = max(0, 20 - (vol_ratio - 1.8) * 20)

        score = max(0, min(100, score))

        annualized_vol = full_vol * math.sqrt(8760)  # hourly to annual

        if vol_ratio > 1.5:
            regime = 'High volatility (fear)'
        elif vol_ratio < 0.7:
            regime = 'Low volatility (complacency)'
        else:
            regime = 'Normal volatility'

        detail = (
            f'{regime}, '
            f'vol ratio {vol_ratio:.2f}x, '
            f'annualized {annualized_vol * 100:.0f}%'
        )

        return {'score': round(score, 1), 'detail': detail}

    # ──────────────────────────────────────────────────────────────────
    # Component: Crowd behavior
    # ──────────────────────────────────────────────────────────────────

    @staticmethod
    def _calculate_crowd_behavior(prices):
        """Detect crowd behavior patterns from price action.

        Chasing (FOMO): accelerating price rise with increasing moves.
        Panic selling: sharp drops with capitulation volume.
        Exhaustion: diminishing returns after a strong move.
        """
        if len(prices) < 48:
            return {'score': 50, 'detail': 'Insufficient data'}

        # Divide into 6 segments of ~28 hours each
        seg_len = len(prices) // 6
        segments = []
        for i in range(6):
            start = i * seg_len
            end = start + seg_len
            seg = prices[start:end]
            if len(seg) >= 2 and seg[0] != 0:
                ret = (seg[-1] - seg[0]) / seg[0] * 100
                segments.append(ret)

        if len(segments) < 3:
            return {'score': 50, 'detail': 'Insufficient segments'}

        # Check for acceleration pattern (FOMO)
        # Each segment return is larger than previous
        fomo_score = 0
        accel_count = 0
        for i in range(1, len(segments)):
            if segments[i] > segments[i - 1] and segments[i] > 0:
                accel_count += 1

        if accel_count >= 4:
            fomo_score = 30
        elif accel_count >= 3:
            fomo_score = 20
        elif accel_count >= 2:
            fomo_score = 10

        # Check for panic pattern
        # Increasingly negative returns
        panic_score = 0
        decel_count = 0
        for i in range(1, len(segments)):
            if segments[i] < segments[i - 1] and segments[i] < 0:
                decel_count += 1

        if decel_count >= 4:
            panic_score = 30
        elif decel_count >= 3:
            panic_score = 20
        elif decel_count >= 2:
            panic_score = 10

        # Overall return trend
        overall_trend = sum(segments) / len(segments)

        # Combine: FOMO pushes score up, panic pushes down
        base = 50
        if fomo_score > panic_score:
            score = base + fomo_score + min(20, overall_trend * 2)
            behavior = 'FOMO chasing'
        elif panic_score > fomo_score:
            score = base - panic_score + max(-20, overall_trend * 2)
            behavior = 'Panic selling'
        elif overall_trend > 2:
            score = 60 + min(15, overall_trend)
            behavior = 'Mild optimism'
        elif overall_trend < -2:
            score = 40 + max(-15, overall_trend)
            behavior = 'Mild pessimism'
        else:
            score = 50
            behavior = 'Neutral'

        score = max(0, min(100, score))

        detail = (
            f'{behavior}, '
            f'acceleration segments: {accel_count}, '
            f'deceleration: {decel_count}'
        )

        return {'score': round(score, 1), 'detail': detail}

    # ──────────────────────────────────────────────────────────────────
    # Component: ATH/ATL proximity sentiment
    # ──────────────────────────────────────────────────────────────────

    @staticmethod
    def _calculate_ath_atl_sentiment(asset, current_price):
        """Sentiment based on proximity to all-time high/low.

        Near ATH = extreme greed; near ATL = extreme fear.
        """
        ath = _safe_float(asset.ath)
        atl = _safe_float(asset.atl)

        if ath <= 0 or atl <= 0 or ath <= atl:
            return {'score': 50, 'detail': 'No ATH/ATL data available'}

        # Position within ATL-ATH range
        range_size = ath - atl
        position = (current_price - atl) / range_size if range_size > 0 else 0.5
        position = max(0, min(1, position))

        # Near ATH = greed, near ATL = fear
        score = position * 100

        pct_from_ath = (ath - current_price) / ath * 100 if ath > 0 else 0
        pct_from_atl = (current_price - atl) / atl * 100 if atl > 0 else 0

        detail = (
            f'{pct_from_ath:.1f}% below ATH, '
            f'{pct_from_atl:.1f}% above ATL'
        )

        return {'score': round(score, 1), 'detail': detail}

    # ──────────────────────────────────────────────────────────────────
    # FOMO detection
    # ──────────────────────────────────────────────────────────────────

    @staticmethod
    def _detect_fomo_signals(prices, momentum, volume):
        """Detect FOMO — parabolic price rise + accelerating volume.

        Returns dict with FOMO level (0-100) and description.
        """
        level = 0
        signals = []

        # Parabolic check: last 24h return > 2x the 7d average daily
        if len(prices) >= 48:
            ret_24h = (
                (prices[-1] - prices[-24]) / prices[-24] * 100
            ) if prices[-24] != 0 else 0
            ret_7d = (
                (prices[-1] - prices[0]) / prices[0] * 100
            ) if prices[0] != 0 else 0
            avg_daily = ret_7d / 7

            if ret_24h > 0 and avg_daily != 0 and ret_24h > abs(avg_daily) * 3:
                level += 30
                signals.append('Parabolic price rise')

            if ret_24h > 10:
                level += 20
                signals.append(f'24h return {ret_24h:.1f}%')

        # Momentum acceleration
        if momentum['score'] > 75:
            level += 20
            signals.append('Strong momentum')

        # Volume surge
        if volume['score'] > 70:
            level += 15
            signals.append('Volume surging')

        # Consecutive green candles
        if len(prices) >= 12:
            consecutive = 0
            for i in range(len(prices) - 12, len(prices)):
                if i > 0 and prices[i] > prices[i - 1]:
                    consecutive += 1
                else:
                    consecutive = 0
            if consecutive >= 8:
                level += 15
                signals.append(f'{consecutive} consecutive up hours')

        level = min(100, level)

        return {
            'level': level,
            'signals': signals,
            'description': (
                'Strong FOMO detected' if level >= 60
                else 'Mild FOMO present' if level >= 30
                else 'No significant FOMO'
            ),
        }

    # ──────────────────────────────────────────────────────────────────
    # Panic detection
    # ──────────────────────────────────────────────────────────────────

    @staticmethod
    def _detect_panic_signals(prices, momentum, volume):
        """Detect panic selling — sharp drops + volume spikes.

        Returns dict with panic level (0-100) and description.
        """
        level = 0
        signals = []

        if len(prices) >= 48:
            ret_24h = (
                (prices[-1] - prices[-24]) / prices[-24] * 100
            ) if prices[-24] != 0 else 0

            if ret_24h < -10:
                level += 30
                signals.append(f'Sharp 24h drop: {ret_24h:.1f}%')

            # Flash crash: any single hour > 5% drop
            for i in range(max(1, len(prices) - 24), len(prices)):
                if prices[i - 1] != 0:
                    hourly_ret = (prices[i] - prices[i - 1]) / prices[i - 1] * 100
                    if hourly_ret < -5:
                        level += 20
                        signals.append(f'Flash crash: {hourly_ret:.1f}% in one hour')
                        break

        # Low momentum = fear
        if momentum['score'] < 25:
            level += 20
            signals.append('Very weak momentum')

        # High volume + negative = capitulation
        if volume['score'] < 30:
            level += 15
            signals.append('Capitulation volume pattern')

        # Consecutive red candles
        if len(prices) >= 12:
            consecutive = 0
            for i in range(len(prices) - 12, len(prices)):
                if i > 0 and prices[i] < prices[i - 1]:
                    consecutive += 1
                else:
                    consecutive = 0
            if consecutive >= 8:
                level += 15
                signals.append(f'{consecutive} consecutive down hours')

        level = min(100, level)

        return {
            'level': level,
            'signals': signals,
            'description': (
                'Strong panic selling' if level >= 60
                else 'Mild panic present' if level >= 30
                else 'No significant panic'
            ),
        }

    # ──────────────────────────────────────────────────────────────────
    # Contrarian score
    # ──────────────────────────────────────────────────────────────────

    @staticmethod
    def _calculate_contrarian_score(composite, fomo, panic):
        """Calculate contrarian opportunity score.

        When sentiment is extreme, contrarian positions have higher
        expected value.
        """
        score = 0
        signal = 'neutral'

        if composite <= 20:
            # Extreme fear = contrarian buy
            score = min(100, (20 - composite) * 5 + panic['level'] * 0.5)
            signal = 'buy'
        elif composite <= 35:
            score = min(70, (35 - composite) * 3 + panic['level'] * 0.3)
            signal = 'buy'
        elif composite >= 80:
            # Extreme greed = contrarian sell
            score = min(100, (composite - 80) * 5 + fomo['level'] * 0.5)
            signal = 'sell'
        elif composite >= 65:
            score = min(70, (composite - 65) * 3 + fomo['level'] * 0.3)
            signal = 'sell'

        score = round(max(0, min(100, score)), 1)

        return {
            'score': score,
            'signal': signal,
            'description': (
                f'Strong contrarian {signal} signal'
                if score >= 60
                else f'Moderate contrarian {signal} signal'
                if score >= 30
                else 'No contrarian signal'
            ),
        }

    # ──────────────────────────────────────────────────────────────────
    # Composite calculation
    # ──────────────────────────────────────────────────────────────────

    def _calculate_composite_sentiment(
        self, momentum, volume, volatility, crowd, ath_atl
    ):
        """Calculate weighted composite sentiment score (0-100)."""
        composite = (
            momentum['score'] * self.WEIGHTS['momentum']
            + volume['score'] * self.WEIGHTS['volume']
            + volatility['score'] * self.WEIGHTS['volatility']
            + crowd['score'] * self.WEIGHTS['crowd_behavior']
            + ath_atl['score'] * self.WEIGHTS['ath_atl_proximity']
        ) / 100.0

        return round(max(0, min(100, composite)), 1)

    # ──────────────────────────────────────────────────────────────────
    # Historical estimation
    # ──────────────────────────────────────────────────────────────────

    @staticmethod
    def _estimate_historical_sentiment(prices):
        """Estimate daily sentiment from sparkline sub-windows."""
        history = []
        for day in range(7):
            start = day * 24
            end = start + 24
            if end > len(prices):
                break
            segment = prices[start:end]
            if len(segment) < 12 or segment[0] == 0:
                continue

            ret = (segment[-1] - segment[0]) / segment[0] * 100
            # Simple sentiment proxy from return
            day_sentiment = max(0, min(100, 50 + ret * 3.33))
            history.append({
                'day_index': day,
                'sentiment_score': round(day_sentiment, 1),
                'return_pct': round(ret, 2),
            })

        return history

    # ──────────────────────────────────────────────────────────────────
    # Sentiment label
    # ──────────────────────────────────────────────────────────────────

    def _get_sentiment_label(self, score):
        """Map score to sentiment label."""
        for threshold, label in self.SENTIMENT_LABELS:
            if score <= threshold:
                return label
        return 'Extreme Greed'

    # ──────────────────────────────────────────────────────────────────
    # Recommendations
    # ──────────────────────────────────────────────────────────────────

    @staticmethod
    def _generate_recommendations(composite, label, contrarian, fomo, panic):
        """Generate recommendations for a single asset."""
        recs = []

        if composite < 20:
            recs.append({
                'type': 'opportunity',
                'message': (
                    f'Extreme fear ({composite}/100). '
                    'Historically this is a strong buying zone for quality assets.'
                ),
            })
        elif composite > 80:
            recs.append({
                'type': 'warning',
                'message': (
                    f'Extreme greed ({composite}/100). '
                    'Consider taking profits — high risk of correction.'
                ),
            })

        if contrarian['score'] >= 50:
            recs.append({
                'type': 'insight',
                'message': (
                    f'Contrarian {contrarian["signal"]} signal '
                    f'(strength: {contrarian["score"]}/100).'
                ),
            })

        if fomo['level'] >= 60:
            recs.append({
                'type': 'warning',
                'message': f'High FOMO ({fomo["level"]}/100) — avoid chasing.',
            })

        if panic['level'] >= 60:
            recs.append({
                'type': 'insight',
                'message': (
                    f'High panic ({panic["level"]}/100) — '
                    'capitulation may signal bottom.'
                ),
            })

        return recs

    @staticmethod
    def _generate_market_recommendations(
        market_sentiment, market_label,
        extreme_fear, extreme_greed, contrarian_buys
    ):
        """Generate market-wide recommendations."""
        recs = []

        recs.append({
            'type': 'insight',
            'message': (
                f'Market-wide sentiment: {market_label} ({market_sentiment}/100).'
            ),
        })

        if len(extreme_fear) >= 5:
            recs.append({
                'type': 'opportunity',
                'message': (
                    f'{len(extreme_fear)} assets in extreme fear — '
                    'potential contrarian buying opportunity.'
                ),
            })

        if len(extreme_greed) >= 5:
            recs.append({
                'type': 'warning',
                'message': (
                    f'{len(extreme_greed)} assets in extreme greed — '
                    'market may be overheated.'
                ),
            })

        if contrarian_buys:
            symbols = ', '.join(
                r['symbol'] for r in contrarian_buys[:3]
            )
            recs.append({
                'type': 'opportunity',
                'message': (
                    f'Top contrarian buys: {symbols} — '
                    'extreme fear with high reversal potential.'
                ),
            })

        return recs
