"""
VYNDR Evolution Engine — Blueprint
PELT changepoint detection for player metric evolution.
Structural migration from evolutionEngine.py — logic unchanged.
"""

import sys
import numpy as np
from flask import Blueprint, request, jsonify

evolution_bp = Blueprint('evolution', __name__)

# Graceful import — ruptures may not be installed
try:
    import ruptures as rpt
    HAS_RUPTURES = True
except ImportError:
    HAS_RUPTURES = False
    print("[evolution-engine] WARNING: ruptures not installed. Using fallback.", file=sys.stderr)


def detect_changepoints_pelt(values, min_size=5, penalty=3.0):
    """
    Use PELT algorithm from ruptures library.
    Detects changepoints in time-series data for player metric evolution.

    Args:
        values: List of numeric values (e.g., game-by-game stat line).
        min_size: Minimum segment length between changepoints.
        penalty: PELT penalty parameter — higher = fewer changepoints.

    Returns:
        Dict with changepoints list, confidence scores, and algorithm used.
    """
    if not HAS_RUPTURES:
        return fallback_detect(values)

    signal = np.array(values, dtype=float)
    if len(signal) < min_size * 2:
        return {"changepoints": [], "confidence": [], "algorithm": "PELT"}

    algo = rpt.Pelt(model="rbf", min_size=min_size).fit(signal)
    result = algo.predict(pen=penalty)

    # Remove the last element (always = len(signal))
    changepoints = [cp for cp in result if cp < len(signal)]

    # Calculate confidence for each changepoint
    confidences = []
    for cp in changepoints:
        left = signal[max(0, cp - min_size):cp]
        right = signal[cp:min(len(signal), cp + min_size)]
        if len(left) > 0 and len(right) > 0:
            diff = abs(np.mean(right) - np.mean(left))
            std = max(np.std(signal), 0.01)
            conf = min(diff / std, 1.0)
            confidences.append(round(conf, 3))
        else:
            confidences.append(0.0)

    return {
        "changepoints": changepoints,
        "confidence": confidences,
        "algorithm": "PELT",
    }


def fallback_detect(values):
    """Simple window-based fallback when ruptures unavailable."""
    if len(values) < 10:
        return {"changepoints": [], "confidence": [], "algorithm": "fallback"}

    signal = np.array(values, dtype=float)
    window = max(5, len(signal) // 5)
    changepoints = []
    confidences = []

    for i in range(window, len(signal) - window):
        left_mean = np.mean(signal[i - window:i])
        right_mean = np.mean(signal[i:i + window])
        std = max(np.std(signal), 0.01)
        diff = abs(right_mean - left_mean)
        if diff / std > 1.5:
            changepoints.append(i)
            confidences.append(min(round(diff / std / 3.0, 3), 1.0))

    # Deduplicate nearby changepoints
    filtered_cp = []
    filtered_conf = []
    for cp, conf in zip(changepoints, confidences):
        if not filtered_cp or cp - filtered_cp[-1] >= window:
            filtered_cp.append(cp)
            filtered_conf.append(conf)

    return {
        "changepoints": filtered_cp,
        "confidence": filtered_conf,
        "algorithm": "fallback",
    }


@evolution_bp.route("/health", methods=["GET"])
def evolution_health():
    """Health check for evolution engine subsystem."""
    return jsonify({
        "status": "ok",
        "ruptures_available": HAS_RUPTURES,
    })


@evolution_bp.route("/detect-changepoints", methods=["POST"])
def detect_changepoints():
    """
    Detect changepoints in a time-series of player metrics.

    Request body:
        values: List[float] — metric values in chronological order.
        min_size: int (optional, default 5) — minimum segment length.
        penalty: float (optional, default 3.0) — PELT penalty.
        player_id: str (optional) — for logging.
        metric: str (optional) — metric name for logging.

    Returns:
        changepoints: List[int] — indices where regime changes detected.
        confidence: List[float] — confidence score per changepoint.
        algorithm: str — 'PELT' or 'fallback'.
    """
    data = request.get_json()
    if not data:
        return jsonify({"error": "JSON body required"}), 400

    values = data.get("values", [])
    if not values or len(values) < 5:
        return jsonify({
            "changepoints": [],
            "confidence": [],
            "algorithm": "PELT",
            "note": "Insufficient data points",
        })

    result = detect_changepoints_pelt(
        values,
        min_size=data.get("min_size", 5),
        penalty=data.get("penalty", 3.0),
    )
    result["player_id"] = data.get("player_id")
    result["metric"] = data.get("metric")

    return jsonify(result)


# ============================================================
# SUPPLEMENT: Player Evolution Alerting
# ============================================================

import json
import logging
from datetime import date

logger = logging.getLogger('vyndr')

# Metrics to scan per sport
EVOLUTION_METRICS = {
    'nba': ['usage_rate', 'assist_rate', 'three_pa_rate', 'fg_pct', 'minutes'],
    'mlb': ['k_rate', 'bb_rate', 'exit_velocity', 'hard_hit_pct', 'fb_velo']
}

EVOLUTION_MIN_GAMES = 15
EVOLUTION_CHANGE_THRESHOLD = 0.10  # 10% change
EVOLUTION_MIN_CONCURRENT = 2  # 2+ metrics must inflect


def detect_player_evolution(player_id, sport, metric_data=None):
    """
    Use PELT to detect inflection points across multiple metrics simultaneously.
    Flag PLAYER_EVOLUTION_DETECTED when 2+ metrics show concurrent inflection
    (10%+ change in last 5 games vs prior window, minimum 15 games total).

    Args:
        player_id: Player identifier.
        sport: 'nba' or 'mlb'.
        metric_data: Optional dict mapping metric name to list of values.
            If None, would be fetched from data warehouse in production.

    Returns:
        Dict with evolution_detected (bool) and inflection details if detected.
    """
    metrics = EVOLUTION_METRICS.get(sport, [])
    inflections = {}

    for metric in metrics:
        if metric_data and metric in metric_data:
            values = metric_data[metric]
        else:
            values = _get_player_metric_series(player_id, metric)

        if len(values) < EVOLUTION_MIN_GAMES:
            continue

        result = detect_changepoints_pelt(values)
        changepoints = result.get('changepoints', [])

        if changepoints:
            latest_cp = max(changepoints)
            # Inflection must be in last 5 games of the series
            if latest_cp >= len(values) - 5:
                before_vals = values[:latest_cp]
                after_vals = values[latest_cp:]
                if before_vals and after_vals:
                    before_mean = float(np.mean(before_vals))
                    after_mean = float(np.mean(after_vals))
                    denominator = max(abs(before_mean), 0.01)
                    pct_change = (after_mean - before_mean) / denominator

                    if abs(pct_change) > EVOLUTION_CHANGE_THRESHOLD:
                        inflections[metric] = {
                            'before': round(before_mean, 3),
                            'after': round(after_mean, 3),
                            'change_pct': round(pct_change * 100, 1),
                            'direction': 'ascending' if pct_change > 0 else 'descending',
                            'changepoint_game': latest_cp
                        }

    if len(inflections) >= EVOLUTION_MIN_CONCURRENT:
        return {
            'evolution_detected': True,
            'player_id': player_id,
            'sport': sport,
            'detection_date': date.today().isoformat(),
            'metrics_inflecting': len(inflections),
            'inflections': inflections,
        }

    return {'evolution_detected': False, 'player_id': player_id}


def log_evolution_detection(evolution):
    """
    Create timestamped, verifiable record in evolution_detections table.
    After one season: 'We detected X inflection points. Y confirmed by market movement.'

    Args:
        evolution: Dict from detect_player_evolution with evolution_detected=True.
    """
    try:
        from utils.supabase_client import get_supabase_client
        supabase = get_supabase_client()
        if supabase:
            supabase.table('evolution_detections').insert({
                'player_id': evolution['player_id'],
                'player_name': evolution.get('player_name'),
                'sport': evolution['sport'],
                'detection_date': evolution['detection_date'],
                'metrics': json.dumps(evolution['inflections']),
                'market_adjusted_at': None,
                'confirmed': None
            }).execute()
    except Exception as e:
        logger.warning(f'[VYNDR] Evolution detection log failed: {e}')


def format_evolution_watch_post(evolutions):
    """
    Weekly content: 'VYNDR Evolution Watch'
    Players whose stats are inflecting before market adjustment.

    Args:
        evolutions: List of evolution detection dicts.

    Returns:
        Formatted post string, or None if no evolutions.
    """
    if not evolutions:
        return None

    lines = ["\U0001f52c VYNDR Evolution Watch\n"]
    lines.append("Players inflecting before the market catches up:\n")

    for evo in evolutions[:5]:
        metrics = evo.get('inflections', {})
        if not metrics:
            continue
        top_metric = max(metrics.items(), key=lambda x: abs(x[1]['change_pct']))
        direction = '\U0001f4c8' if top_metric[1]['direction'] == 'ascending' else '\U0001f4c9'

        lines.append(
            f"{direction} {evo.get('player_name', evo['player_id'])} \u2014 "
            f"{top_metric[0]}: {top_metric[1]['before']} \u2192 {top_metric[1]['after']} "
            f"({top_metric[1]['change_pct']:+.1f}%)"
        )

    lines.append("\nThe model sees it. The market hasn't priced it yet.")
    return '\n'.join(lines)


def _get_player_metric_series(player_id, metric, n_games=30):
    """Stub: fetch player metric time series from data warehouse."""
    return []


@evolution_bp.route("/scan/<sport>", methods=["GET"])
def scan_for_evolutions(sport):
    """
    Scan all active players for evolution inflection points.
    Run daily. Creates timestamped records for accuracy ledger.

    Args:
        sport: 'nba' or 'mlb'.

    Returns:
        JSON with scan results and detected evolutions.
    """
    # In production, get_active_players fetches from Supabase
    evolutions = []
    return jsonify({
        'sport': sport,
        'scan_date': date.today().isoformat(),
        'evolutions_detected': len(evolutions),
        'evolutions': evolutions,
        'note': 'Connect to player data source for live scanning'
    })


@evolution_bp.route("/watch-post", methods=["GET"])
def get_evolution_watch():
    """Get formatted Evolution Watch post for capper account."""
    return jsonify({
        'post': None,
        'note': 'No evolutions detected yet'
    })


# ============================================================
# PATCH Item 8: Evolution Persistence Check
# ============================================================

EVOLUTION_PERSISTENCE_GAMES = 3  # games before public promotion


def promote_evolution_to_public(evolution_record, games_since_detection):
    """
    Evolution detected internally on day X.
    Only promote to Evolution Watch content after 3 games of persistence.
    If inflection didn't hold, mark as false positive.

    Args:
        evolution_record: Dict with player_id, detection_date, inflections.
        games_since_detection: Number of games played since detection.

    Returns:
        Dict with promoted (bool) and reason.
    """
    if games_since_detection < EVOLUTION_PERSISTENCE_GAMES:
        return {
            'promoted': False,
            'reason': f'Persistence check: {games_since_detection}/{EVOLUTION_PERSISTENCE_GAMES} games'
        }

    # Check if inflection held (would verify against recent data in production)
    still_inflecting = verify_inflection_persists(evolution_record)

    if still_inflecting:
        return {'promoted': True, 'games_verified': games_since_detection}
    else:
        return {
            'promoted': False,
            'reason': 'Inflection did not persist — false positive',
            'false_positive': True
        }


def verify_inflection_persists(evolution_record):
    """
    Verify that detected inflection points are still present in recent data.
    Returns True if the change direction is maintained.

    Args:
        evolution_record: Dict with inflections data.

    Returns:
        True if inflection persists, False if reverted.
    """
    inflections = evolution_record.get('inflections', {})
    if not inflections:
        return False
    # In production: re-fetch recent metric values and compare to post-inflection mean
    # For now, stub returns True (would be replaced with actual data check)
    return True


def scan_for_evolutions_internal(sport):
    """
    Internal version of evolution scan called by nightly resolution.
    Does not require Flask request context.

    Args:
        sport: 'nba' or 'mlb'.
    """
    logger.info(f'[VYNDR] Running evolution scan for {sport}')
    # In production: iterate active players, call detect_player_evolution
