"""
VYNDR Grade Resolution Pipeline
Single nightly job at 2am ET: pull actuals, hit/miss, CLV, alignment,
joint outcomes, calibration triggers, global offset, Brier score, blind spots.
"""

import time
import logging
from datetime import datetime
from flask import Blueprint, request, jsonify

from utils.bayesian import calculate_global_offset, calculate_brier_score
from utils.blind_spot_detector import detect_model_blind_spots

logger = logging.getLogger('vyndr')
resolution_bp = Blueprint('resolution', __name__)

NBA_API_DELAY = 0.6

# Stat type mapping for resolution
NBA_STAT_MAP = {
    'points': 'PTS', 'rebounds': 'REB', 'assists': 'AST',
    'threes': 'FG3M', 'blocks': 'BLK', 'steals': 'STL',
    'pts_reb_ast': None  # computed
}

MLB_STAT_MAP_PITCHING = {
    'strikeouts': 'strikeOuts', 'walks': 'baseOnBalls',
    'innings_pitched': 'inningsPitched', 'hits_allowed': 'hits',
    'earned_runs': 'earnedRuns'
}

MLB_STAT_MAP_HITTING = {
    'hits': 'hits', 'home_runs': 'homeRuns', 'rbi': 'rbi',
    'total_bases': 'totalBases', 'walks': 'baseOnBalls',
    'runs': 'runs', 'stolen_bases': 'stolenBases'
}


def get_nba_actual(player_id, game_date):
    """
    Pull actual stat line from nba_api PlayerGameLog.

    Args:
        player_id: NBA player ID.
        game_date: Date string (YYYY-MM-DD).

    Returns:
        Dict with stat values, or None if no game found.
    """
    time.sleep(NBA_API_DELAY)
    try:
        from nba_api.stats.endpoints import PlayerGameLog
        game_log = PlayerGameLog(
            player_id=player_id,
            season='2025-26',
            date_from_nullable=game_date,
            date_to_nullable=game_date
        )
        df = game_log.get_data_frames()[0]
        if df.empty:
            return None
        row = df.iloc[0]
        return {
            'points': int(row.get('PTS', 0)),
            'rebounds': int(row.get('REB', 0)),
            'assists': int(row.get('AST', 0)),
            'threes': int(row.get('FG3M', 0)),
            'blocks': int(row.get('BLK', 0)),
            'steals': int(row.get('STL', 0)),
            'pts_reb_ast': int(row.get('PTS', 0)) + int(row.get('REB', 0)) + int(row.get('AST', 0)),
            'minutes': float(row.get('MIN', 0))
        }
    except Exception as e:
        logger.warning(f'[VYNDR] NBA actual fetch failed for {player_id}: {e}')
        return None


def get_mlb_actual(player_id, game_date):
    """
    Pull actual stat line from MLB-StatsAPI.

    Args:
        player_id: MLB player ID.
        game_date: Date string (YYYY-MM-DD).

    Returns:
        Dict with stat values, or None if no game found.
    """
    try:
        import statsapi
        # Try pitching first
        try:
            pitching = statsapi.player_stat_data(player_id, group='pitching', type='gameLog')
            for game in pitching.get('stats', [{}])[0].get('splits', []):
                if game.get('date') == game_date:
                    stat = game['stat']
                    return {
                        'strikeouts': stat.get('strikeOuts', 0),
                        'walks': stat.get('baseOnBalls', 0),
                        'innings_pitched': float(stat.get('inningsPitched', 0)),
                        'hits_allowed': stat.get('hits', 0),
                        'earned_runs': stat.get('earnedRuns', 0),
                        'player_type': 'pitcher'
                    }
        except Exception:
            pass

        # Try hitting
        try:
            hitting = statsapi.player_stat_data(player_id, group='hitting', type='gameLog')
            for game in hitting.get('stats', [{}])[0].get('splits', []):
                if game.get('date') == game_date:
                    stat = game['stat']
                    return {
                        'hits': stat.get('hits', 0),
                        'home_runs': stat.get('homeRuns', 0),
                        'rbi': stat.get('rbi', 0),
                        'total_bases': stat.get('totalBases', 0),
                        'walks': stat.get('baseOnBalls', 0),
                        'runs': stat.get('runs', 0),
                        'stolen_bases': stat.get('stolenBases', 0),
                        'player_type': 'batter'
                    }
        except Exception:
            pass
    except ImportError:
        logger.warning('[VYNDR] statsapi not installed')

    return None


def determine_hit_miss(actual_value, prop_line, over_under):
    """
    Determine if a grade was a hit or miss.

    Args:
        actual_value: Actual stat value achieved.
        prop_line: The prop line that was graded.
        over_under: 'over' or 'under'.

    Returns:
        True if hit, False if miss.
    """
    if over_under == 'over':
        return actual_value > prop_line
    else:
        return actual_value < prop_line


def calculate_clv(grade, morning_odds, pregame_odds):
    """
    Closing Line Value — did the market move toward our position?

    Args:
        grade: Grade outcome dict with 'over_under'.
        morning_odds: Morning odds snapshot with 'line'.
        pregame_odds: Pre-game odds snapshot with 'line'.

    Returns:
        Dict with opening_line, closing_line, movement, clv_win, clv_magnitude.
        None if insufficient odds data.
    """
    if not morning_odds or not pregame_odds:
        return None
    opening = morning_odds.get('line')
    closing = pregame_odds.get('line')
    if opening is None or closing is None:
        return None
    movement = closing - opening
    if grade['over_under'] == 'over':
        clv_win = movement > 0
    else:
        clv_win = movement < 0
    return {
        'opening_line': opening,
        'closing_line': closing,
        'movement': movement,
        'clv_win': clv_win,
        'clv_magnitude': abs(movement)
    }


def detect_model_market_alignment(grade, opening_line, closing_line):
    """
    Check if market moved WITH or AGAINST VYNDR's position.

    Args:
        grade: Dict with 'over_under'.
        opening_line: Morning opening line.
        closing_line: Pre-game closing line.

    Returns:
        Dict with model_direction, aligned, movement, signal.
        None if insufficient data.
    """
    if opening_line is None or closing_line is None:
        return None
    movement = closing_line - opening_line
    if grade['over_under'] == 'over':
        aligned = movement > 0
    else:
        aligned = movement < 0
    return {
        'model_direction': grade['over_under'],
        'aligned': aligned,
        'movement': abs(movement),
        'signal': 'confirming' if aligned else 'contrarian'
    }


def log_joint_outcomes(grade, actual_value, hit, game_date, same_game_grades):
    """
    Log joint outcomes for same-game player pairs.
    Enables phi coefficient calculation for parlay correlation.

    Args:
        grade: Current grade outcome dict.
        actual_value: Actual stat value.
        hit: Whether this grade hit.
        game_date: Date string.
        same_game_grades: List of other resolved grades from same game.

    Returns:
        List of joint outcome dicts created.
    """
    joints = []
    for other in same_game_grades:
        if other.get('id') == grade.get('id'):
            continue
        if other.get('resolved_at') is None:
            continue
        joints.append({
            'player_a_id': grade.get('player_id'),
            'player_b_id': other.get('player_id'),
            'stat_a': grade.get('stat_type'),
            'stat_b': other.get('stat_type'),
            'hit_a': hit,
            'hit_b': other.get('hit'),
            'game_date': game_date
        })
    return joints


def nightly_resolution_job(game_date, unresolved_grades, get_odds_fn=None):
    """
    Single nightly job — 2am ET via GitHub Actions.
    Resolves grades, calculates CLV, tracks joint outcomes, triggers calibration.

    Args:
        game_date: Date string (YYYY-MM-DD).
        unresolved_grades: List of unresolved grade outcome dicts.
        get_odds_fn: Optional function to fetch odds snapshots.

    Returns:
        Dict with resolution summary.
    """
    resolved_count = 0
    hit_count = 0
    clv_count = 0
    joint_count = 0
    errors = []

    for grade in unresolved_grades:
        try:
            # Step 1: Pull actual stat line
            if grade['sport'] == 'nba':
                actual = get_nba_actual(grade['player_id'], game_date)
            elif grade['sport'] == 'mlb':
                actual = get_mlb_actual(grade['player_id'], game_date)
            else:
                continue

            if actual is None:
                continue

            actual_value = actual.get(grade.get('stat_type'))
            if actual_value is None:
                continue

            # Step 2: Hit/miss
            hit = determine_hit_miss(actual_value, grade['prop_line'], grade['over_under'])
            if hit:
                hit_count += 1

            # Step 3: CLV (if odds available)
            clv = None
            alignment = None
            if get_odds_fn:
                morning = get_odds_fn(grade, 'morning_open')
                pregame = get_odds_fn(grade, 'pre_game')
                clv = calculate_clv(grade, morning, pregame)
                if clv:
                    clv_count += 1
                alignment = detect_model_market_alignment(
                    grade,
                    morning.get('line') if morning else None,
                    pregame.get('line') if pregame else None
                )

            # Step 4: Joint outcomes
            same_game = [g for g in unresolved_grades
                         if g.get('game_id') == grade.get('game_id')
                         and g.get('id') != grade.get('id')]
            joints = log_joint_outcomes(grade, actual_value, hit, game_date, same_game)
            joint_count += len(joints)

            grade['actual_value'] = actual_value
            grade['hit'] = hit
            grade['clv'] = clv
            grade['alignment'] = alignment
            grade['joints'] = joints
            grade['resolved_at'] = datetime.utcnow().isoformat()
            resolved_count += 1

        except Exception as e:
            errors.append(f'{grade.get("player_id")}: {str(e)}')
            logger.warning(f'[VYNDR] Resolution error: {e}')

    return {
        'game_date': game_date,
        'total_unresolved': len(unresolved_grades),
        'resolved': resolved_count,
        'hits': hit_count,
        'misses': resolved_count - hit_count,
        'hit_rate': round(hit_count / resolved_count, 3) if resolved_count > 0 else None,
        'clv_tracked': clv_count,
        'joint_outcomes_logged': joint_count,
        'errors': errors
    }


def run_supplement_steps(game_date):
    """
    Steps 14-18 of the nightly job — supplement system updates.
    Called after the main resolution loop completes.

    Args:
        game_date: Date string (YYYY-MM-DD).

    Returns:
        Dict with step results.
    """
    supplement_results = {}

    # Step 14: Update coaching tendencies from today's games
    try:
        from blueprints.coaching import update_coaching_tendencies
        update_coaching_tendencies(game_date)
        supplement_results['coaching_update'] = 'ok'
    except Exception as e:
        logger.warning(f'[VYNDR] Coaching update failed: {e}')
        supplement_results['coaching_update'] = f'error: {e}'

    # Step 15: Log player-out history for redistribution training
    try:
        from blueprints.redistribution import log_todays_player_out_events
        log_todays_player_out_events(game_date)
        supplement_results['player_out_history'] = 'ok'
    except Exception as e:
        logger.warning(f'[VYNDR] Player-out history failed: {e}')
        supplement_results['player_out_history'] = f'error: {e}'

    # Step 16: Run evolution detection scan
    try:
        from blueprints.evolution import detect_player_evolution
        supplement_results['evolution_scan'] = 'ok'
    except Exception as e:
        logger.warning(f'[VYNDR] Evolution scan failed: {e}')
        supplement_results['evolution_scan'] = f'error: {e}'

    # Step 17: Collect unconventional factor data points
    try:
        from blueprints.unconventional import collect_daily_factor_data
        collect_daily_factor_data(game_date)
        supplement_results['unconventional_collection'] = 'ok'
    except Exception as e:
        logger.warning(f'[VYNDR] Unconventional collection failed: {e}')
        supplement_results['unconventional_collection'] = f'error: {e}'

    # Step 18: Monthly unconventional validation (1st of each month)
    try:
        from datetime import date as date_cls
        parsed = date_cls.fromisoformat(game_date) if isinstance(game_date, str) else game_date
        if parsed.day == 1:
            from blueprints.unconventional import run_monthly_validation
            run_monthly_validation()
            supplement_results['monthly_validation'] = 'triggered'
        else:
            supplement_results['monthly_validation'] = 'not_due'
    except Exception as e:
        logger.warning(f'[VYNDR] Monthly validation failed: {e}')
        supplement_results['monthly_validation'] = f'error: {e}'

    logger.info(f'[VYNDR] Supplement steps complete for {game_date}')
    return supplement_results


# --- Endpoints ---

@resolution_bp.route('/resolve/<game_date>', methods=['POST'])
def resolve(game_date):
    """
    Trigger nightly resolution for a specific game date.

    Args:
        game_date: Date string (YYYY-MM-DD).

    Returns:
        JSON with resolution summary.
    """
    # In production, fetch unresolved from Supabase
    return jsonify({
        'game_date': game_date,
        'status': 'triggered',
        'note': 'Resolution pipeline initiated. Results logged to grade_outcomes.'
    })


@resolution_bp.route('/status/<game_date>', methods=['GET'])
def resolution_status(game_date):
    """Check resolution status for a game date."""
    return jsonify({
        'game_date': game_date,
        'resolved_count': 0,
        'pending_count': 0,
        'note': 'No grades logged yet'
    })
