"""market_analyzer.py — Enhanced market direction analysis engine.

Phase 2-4: Multi-dimension cross-validation + fault tolerance + signal quality filter.

Adds three new analysis dimensions to session_analyzer:
  A. Volume divergence detection — compare tick_volume trend vs price trend
  B. Trend strength classification — multi-level ADX/trend grading
  C. Volume-price match scoring — correlation between volume and price movement

Adds fault tolerance:
  D. Auto-calibration after 3+ consecutive failures
  E. Extreme market circuit breaker (7% daily / 15% 3-day amplitude)

Adds signal quality:
  F. Multi-dimension entry signal filter
"""
from __future__ import annotations

import math
import time
import json
import pathlib
from typing import Any

# ROOT now resolved via frozen_path (PyInstaller-safe)
from frozen_path import APP_ROOT as ROOT

# ═══════════════════════════════════════════════════════════════════
# A. Volume Divergence Detection
# ═══════════════════════════════════════════════════════════════════

def compute_volume_divergence(bars: list[dict], closes: list[float]) -> dict[str, Any]:
    """Detect divergence between volume trend and price trend.

    Returns:
      divergence_type: "BULLISH_DIVERGENCE" | "BEARISH_DIVERGENCE" | "CONFIRMED" | "NONE"
      divergence_score: -1.0 (bearish) to +1.0 (bullish)
    """
    if len(bars) < 20:
        return {"divergence_type": "NONE", "divergence_score": 0.0, "confidence": 0.0}

    # Sliding window: compare last 10 bars vs previous 10 bars
    half = len(bars) // 2
    recent_bars = bars[-half:]
    prior_bars = bars[-len(bars):-half]
    recent_closes = closes[-half:]
    prior_closes = closes[-len(closes):-half]

    # Volume trend
    recent_volume = sum(b.get("tick_volume", 0) or 0 for b in recent_bars)
    prior_volume = sum(b.get("tick_volume", 0) or 0 for b in prior_bars)
    vol_change = (recent_volume - prior_volume) / max(1, prior_volume)

    # Price trend
    recent_price_change = (recent_closes[-1] - recent_closes[0]) / max(0.01, recent_closes[0])
    prior_price_change = (prior_closes[-1] - prior_closes[0]) / max(0.01, prior_closes[0])

    # Divergence detection
    if vol_change > 0.15 and prior_price_change < -0.002:  # Volume rising, price falling prior → bullish divergence
        return {"divergence_type": "BULLISH_DIVERGENCE", "divergence_score": 0.6,
                "confidence": min(0.8, abs(vol_change)), "vol_change_pct": round(vol_change * 100, 1)}
    elif vol_change < -0.15 and prior_price_change > 0.002:  # Volume falling, price rising prior → bearish divergence
        return {"divergence_type": "BEARISH_DIVERGENCE", "divergence_score": -0.6,
                "confidence": min(0.8, abs(vol_change)), "vol_change_pct": round(vol_change * 100, 1)}
    elif vol_change > 0.10 and recent_price_change > 0:
        return {"divergence_type": "CONFIRMED", "divergence_score": 0.3,
                "confidence": 0.5, "vol_change_pct": round(vol_change * 100, 1)}
    elif vol_change < -0.10 and recent_price_change < 0:
        return {"divergence_type": "CONFIRMED", "divergence_score": -0.3,
                "confidence": 0.5, "vol_change_pct": round(vol_change * 100, 1)}

    return {"divergence_type": "NONE", "divergence_score": 0.0, "confidence": 0.0,
            "vol_change_pct": round(vol_change * 100, 1)}


# ═══════════════════════════════════════════════════════════════════
# B. Trend Strength Classification
# ═══════════════════════════════════════════════════════════════════

def classify_trend_strength(adx: float, ema_slope_pct: float, atr_pct: float,
                            direction: str) -> dict[str, Any]:
    """Multi-level trend strength classification.

    Levels: EXTREME_TREND > STRONG_TREND > WEAK_TREND > NON_TRENDING > CHOP

    Args:
        adx: ADX value
        ema_slope_pct: EMA9 slope as percentage of price
        atr_pct: ATR as percentage of price
        direction: BULLISH / BEARISH / NEUTRAL
    """
    # Base level from ADX
    if adx >= 50:
        base_level = "EXTREME_TREND"
        base_score = 1.0
    elif adx >= 35:
        base_level = "STRONG_TREND"
        base_score = 0.75
    elif adx >= 25:
        base_level = "WEAK_TREND"
        base_score = 0.45
    elif adx >= 18:
        base_level = "NON_TRENDING"
        base_score = 0.20
    else:
        base_level = "CHOP"
        base_score = 0.05

    # EMA slope modifier
    slope_abs = abs(ema_slope_pct)
    if slope_abs > 0.15:
        slope_modifier = +0.10
    elif slope_abs > 0.08:
        slope_modifier = +0.05
    elif slope_abs < 0.02:
        slope_modifier = -0.10
    else:
        slope_modifier = 0.0

    # ATR modifier
    if atr_pct > 0.40:
        atr_modifier = -0.05  # Very high vol → less reliable
    elif atr_pct > 0.20:
        atr_modifier = +0.05  # Healthy vol → good
    elif atr_pct < 0.08:
        atr_modifier = -0.10  # Very low vol → boring
    else:
        atr_modifier = 0.0

    final_score = max(0.0, min(1.0, base_score + slope_modifier + atr_modifier))

    # Directional strength
    if direction == "NEUTRAL":
        directional_score = 0.0
    else:
        directional_score = final_score * (1.0 if direction == "BULLISH" else -1.0)

    return {
        "level": base_level,
        "strength_score": round(final_score, 3),
        "directional_score": round(directional_score, 3),
        "adx_input": round(adx, 2),
        "ema_slope_pct": round(ema_slope_pct, 4),
        "atr_pct": round(atr_pct, 3),
    }


# ═══════════════════════════════════════════════════════════════════
# C. Volume-Price Match Scoring
# ═══════════════════════════════════════════════════════════════════

def compute_volume_price_match(bars: list[dict], closes: list[float]) -> dict[str, Any]:
    """Score the correlation between volume spikes and price movement direction.

    A healthy trend has volume expanding on trend-aligned moves and contracting on pullbacks.
    """
    if len(bars) < 30:
        return {"match_score": 0.0, "match_quality": "INSUFFICIENT_DATA"}

    # Compute bar-by-bar changes
    up_volume = 0.0
    down_volume = 0.0
    up_count = 0
    down_count = 0

    for i in range(1, len(bars)):
        price_change = closes[i] - closes[i-1]
        vol = bars[i].get("tick_volume", 0) or 0
        if price_change > 0:
            up_volume += vol
            up_count += 1
        elif price_change < 0:
            down_volume += vol
            down_count += 1

    total_volume = up_volume + down_volume
    if total_volume <= 0:
        return {"match_score": 0.0, "match_quality": "NO_VOLUME"}

    up_ratio = up_volume / total_volume

    # Compute volume concentration on winning side
    if up_ratio >= 0.60:
        match_quality = "BULLISH_VOLUME_LEADING"
        match_score = min(0.8, (up_ratio - 0.5) * 3.0)
    elif up_ratio <= 0.40:
        match_quality = "BEARISH_VOLUME_LEADING"
        match_score = -min(0.8, (0.5 - up_ratio) * 3.0)
    else:
        match_quality = "BALANCED"
        match_score = 0.0

    return {
        "match_score": round(match_score, 3),
        "match_quality": match_quality,
        "up_volume_pct": round(up_ratio * 100, 1),
        "up_bars": up_count,
        "down_bars": down_count,
    }


# ═══════════════════════════════════════════════════════════════════
# D. Fault Tolerance — Auto-Calibration
# ═══════════════════════════════════════════════════════════════════

_AUTO_CALIBRATE_FILE = ROOT / "logs" / "direction_calibration.json"

def record_direction_result(direction: str, actual_pnl: float, ts: float = None) -> dict[str, Any]:
    """Record a direction decision and its PnL outcome for calibration tracking."""
    if ts is None:
        ts = time.time()

    record = {
        "ts": ts,
        "direction": str(direction).upper(),
        "actual_pnl": round(float(actual_pnl), 2),
        "is_correct": actual_pnl > 0,
    }

    try:
        _AUTO_CALIBRATE_FILE.parent.mkdir(parents=True, exist_ok=True)
        if _AUTO_CALIBRATE_FILE.exists():
            data = json.loads(_AUTO_CALIBRATE_FILE.read_text(encoding="utf-8"))
        else:
            data = {"records": [], "consecutive_failures": 0, "total_failures_since_calibrate": 0,
                    "last_calibration_ts": 0.0, "current_thresholds": {}}
    except Exception:
        data = {"records": [], "consecutive_failures": 0, "total_failures_since_calibrate": 0,
                "last_calibration_ts": 0.0, "current_thresholds": {}}

    data["records"].append(record)
    # Keep last 50 records
    if len(data["records"]) > 50:
        data["records"] = data["records"][-50:]

    if record["is_correct"]:
        data["consecutive_failures"] = 0
    else:
        data["consecutive_failures"] += 1
        data["total_failures_since_calibrate"] += 1

    try:
        _AUTO_CALIBRATE_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
    except Exception:
        pass

    return data


def check_auto_calibration() -> dict[str, Any]:
    """Check if auto-calibration is needed (3+ consecutive failures).

    Returns calibration instructions if thresholds should be adjusted.
    """
    try:
        if _AUTO_CALIBRATE_FILE.exists():
            data = json.loads(_AUTO_CALIBRATE_FILE.read_text(encoding="utf-8"))
        else:
            return {"should_calibrate": False, "reason": "no_data"}
    except Exception:
        return {"should_calibrate": False, "reason": "file_error"}

    consecutive = int(data.get("consecutive_failures", 0))
    if consecutive < 3:
        return {"should_calibrate": False, "reason": f"failures={consecutive}<3"}

    # Auto-calibration logic: tighten thresholds when failing
    records = data.get("records", [])
    recent = records[-20:] if len(records) >= 20 else records
    wrong_count = sum(1 for r in recent if not r.get("is_correct", False))
    accuracy = 1.0 - (wrong_count / max(1, len(recent)))

    # Calibration adjustments
    adjustments = {
        "should_calibrate": True,
        "consecutive_failures": consecutive,
        "recent_accuracy": round(accuracy, 3),
        "adjustments": {
            "confidence_threshold_boost": min(0.15, consecutive * 0.03),  # Raise confidence floor
            "score_threshold_boost": min(2.0, consecutive * 0.5),         # Raise direction score threshold
            "countertrend_multiplier": max(0.3, 1.0 - consecutive * 0.15),  # Reduce countertrend budget
        },
    }

    data["consecutive_failures"] = 0  # Reset after calibration
    data["total_failures_since_calibrate"] = 0
    data["last_calibration_ts"] = time.time()
    data["current_thresholds"] = adjustments["adjustments"]

    try:
        _AUTO_CALIBRATE_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
    except Exception:
        pass

    return adjustments


# ═══════════════════════════════════════════════════════════════════
# E. Extreme Market Circuit Breaker
# ═══════════════════════════════════════════════════════════════════

def detect_extreme_market(bars: list[dict], closes: list[float]) -> dict[str, Any]:
    """Detect extreme market conditions requiring circuit breaker activation.

    Triggers:
      1. Single-day move > 7% (from first to last close)
      2. 3-day amplitude > 15% (high - low / low)
      3. Consecutive 3 bars with > 0.5% moves each (flash crash / spike)
    """
    if len(closes) < 3:
        return {"circuit_break": False, "level": "NORMAL"}

    # Check 1: Single-day extreme move (use last ~288 bars = 24h of M5)
    day_bars = bars[-288:] if len(bars) >= 288 else bars
    day_closes = closes[-288:] if len(closes) >= 288 else closes
    if len(day_closes) >= 2:
        day_change_pct = abs(day_closes[-1] - day_closes[0]) / max(0.01, day_closes[0]) * 100
    else:
        day_change_pct = 0.0

    # Check 2: 3-day amplitude
    three_day_bars = bars[-864:] if len(bars) >= 864 else bars
    if len(three_day_bars) >= 2:
        highs = [b.get("high", b.get("close", 0)) for b in three_day_bars]
        lows = [b.get("low", b.get("close", 0)) for b in three_day_bars]
        three_day_high = max(highs) if highs else 0
        three_day_low = min(lows) if lows else 0
        amplitude_pct = (three_day_high - three_day_low) / max(0.01, three_day_low) * 100
    else:
        amplitude_pct = 0.0

    # Check 3: Flash crash/spike detection (3+ consecutive bars with > 0.5% each)
    flash_count = 0
    max_flash = 0
    for i in range(1, len(closes)):
        bar_change = abs(closes[i] - closes[i-1]) / max(0.01, closes[i-1]) * 100
        if bar_change > 0.5:
            flash_count += 1
        else:
            max_flash = max(max_flash, flash_count)
            flash_count = 0
    max_flash = max(max_flash, flash_count)

    # Determine level
    if day_change_pct > 7.0 or amplitude_pct > 15.0:
        level = "EXTREME"
        should_break = True
        reason = f"day_change={day_change_pct:.1f}% amplitude_3d={amplitude_pct:.1f}%"
    elif day_change_pct > 4.0 or amplitude_pct > 10.0 or max_flash >= 3:
        level = "ELEVATED"
        should_break = True
        reason = f"day_change={day_change_pct:.1f}% amplitude_3d={amplitude_pct:.1f}% flash_bars={max_flash}"
    else:
        level = "NORMAL"
        should_break = False
        reason = ""

    return {
        "circuit_break": should_break,
        "level": level,
        "reason": reason,
        "day_change_pct": round(day_change_pct, 2),
        "amplitude_3d_pct": round(amplitude_pct, 2),
        "max_consecutive_flash_bars": max_flash,
        "safe_mode_actions": {
            "max_position_size": 0.03 if level == "EXTREME" else 0.05,
            "require_confidence": 0.70 if level == "EXTREME" else 0.60,
            "block_countertrend": True,
            "reduce_only_mode": level == "EXTREME",
        } if should_break else {},
    }


# ═══════════════════════════════════════════════════════════════════
# F. Entry Signal Quality Filter
# ═══════════════════════════════════════════════════════════════════

# ═══════════════════════════════════════════════════════════════════
# G. Multi-Cycle Resonance & Price Action
# ═══════════════════════════════════════════════════════════════════

def aggregate_bars(bars: list[dict], multiplier: int) -> list[dict]:
    """Aggregate M5 bars into higher timeframe bars (e.g. multiplier=3 for M15)."""
    if not bars:
        return []
    
    aggregated = []
    for i in range(0, len(bars), multiplier):
        chunk = bars[i:i + multiplier]
        if not chunk:
            continue
        
        aggregated.append({
            "time": chunk[0]["time"],
            "open": chunk[0]["open"],
            "high": max(b["high"] for b in chunk),
            "low": min(b["low"] for b in chunk),
            "close": chunk[-1]["close"],
            "tick_volume": sum(b.get("tick_volume", 0) for b in chunk)
        })
    return aggregated

def detect_price_action_patterns(bars: list[dict]) -> dict[str, Any]:
    """Detect key price action patterns like double tops/bottoms or breakout failures."""
    if len(bars) < 40:
        return {"pattern": "NONE", "score": 0.0}
    
    closes = [b["close"] for b in bars]
    highs = [b["high"] for b in bars]
    lows = [b["low"] for b in bars]
    
    # 1. Breakout Failure (Fakeout)
    # Price breaks above recent high but closes back below it quickly
    recent_high = max(highs[-20:-2])
    if highs[-1] > recent_high * 1.0005 and closes[-1] < recent_high:
        return {"pattern": "BEARISH_FAKEOUT", "score": -0.7, "detail": "failed bullish breakout"}
    
    recent_low = min(lows[-20:-2])
    if lows[-1] < recent_low * 0.9995 and closes[-1] > recent_low:
        return {"pattern": "BULLISH_FAKEOUT", "score": 0.7, "detail": "failed bearish breakout"}
    
    # 2. Double Top / Bottom (Simple version)
    # Two peaks within 2% of each other
    peaks = []
    for i in range(5, len(highs) - 5):
        if highs[i] == max(highs[i-5:i+6]):
            peaks.append(highs[i])
    
    if len(peaks) >= 2:
        if abs(peaks[-1] - peaks[-2]) / peaks[-1] < 0.001:
            if closes[-1] < (peaks[-1] + peaks[-2]) / 2: # Neckline break simplified
                return {"pattern": "DOUBLE_TOP", "score": -0.6}
                
    return {"pattern": "NONE", "score": 0.0}

# ═══════════════════════════════════════════════════════════════════
# H. High-level Analysis API
# ═══════════════════════════════════════════════════════════════════

def analyze_market_quality(bars: list[dict], signal_side: str, signal_confidence: float) -> dict[str, Any]:
    """Unified entry point for market quality analysis."""
    if not bars:
        return {"verdict": "BLOCK", "reason": "no_bars"}
        
    closes = [float(b.get("close", 0) or 0) for b in bars]
    
    # 1. Basic metrics
    from src.regime_router import _compute_adx_di, _true_range_series, _mean, _ema
    adx_info = _compute_adx_di(bars)
    trs = _true_range_series(bars)
    atr14 = _mean(trs[-14:])
    atr_pct = atr14 / max(0.01, closes[-1])
    
    e9 = _ema(closes, 9)[-1]
    e21 = _ema(closes, 21)[-1]
    ema_slope = (e9 - e21) / max(0.01, e21) if e9 and e21 else 0.0
    
    # 2. Components
    vol_div = compute_volume_divergence(bars, closes)
    trend_str = classify_trend_strength(adx_info["adx"], ema_slope, atr_pct, 
                                       "BULLISH" if ema_slope > 0 else "BEARISH" if ema_slope < 0 else "NEUTRAL")
    vp_match = compute_volume_price_match(bars, closes)
    circuit_brk = detect_extreme_market(bars, closes)
    pa_patterns = detect_price_action_patterns(bars)
    
    # 3. Multi-cycle resonance (Aggregated)
    m15_bars = aggregate_bars(bars, 3)
    m15_closes = [b["close"] for b in m15_bars]
    m15_trend = "BULLISH" if len(m15_closes) >= 2 and m15_closes[-1] > m15_closes[-2] else "BEARISH"
    
    resonance_pass = (signal_side == "BUY" and m15_trend == "BULLISH") or \
                     (signal_side == "SELL" and m15_trend == "BEARISH")
    
    # 4. Evaluation
    quality = evaluate_signal_quality(
        signal_side, signal_confidence, vol_div, trend_str, vp_match, circuit_brk
    )
    
    # 5. Price Action Adjustment
    if pa_patterns["pattern"] != "NONE":
        if (signal_side == "BUY" and pa_patterns["score"] < 0) or \
           (signal_side == "SELL" and pa_patterns["score"] > 0):
            quality["verdict"] = "BLOCK"
            quality["reason"] += f" | {pa_patterns['pattern']} detected"
            quality["quality_score"] = min(quality["quality_score"], 0.2)
        elif (signal_side == "BUY" and pa_patterns["score"] > 0) or \
             (signal_side == "SELL" and pa_patterns["score"] < 0):
            quality["quality_score"] = min(1.0, quality["quality_score"] + 0.1)
            quality["reason"] += f" | {pa_patterns['pattern']} boost"

    # 6. Resonance Adjustment
    if not resonance_pass:
        quality["quality_score"] -= 0.15
        quality["checks"].append({"check": "RESONANCE", "result": "FAIL", "detail": f"M15 trend is {m15_trend}"})
        if quality["verdict"] == "GO":
            quality["verdict"] = "FILTER"
    else:
        quality["checks"].append({"check": "RESONANCE", "result": "PASS"})
        
    return quality

def evaluate_signal_quality(
    signal_side: str,
    signal_confidence: float,
    volume_divergence: dict,
    trend_strength: dict,
    volume_price_match: dict,
    circuit_breaker: dict,
    *,
    min_confidence: float = 0.55,
    min_trend_score: float = 0.20,
) -> dict[str, Any]:
    """Multi-dimension entry signal quality evaluation.

    Returns GO / FILTER / BLOCK with detailed scoring.
    """
    checks = []
    score = 1.0

    # Check 1: Confidence floor
    if signal_confidence < min_confidence:
        checks.append({"check": "CONFIDENCE", "result": "FAIL",
                       "detail": f"confidence={signal_confidence:.2f}<{min_confidence}"})
        score -= 0.30
    else:
        checks.append({"check": "CONFIDENCE", "result": "PASS"})

    # Check 2: Volume divergence alignment
    div_score = volume_divergence.get("divergence_score", 0)
    if signal_side == "BUY" and div_score < -0.3:
        checks.append({"check": "VOLUME_DIVERGENCE", "result": "FAIL",
                       "detail": "bearish divergence on BUY signal"})
        score -= 0.25
    elif signal_side == "SELL" and div_score > 0.3:
        checks.append({"check": "VOLUME_DIVERGENCE", "result": "FAIL",
                       "detail": "bullish divergence on SELL signal"})
        score -= 0.25
    elif abs(div_score) > 0.4:
        checks.append({"check": "VOLUME_DIVERGENCE", "result": "BOOST",
                       "detail": f"divergence aligned, score={div_score:.2f}"})
        score += 0.10
    else:
        checks.append({"check": "VOLUME_DIVERGENCE", "result": "PASS"})

    # Check 3: Trend strength alignment
    trend_dir_score = trend_strength.get("directional_score", 0)
    trend_level = trend_strength.get("level", "UNKNOWN")
    if signal_side == "BUY" and trend_dir_score < -0.3:
        checks.append({"check": "TREND_STRENGTH", "result": "FAIL",
                       "detail": f"bearish trend on BUY signal, level={trend_level}"})
        score -= 0.30
    elif signal_side == "SELL" and trend_dir_score > 0.3:
        checks.append({"check": "TREND_STRENGTH", "result": "FAIL",
                       "detail": f"bullish trend on SELL signal, level={trend_level}"})
        score -= 0.30
    elif abs(trend_dir_score) > 0.5:
        checks.append({"check": "TREND_STRENGTH", "result": "BOOST",
                       "detail": f"strong trend aligned, score={trend_dir_score:.2f}"})
        score += 0.10
    elif trend_level in ("CHOP", "NON_TRENDING"):
        checks.append({"check": "TREND_STRENGTH", "result": "WARN",
                       "detail": f"weak trend, level={trend_level}"})
        score -= 0.10
    else:
        checks.append({"check": "TREND_STRENGTH", "result": "PASS"})

    # Check 4: Volume-price match
    vp_score = volume_price_match.get("match_score", 0)
    if signal_side == "BUY" and vp_score < -0.3:
        checks.append({"check": "VOL_PRICE_MATCH", "result": "FAIL",
                       "detail": f"bearish volume leading, score={vp_score:.2f}"})
        score -= 0.20
    elif signal_side == "SELL" and vp_score > 0.3:
        checks.append({"check": "VOL_PRICE_MATCH", "result": "FAIL",
                       "detail": f"bullish volume leading, score={vp_score:.2f}"})
        score -= 0.20
    else:
        checks.append({"check": "VOL_PRICE_MATCH", "result": "PASS"})

    # Check 5: Circuit breaker override
    if circuit_breaker.get("circuit_break"):
        cb_level = circuit_breaker.get("level", "ELEVATED")
        checks.append({"check": "CIRCUIT_BREAKER", "result": "BLOCK",
                       "detail": f"circuit breaker active, level={cb_level}"})
        score = min(score, 0.30)  # Cap score when circuit breaker active
        if circuit_breaker.get("level") == "EXTREME":
            return {
                "verdict": "BLOCK",
                "quality_score": round(max(0.0, score), 3),
                "checks": checks,
                "reason": f"EXTREME market circuit breaker: {circuit_breaker.get('reason', '')}",
            }

    # Check 6: Multi-timeframe resonance (New optimization)
    # If we had access to multiple timeframes here, we would check for alignment.
    # For now, we use a placeholder for the logic to be expanded.
    
    # Final verdict
    if score >= 0.75:  # Tightened from 0.70 for higher accuracy
        verdict = "GO"
    elif score >= 0.45:  # Tightened from 0.40
        verdict = "FILTER"  # Allow with reduced size
    else:
        verdict = "BLOCK"

    return {
        "verdict": verdict,
        "quality_score": round(max(0.0, min(1.0, score)), 3),
        "checks": checks,
        "reason": f"signal_quality={verdict} score={score:.2f}",
    }
