"""
VYNDR Odds Scanner — Blueprint
Fetches, parses, stores, and analyzes odds from The Odds API.
Manages scan scheduling, line movement detection, and full-slate grading.
"""

import os
import logging
from datetime import datetime, timezone

import requests
from flask import Blueprint, request, jsonify

from utils.data_warehouse import (
    store_odds_batch,
    fetch_odds_by_date,
    fetch_odds_by_scan_type,
)
from utils.retry import retry_with_backoff
from utils.edge_calculator import calculate_real_edge, grade_edge

logger = logging.getLogger(__name__)

odds_bp = Blueprint('odds_scanner', __name__)

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

ODDS_API_BASE = 'https://api.the-odds-api.com/v4/sports'
ODDS_API_KEY = os.environ.get('ODDS_API_KEY')

SPORT_KEYS = {
    'nba': 'basketball_nba',
    'mlb': 'baseball_mlb',
}

# Free-tier scan strategy — maximizes coverage on 2 pulls/day
ODDS_SCAN_STRATEGY = {
    'morning_scan': '10:00 AM ET',
    'pre_game_scan': '90min before first game',
    'max_daily_pulls': 2,
    'priority': 'games_with_confirmed_lineups_first',
    'market_priority': [
        'player_points',
        'player_rebounds',
        'player_assists',
        'player_threes',
        'player_points_rebounds_assists',
        'player_strikeouts',
        'player_hits',
        'player_total_bases',
    ],
}


# ---------------------------------------------------------------------------
# Core functions
# ---------------------------------------------------------------------------

@retry_with_backoff(max_retries=3, base_delay=2.0)
def fetch_player_props(sport, scan_type='morning_open'):
    """
    Fetch player prop odds from The Odds API for a given sport.

    Pulls markets based on ODDS_SCAN_STRATEGY priority, parses them into
    a flat prop list, and stores the batch in the odds warehouse.

    Args:
        sport: Sport key (e.g. 'nba', 'mlb').
        scan_type: One of 'morning_open' or 'pre_game'.

    Returns:
        dict with 'props_stored' count and 'api_requests_remaining'.

    Raises:
        ValueError: If sport is not supported or API key is missing.
        requests.exceptions.RequestException: On network failures (retried).
    """
    if not ODDS_API_KEY:
        raise ValueError('ODDS_API_KEY environment variable is not set')

    sport_key = SPORT_KEYS.get(sport)
    if not sport_key:
        raise ValueError(f'Unsupported sport: {sport}. Supported: {list(SPORT_KEYS.keys())}')

    markets = ','.join(ODDS_SCAN_STRATEGY['market_priority'])

    url = f'{ODDS_API_BASE}/{sport_key}/odds'
    params = {
        'apiKey': ODDS_API_KEY,
        'regions': 'us',
        'markets': markets,
        'oddsFormat': 'american',
    }

    logger.info('Fetching props for %s (scan_type=%s)', sport, scan_type)
    response = requests.get(url, params=params, timeout=30)
    response.raise_for_status()

    api_requests_remaining = response.headers.get('x-requests-remaining', 'unknown')
    logger.info('API requests remaining: %s', api_requests_remaining)

    raw_games = response.json()
    props = parse_odds_response(raw_games, sport)

    stored_count = store_in_odds_warehouse(props, sport, scan_type)

    return {
        'props_stored': stored_count,
        'api_requests_remaining': api_requests_remaining,
    }


def parse_odds_response(response, sport):
    """
    Parse raw Odds API response into a flat list of prop dicts.

    Walks the bookmaker -> market -> outcome hierarchy and normalizes
    each outcome into a consistent shape for downstream analysis.

    Args:
        response: List of game objects from The Odds API.
        sport: Sport key for tagging.

    Returns:
        List of dicts, each representing a single prop line:
        {
            'game_id', 'home_team', 'away_team', 'commence_time',
            'bookmaker', 'market', 'player', 'line', 'over_price',
            'under_price', 'sport', 'fetched_at'
        }
    """
    props = []
    fetched_at = datetime.now(timezone.utc).isoformat()

    for game in response:
        game_id = game.get('id')
        home_team = game.get('home_team')
        away_team = game.get('away_team')
        commence_time = game.get('commence_time')

        for bookmaker in game.get('bookmakers', []):
            bookmaker_key = bookmaker.get('key')

            for market in bookmaker.get('markets', []):
                market_key = market.get('key')
                outcomes = market.get('outcomes', [])

                # Outcomes come in Over/Under pairs — group by player + line
                outcome_map = {}
                for outcome in outcomes:
                    player = outcome.get('description', 'unknown')
                    point = outcome.get('point')
                    side = outcome.get('name', '').lower()  # 'over' or 'under'
                    price = outcome.get('price')

                    key = (player, point)
                    if key not in outcome_map:
                        outcome_map[key] = {
                            'player': player,
                            'line': point,
                            'over_price': None,
                            'under_price': None,
                        }

                    if side == 'over':
                        outcome_map[key]['over_price'] = price
                    elif side == 'under':
                        outcome_map[key]['under_price'] = price

                for (player, line), data in outcome_map.items():
                    props.append({
                        'game_id': game_id,
                        'home_team': home_team,
                        'away_team': away_team,
                        'commence_time': commence_time,
                        'bookmaker': bookmaker_key,
                        'market': market_key,
                        'player': data['player'],
                        'line': data['line'],
                        'over_price': data['over_price'],
                        'under_price': data['under_price'],
                        'sport': sport,
                        'fetched_at': fetched_at,
                    })

    logger.info('Parsed %d props from %d games', len(props), len(response))
    return props


def store_in_odds_warehouse(props, sport, scan_type):
    """
    Persist parsed props to the Supabase odds_warehouse table.

    Each row is tagged with sport, scan_type, and insertion timestamp
    to enable historical comparison and line movement detection.

    Args:
        props: List of prop dicts from parse_odds_response.
        sport: Sport key.
        scan_type: 'morning_open' or 'pre_game'.

    Returns:
        int — number of rows stored.
    """
    if not props:
        logger.warning('No props to store for %s (%s)', sport, scan_type)
        return 0

    rows = []
    for prop in props:
        rows.append({
            **prop,
            'scan_type': scan_type,
            'stored_at': datetime.now(timezone.utc).isoformat(),
        })

    try:
        result = store_odds_batch(rows)
        stored = result.get('count', len(rows))
        logger.info('Stored %d props to odds_warehouse (%s / %s)', stored, sport, scan_type)
        return stored
    except Exception:
        logger.exception('Failed to store props for %s (%s)', sport, scan_type)
        raise


def detect_line_movements(sport, threshold=0.5):
    """
    Compare morning_open vs pre_game scans and flag significant line moves.

    A movement exceeding the threshold triggers a regrade of the affected
    prop, since the edge calculation may have shifted.

    Args:
        sport: Sport key.
        threshold: Minimum absolute line change to flag (default 0.5).

    Returns:
        List of dicts describing each significant movement:
        {
            'player', 'market', 'game_id',
            'morning_line', 'pregame_line', 'movement',
            'morning_over', 'pregame_over', 'price_shift',
            'regrade_triggered'
        }
    """
    try:
        morning_props = fetch_odds_by_scan_type(sport, 'morning_open')
        pregame_props = fetch_odds_by_scan_type(sport, 'pre_game')
    except Exception:
        logger.exception('Failed to fetch scans for movement detection (%s)', sport)
        raise

    # Index morning props by (player, market, game_id) for fast lookup
    morning_index = {}
    for prop in morning_props:
        key = (prop['player'], prop['market'], prop['game_id'])
        morning_index[key] = prop

    movements = []
    for prop in pregame_props:
        key = (prop['player'], prop['market'], prop['game_id'])
        morning = morning_index.get(key)
        if not morning:
            continue

        morning_line = morning.get('line') or 0
        pregame_line = prop.get('line') or 0
        line_movement = abs(pregame_line - morning_line)

        morning_over = morning.get('over_price') or 0
        pregame_over = prop.get('over_price') or 0
        price_shift = pregame_over - morning_over

        if line_movement >= threshold:
            regrade_triggered = True
            logger.info(
                'Line movement detected: %s %s — %.1f -> %.1f (delta %.1f)',
                prop['player'], prop['market'], morning_line, pregame_line, line_movement,
            )
        else:
            regrade_triggered = False

        if line_movement >= threshold or abs(price_shift) >= 15:
            movements.append({
                'player': prop['player'],
                'market': prop['market'],
                'game_id': prop['game_id'],
                'morning_line': morning_line,
                'pregame_line': pregame_line,
                'movement': round(pregame_line - morning_line, 2),
                'morning_over': morning_over,
                'pregame_over': pregame_over,
                'price_shift': price_shift,
                'regrade_triggered': regrade_triggered,
            })

    logger.info('Detected %d significant movements for %s', len(movements), sport)
    return movements


def scan_full_slate(sport):
    """
    Grade every prop on tonight's slate and return ranked results.

    Fetches the latest pre_game scan (or morning_open if pre_game is
    unavailable), calculates real edge for each prop, assigns a letter
    grade, and sorts by descending edge. The capper account only posts
    plays graded A- and above.

    Args:
        sport: Sport key.

    Returns:
        dict with 'total_props', 'postable_plays' (A- and above),
        and 'full_slate' (all graded props sorted by edge).
    """
    try:
        props = fetch_odds_by_scan_type(sport, 'pre_game')
        if not props:
            props = fetch_odds_by_scan_type(sport, 'morning_open')
    except Exception:
        logger.exception('Failed to fetch props for full slate scan (%s)', sport)
        raise

    if not props:
        return {'total_props': 0, 'postable_plays': [], 'full_slate': []}

    graded = []
    for prop in props:
        try:
            edge_result = calculate_real_edge(prop)
            real_edge = edge_result.get('edge', 0)
            grade = grade_edge(real_edge)

            graded.append({
                'player': prop.get('player'),
                'market': prop.get('market'),
                'game_id': prop.get('game_id'),
                'home_team': prop.get('home_team'),
                'away_team': prop.get('away_team'),
                'line': prop.get('line'),
                'over_price': prop.get('over_price'),
                'under_price': prop.get('under_price'),
                'bookmaker': prop.get('bookmaker'),
                'real_edge': round(real_edge, 4),
                'grade': grade,
            })
        except Exception:
            logger.warning('Failed to grade prop: %s %s', prop.get('player'), prop.get('market'))
            continue

    # Sort by real edge descending
    graded.sort(key=lambda p: p['real_edge'], reverse=True)

    # Capper account only posts A- and above
    postable_grades = {'A+', 'A', 'A-'}
    postable = [p for p in graded if p['grade'] in postable_grades]

    logger.info(
        'Slate scan complete for %s: %d total, %d postable',
        sport, len(graded), len(postable),
    )

    return {
        'total_props': len(graded),
        'postable_plays': postable,
        'full_slate': graded,
    }


# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------

@odds_bp.route('/scan/<sport>', methods=['GET'])
def route_scan_slate(sport):
    """
    GET /scan/<sport>
    Scan the full slate for a sport. Returns graded props ranked by edge.
    Query params:
        fetch (bool): If true, fetch fresh odds before scanning. Default false.
    """
    try:
        if sport not in SPORT_KEYS:
            return jsonify({'error': f'Unsupported sport: {sport}'}), 400

        fetch_fresh = request.args.get('fetch', 'false').lower() == 'true'
        scan_type = request.args.get('scan_type', 'morning_open')

        if fetch_fresh:
            fetch_result = fetch_player_props(sport, scan_type=scan_type)
            logger.info('Fresh fetch completed: %s', fetch_result)

        result = scan_full_slate(sport)
        return jsonify(result), 200

    except ValueError as e:
        return jsonify({'error': str(e)}), 400
    except requests.exceptions.RequestException as e:
        logger.exception('Odds API request failed')
        return jsonify({'error': 'Odds API request failed', 'detail': str(e)}), 502
    except Exception as e:
        logger.exception('Unexpected error in scan route')
        return jsonify({'error': 'Internal server error'}), 500


@odds_bp.route('/movements/<sport>', methods=['GET'])
def route_line_movements(sport):
    """
    GET /movements/<sport>
    Compare morning vs pre-game scans and return significant line movements.
    Query params:
        threshold (float): Minimum line change to flag. Default 0.5.
    """
    try:
        if sport not in SPORT_KEYS:
            return jsonify({'error': f'Unsupported sport: {sport}'}), 400

        threshold = float(request.args.get('threshold', 0.5))
        movements = detect_line_movements(sport, threshold=threshold)

        return jsonify({
            'sport': sport,
            'threshold': threshold,
            'count': len(movements),
            'movements': movements,
        }), 200

    except ValueError as e:
        return jsonify({'error': str(e)}), 400
    except Exception as e:
        logger.exception('Unexpected error in movements route')
        return jsonify({'error': 'Internal server error'}), 500


@odds_bp.route('/warehouse/<sport>/<game_date>', methods=['GET'])
def route_warehouse_lookup(sport, game_date):
    """
    GET /warehouse/<sport>/<game_date>
    Retrieve stored odds from the warehouse for a given sport and date.
    game_date format: YYYY-MM-DD
    Query params:
        scan_type (str): Filter by scan type. Optional.
        market (str): Filter by market key. Optional.
    """
    try:
        if sport not in SPORT_KEYS:
            return jsonify({'error': f'Unsupported sport: {sport}'}), 400

        # Validate date format
        try:
            datetime.strptime(game_date, '%Y-%m-%d')
        except ValueError:
            return jsonify({'error': 'Invalid date format. Use YYYY-MM-DD.'}), 400

        scan_type = request.args.get('scan_type')
        market = request.args.get('market')

        props = fetch_odds_by_date(sport, game_date)

        # Apply optional filters
        if scan_type:
            props = [p for p in props if p.get('scan_type') == scan_type]
        if market:
            props = [p for p in props if p.get('market') == market]

        return jsonify({
            'sport': sport,
            'game_date': game_date,
            'count': len(props),
            'props': props,
        }), 200

    except Exception as e:
        logger.exception('Unexpected error in warehouse route')
        return jsonify({'error': 'Internal server error'}), 500


# ============================================================
# SUPPLEMENT: Alt Line Scanner
# ============================================================

ALT_LINE_EDGE_IMPROVEMENT_THRESHOLD = 0.03  # 3% edge improvement minimum
ALT_LINE_MODE = os.environ.get('ALT_LINE_MODE', 'manual')
# 'api' = pull from Odds API (requires paid tier with alt markets)
# 'manual' = generate probability ladder at common alt line intervals


def scan_alt_lines_internal(sport, player_name, stat_type, standard_grade=None):
    """
    Scan alt lines for a single prop. Finds the alt line with the best
    edge-to-odds ratio. Only recommends if edge exceeds standard by 3%+.

    Args:
        sport: 'nba' or 'mlb'.
        player_name: Player name string.
        stat_type: Stat type string.
        standard_grade: Optional pre-fetched grade result dict.

    Returns:
        Dict with eligible, optimal_alt, recommend_alt, all_positive_ev_alts.
    """
    if not standard_grade:
        return {'eligible': False, 'reason': 'No standard grade provided'}

    if standard_grade.get('grade') not in ['A+', 'A', 'A-']:
        return {'eligible': False, 'reason': 'Only runs on A-grade props'}

    model_projection = standard_grade.get('projected_value', 0)
    model_std = standard_grade.get('projected_std', 1)
    standard_edge = standard_grade.get('real_edge', {}).get('real_edge', 0)

    # Get alt lines from odds warehouse
    alt_lines = _get_alt_lines_from_warehouse(player_name, stat_type, sport)
    if not alt_lines:
        return {'eligible': True, 'alt_lines': [], 'reason': 'No alt lines available'}

    from utils.bayesian import norm_cdf
    from utils.edge_calculator import calculate_real_edge, kelly_criterion

    scored_alts = []
    for alt in alt_lines:
        alt_line = alt.get('line')
        alt_odds = alt.get('price', -110)
        over_under = alt.get('over_under', 'over')

        if alt_line is None or alt_odds is None:
            continue

        # Calculate model probability at this alt line
        if over_under == 'over':
            model_prob = 1 - norm_cdf(alt_line, model_projection, model_std)
        else:
            model_prob = norm_cdf(alt_line, model_projection, model_std)

        # Calculate real edge with vig
        edge = calculate_real_edge(model_prob, alt_odds)
        kelly = kelly_criterion(model_prob, alt_odds)

        if edge['is_positive_ev']:
            scored_alts.append({
                'line': alt_line,
                'odds': alt_odds,
                'over_under': over_under,
                'model_probability': round(model_prob, 3),
                'implied_probability': edge['implied_probability'],
                'real_edge': edge['real_edge'],
                'ev_per_dollar': edge['ev_per_dollar'],
                'kelly': kelly,
                'bookmaker': alt.get('bookmaker'),
                'edge_vs_standard': round(edge['real_edge'] - standard_edge, 3)
            })

    scored_alts.sort(key=lambda x: x['ev_per_dollar'], reverse=True)

    optimal = scored_alts[0] if scored_alts else None
    recommend = (optimal is not None and
                 optimal['edge_vs_standard'] >= ALT_LINE_EDGE_IMPROVEMENT_THRESHOLD)

    return {
        'eligible': True,
        'player': player_name,
        'stat_type': stat_type,
        'standard_grade': standard_grade.get('grade'),
        'standard_edge': standard_edge,
        'alt_lines_found': len(scored_alts),
        'optimal_alt': optimal,
        'recommend_alt': recommend,
        'all_positive_ev_alts': scored_alts[:5]
    }


def auto_scan_alt_lines_for_a_grades(sport, a_grades=None):
    """
    Called after slate scan. For every A-grade prop,
    automatically find the best alt line opportunity.

    Args:
        sport: 'nba' or 'mlb'.
        a_grades: Optional list of A-grade result dicts.

    Returns:
        List of alt line opportunity dicts where alt is recommended.
    """
    if not a_grades:
        return []

    alt_opportunities = []
    for grade in a_grades:
        result = scan_alt_lines_internal(
            sport,
            grade.get('player_name', ''),
            grade.get('stat_type', ''),
            standard_grade=grade
        )
        if result.get('recommend_alt') and result.get('optimal_alt'):
            alt_opportunities.append({
                'player': grade.get('player_name'),
                'standard': {
                    'line': grade.get('line'),
                    'grade': grade.get('grade'),
                    'edge': grade.get('real_edge', {}).get('real_edge')
                },
                'alt': result['optimal_alt'],
                'edge_improvement': result['optimal_alt']['edge_vs_standard']
            })

    return alt_opportunities


def _get_alt_lines_from_warehouse(player_name, stat_type, sport):
    """Stub: fetch alt lines from odds_warehouse table."""
    return []


@odds_bp.route('/alt-lines/<sport>/<player_name>/<stat_type>', methods=['GET'])
def scan_alt_lines_endpoint(sport, player_name, stat_type):
    """
    Scan alt lines for a specific player prop. Auto-runs on A-grade props.
    Finds the alt line with the best edge-to-odds ratio.

    Args:
        sport: 'nba' or 'mlb'.
        player_name: Player name.
        stat_type: Stat type.

    Returns:
        JSON with eligible, optimal_alt, recommend_alt, positive EV alts.
    """
    result = scan_alt_lines_internal(sport, player_name, stat_type)
    return jsonify(result)


# ---------------------------------------------------------------------------
# PATCH Item 10: Alt line ladder mode
# ---------------------------------------------------------------------------

def generate_alt_line_ladder(player_name, stat_type, sport, standard_grade=None):
    """
    When alt lines aren't available from API, generate a probability ladder
    showing model probability at each half-point from the standard line.
    User can then manually check their book for pricing.

    Args:
        player_name: Player name.
        stat_type: Stat type.
        sport: 'nba' or 'mlb'.
        standard_grade: Optional pre-fetched grade result.

    Returns:
        Dict with ladder of probabilities at common alt line offsets.
    """
    if not standard_grade:
        return {'eligible': False, 'reason': 'No standard grade'}

    from utils.bayesian import norm_cdf

    mean = standard_grade.get('projected_value', 0)
    std = standard_grade.get('projected_std', 1)
    base_line = standard_grade.get('line', 0)

    if std <= 0:
        return {'eligible': False, 'reason': 'Invalid projection std'}

    ladder = []
    for offset in [1, 1.5, 2, 2.5, 3, 4, 5]:
        over_line = base_line + offset
        under_line = base_line - offset
        prob_over = round(1 - norm_cdf(over_line, mean, std), 3)
        prob_under = round(norm_cdf(under_line, mean, std), 3)

        ladder.append({
            'over_line': over_line,
            'over_probability': prob_over,
            'under_line': under_line,
            'under_probability': prob_under,
            'offset': offset
        })

    return {
        'eligible': True,
        'mode': 'ladder',
        'standard_line': base_line,
        'projection': mean,
        'ladder': ladder,
        'note': 'Compare these probabilities to your book alt line pricing to find edge'
    }


def fetch_and_store_odds(sport, scan_type):
    """
    Fetch odds from API and store in warehouse. Called by GitHub Actions crons.

    Args:
        sport: 'nba' or 'mlb'.
        scan_type: 'morning_open' or 'pre_game'.
    """
    logger.info(f'[VYNDR] Fetching {scan_type} odds for {sport}')
    # In production: calls fetch_player_props and stores result


def check_all_games_weather_regrade():
    """
    Check weather for all today's games and trigger regrade if needed.
    Called by weather monitoring cron.
    """
    from utils.weather import check_weather_for_regrade
    logger.info('[VYNDR] Checking weather for all games')
    # In production: iterate today's MLB games, call check_weather_for_regrade
