"""
VYNDR Usage Redistribution Engine — Blueprint
Calculates how a player's usage, minutes, and role redistribute across
teammates when a key player is ruled OUT. Layers minutes redistribution
on top of archetype-driven system-change modifiers, applies efficiency
tradeoffs, and surfaces auto-grade targets for the scanner.
"""

import logging
from flask import Blueprint, request, jsonify

from utils.data_warehouse import fetch_with_cache
from utils.retry import api_call_with_retry

logger = logging.getLogger('vyndr')
redistribution_bp = Blueprint('redistribution', __name__)

# ---------------------------------------------------------------------------
# System-change archetype maps
# ---------------------------------------------------------------------------

SYSTEM_SHIFT_MAP = {
    'primary_scorer': {
        'secondary_creator': 0.08,
        'primary_playmaker': 0.03,
        'three_and_d': 0.04,
    },
    'primary_playmaker': {
        'primary_scorer': 0.05,
        'secondary_creator': 0.06,
        'three_and_d': -0.02,
    },
    'interior_big': {
        'stretch_big': 0.07,
        'primary_scorer': 0.03,
    },
}

# Usage-efficiency tradeoff slope: each +5 pct of raw usage boost
# carries a -1.5 pct efficiency drag.
USAGE_EFFICIENCY_PENALTY_PER_UNIT = -0.015 / 0.05  # -0.30 per 1.0

# Absorption tier thresholds
TIER_PRIMARY = {'min_boost': 0.20, 'min_confidence': 0.75}
TIER_SECONDARY = {'min_boost': 0.10, 'min_confidence': 0.60}
TIER_TERTIARY = {'min_boost': 0.05, 'min_confidence': 0.0}

# Auto-grade qualifying thresholds
AUTO_GRADE_MIN_BOOST = 0.15
AUTO_GRADE_MIN_CONFIDENCE = 0.65

# Minimum historical player-out events for data-driven redistribution
MIN_HISTORICAL_EVENTS = 5

# ---------------------------------------------------------------------------
# Helper stubs — backed by Supabase / external APIs via data_warehouse
# ---------------------------------------------------------------------------


def get_player_profile(player_id):
    """
    Retrieve a player's profile including archetype, position, and
    season usage rate from the data warehouse.

    Args:
        player_id: Unique player identifier.

    Returns:
        Dict with keys: player_id, name, position, archetype, usage_rate,
        minutes_per_game, team_id.  None if not found.
    """
    cache_key = f'player_profile:{player_id}'
    return fetch_with_cache(
        cache_key,
        lambda: api_call_with_retry(
            _fetch_player_profile_from_db, player_id
        ),
        ttl_hours=24,
    )


def _fetch_player_profile_from_db(player_id):
    """Raw DB fetch for player profile. Stub — replace with Supabase query."""
    logger.warning('get_player_profile stub called for %s', player_id)
    return None


def get_game_context(game_id):
    """
    Retrieve game context: teams, schedule, venue, pace environment.

    Args:
        game_id: Unique game identifier.

    Returns:
        Dict with keys: game_id, home_team_id, away_team_id, venue, pace.
    """
    cache_key = f'game_context:{game_id}'
    return fetch_with_cache(
        cache_key,
        lambda: api_call_with_retry(_fetch_game_context_from_db, game_id),
        ttl_hours=6,
    )


def _fetch_game_context_from_db(game_id):
    """Raw DB fetch for game context. Stub — replace with Supabase query."""
    logger.warning('get_game_context stub called for %s', game_id)
    return None


def get_team_coach(team_id):
    """
    Look up the head coach for a team.

    Args:
        team_id: Team identifier.

    Returns:
        Dict with keys: coach_id, name, team_id.
    """
    cache_key = f'team_coach:{team_id}'
    return fetch_with_cache(
        cache_key,
        lambda: api_call_with_retry(_fetch_team_coach, team_id),
        ttl_hours=168,
    )


def _fetch_team_coach(team_id):
    """Stub — replace with Supabase query."""
    logger.warning('get_team_coach stub called for %s', team_id)
    return None


def get_coaching_tendencies(coach_id):
    """
    Retrieve coaching tendency profile: rotation depth, archetype preferences,
    and any redistribution_profile overrides.

    Args:
        coach_id: Unique coach identifier.

    Returns:
        Dict with keys: coach_id, rotation_depth (int), style,
        redistribution_profile (dict or None).
    """
    cache_key = f'coaching_tendencies:{coach_id}'
    return fetch_with_cache(
        cache_key,
        lambda: api_call_with_retry(_fetch_coaching_tendencies, coach_id),
        ttl_hours=168,
    )


def _fetch_coaching_tendencies(coach_id):
    """Stub — replace with Supabase query."""
    logger.warning('get_coaching_tendencies stub called for %s', coach_id)
    return None


def get_available_roster(team_id, game_id):
    """
    Get the roster of available (non-injured, non-out) players for a
    specific game.

    Args:
        team_id: Team identifier.
        game_id: Game identifier.

    Returns:
        List of player profile dicts (same shape as get_player_profile).
    """
    cache_key = f'available_roster:{team_id}:{game_id}'
    return fetch_with_cache(
        cache_key,
        lambda: api_call_with_retry(
            _fetch_available_roster, team_id, game_id
        ),
        ttl_hours=1,
    )


def _fetch_available_roster(team_id, game_id):
    """Stub — replace with lineup service integration."""
    logger.warning(
        'get_available_roster stub called for team=%s game=%s',
        team_id,
        game_id,
    )
    return []


def get_player_out_history(player_id):
    """
    Retrieve historical instances where this player was ruled OUT,
    including how minutes and usage redistributed in those games.

    Args:
        player_id: Unique player identifier.

    Returns:
        List of dicts, each with keys: game_id, date, teammate_impacts
        (list of {player_id, minutes_gained, usage_gained}).
    """
    cache_key = f'player_out_history:{player_id}'
    return fetch_with_cache(
        cache_key,
        lambda: api_call_with_retry(
            _fetch_player_out_history, player_id
        ),
        ttl_hours=24,
    )


def _fetch_player_out_history(player_id):
    """Stub — replace with Supabase query on historical game logs."""
    logger.warning('get_player_out_history stub called for %s', player_id)
    return []


def get_team_roster(team_id):
    """
    Get the full active roster for a team (not filtered by game availability).

    Args:
        team_id: Team identifier.

    Returns:
        List of player profile dicts.
    """
    cache_key = f'team_roster:{team_id}'
    return fetch_with_cache(
        cache_key,
        lambda: api_call_with_retry(_fetch_team_roster, team_id),
        ttl_hours=24,
    )


def _fetch_team_roster(team_id):
    """Stub — replace with Supabase query."""
    logger.warning('get_team_roster stub called for %s', team_id)
    return []


def aggregate_historical_minutes(history):
    """
    Aggregate historical player-out events into average per-teammate
    minutes and usage gains.

    Args:
        history: List of historical event dicts from get_player_out_history.

    Returns:
        Dict mapping teammate player_id to {avg_minutes_gained,
        avg_usage_gained, sample_size}.
    """
    if not history:
        return {}

    teammate_totals = {}

    for event in history:
        for impact in event.get('teammate_impacts', []):
            pid = impact.get('player_id')
            if pid is None:
                continue
            if pid not in teammate_totals:
                teammate_totals[pid] = {
                    'total_minutes': 0.0,
                    'total_usage': 0.0,
                    'count': 0,
                }
            teammate_totals[pid]['total_minutes'] += impact.get(
                'minutes_gained', 0.0
            )
            teammate_totals[pid]['total_usage'] += impact.get(
                'usage_gained', 0.0
            )
            teammate_totals[pid]['count'] += 1

    aggregated = {}
    for pid, totals in teammate_totals.items():
        n = totals['count']
        aggregated[pid] = {
            'avg_minutes_gained': round(totals['total_minutes'] / n, 2),
            'avg_usage_gained': round(totals['total_usage'] / n, 4),
            'sample_size': n,
        }

    return aggregated


# ---------------------------------------------------------------------------
# Core calculation layers
# ---------------------------------------------------------------------------


def calculate_minutes_redistribution(
    player_out, game_context, coaching, available_roster
):
    """
    Layer A: Determine how the absent player's minutes redistribute.

    Strategy:
        1. If 5+ historical player-out events exist, use empirical data.
        2. Otherwise, fall back to positional fit + coaching rotation depth.
           - Concentrated coach (rotation_depth <= 7): backup gets 70%,
             remaining positional matches split 10% each.
           - Distributed coach (rotation_depth > 7): spread across 3-4
             players roughly evenly.

    Args:
        player_out: Player profile dict of the absent player.
        game_context: Game context dict.
        coaching: Coaching tendencies dict.
        available_roster: List of available teammate profile dicts.

    Returns:
        List of dicts: [{player_id, name, minutes_share, source}]
        sorted descending by minutes_share.
    """
    player_out_id = player_out.get('player_id')
    history = get_player_out_history(player_out_id)

    # --- Path 1: Historical data-driven ---
    if len(history) >= MIN_HISTORICAL_EVENTS:
        logger.info(
            'Using historical redistribution for player %s (%d events)',
            player_out_id,
            len(history),
        )
        aggregated = aggregate_historical_minutes(history)
        available_ids = {p.get('player_id') for p in available_roster}

        results = []
        for pid, stats in aggregated.items():
            if pid not in available_ids:
                continue
            teammate = next(
                (p for p in available_roster if p.get('player_id') == pid),
                None,
            )
            if teammate is None:
                continue
            results.append({
                'player_id': pid,
                'name': teammate.get('name', 'Unknown'),
                'minutes_share': stats['avg_minutes_gained'],
                'source': 'historical',
            })

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

    # --- Path 2: Positional + coaching fallback ---
    logger.info(
        'Using positional/coaching fallback for player %s', player_out_id
    )
    position = player_out.get('position', 'G')
    rotation_depth = coaching.get('rotation_depth', 8) if coaching else 8
    minutes_to_distribute = player_out.get('minutes_per_game', 32.0)

    # Find positional matches
    positional_matches = [
        p
        for p in available_roster
        if p.get('position') == position
        and p.get('player_id') != player_out_id
    ]
    other_roster = [
        p
        for p in available_roster
        if p.get('position') != position
        and p.get('player_id') != player_out_id
    ]

    results = []

    if rotation_depth <= 7:
        # Concentrated coach — backup gets 70%, others share 10% each
        if positional_matches:
            backup = positional_matches[0]
            results.append({
                'player_id': backup.get('player_id'),
                'name': backup.get('name', 'Unknown'),
                'minutes_share': round(minutes_to_distribute * 0.70, 1),
                'source': 'positional_concentrated',
            })
            remaining = minutes_to_distribute * 0.30
            fill_players = positional_matches[1:] + other_roster
            per_player = (
                round(minutes_to_distribute * 0.10, 1)
                if fill_players
                else 0.0
            )
            for p in fill_players[:3]:
                results.append({
                    'player_id': p.get('player_id'),
                    'name': p.get('name', 'Unknown'),
                    'minutes_share': per_player,
                    'source': 'positional_concentrated',
                })
    else:
        # Distributed coach — spread across 3-4 players
        spread_players = (positional_matches + other_roster)[:4]
        if spread_players:
            share = round(minutes_to_distribute / len(spread_players), 1)
            for p in spread_players:
                results.append({
                    'player_id': p.get('player_id'),
                    'name': p.get('name', 'Unknown'),
                    'minutes_share': share,
                    'source': 'positional_distributed',
                })

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


def calculate_system_change(player_out, coaching, available_roster):
    """
    Layer B: Determine archetype-driven usage shifts when a player is OUT.

    Maps the absent player's archetype to a system-shift dict, then applies
    coach-specific overrides if the coaching profile contains a
    redistribution_profile.

    Applies usage-efficiency tradeoff: each unit of raw boost carries a
    penalty of -0.015 per 0.05 boost (i.e. higher boosts are less efficient).

    Args:
        player_out: Player profile dict of the absent player.
        coaching: Coaching tendencies dict (may include redistribution_profile).
        available_roster: List of available teammate profile dicts.

    Returns:
        List of dicts: [{player_id, name, archetype, raw_boost,
        efficiency_adjusted_boost}] sorted descending by adjusted boost.
    """
    player_archetype = player_out.get('archetype', 'unknown')
    base_shifts = SYSTEM_SHIFT_MAP.get(player_archetype, {})

    # Coach-specific overrides take precedence
    coach_overrides = {}
    if coaching and coaching.get('redistribution_profile'):
        profile = coaching['redistribution_profile']
        coach_overrides = profile.get(player_archetype, {})

    # Merge: coach overrides win
    effective_shifts = {**base_shifts, **coach_overrides}

    results = []
    for teammate in available_roster:
        if teammate.get('player_id') == player_out.get('player_id'):
            continue
        teammate_archetype = teammate.get('archetype', 'unknown')
        raw_boost = effective_shifts.get(teammate_archetype, 0.0)
        if raw_boost == 0.0:
            continue

        # Apply usage-efficiency tradeoff
        penalty = raw_boost * USAGE_EFFICIENCY_PENALTY_PER_UNIT
        adjusted_boost = round(raw_boost + penalty, 4)

        results.append({
            'player_id': teammate.get('player_id'),
            'name': teammate.get('name', 'Unknown'),
            'archetype': teammate_archetype,
            'raw_boost': round(raw_boost, 4),
            'efficiency_adjusted_boost': adjusted_boost,
        })

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


# ---------------------------------------------------------------------------
# Classification and formatting
# ---------------------------------------------------------------------------


def classify_absorption_tier(boost, confidence):
    """
    Classify a teammate's absorption tier based on projected usage boost
    and confidence level.

    Tiers:
        - primary:   boost >= 0.20 AND confidence >= 0.75
        - secondary: boost >= 0.10 AND confidence >= 0.60
        - tertiary:  boost >= 0.05
        - minimal:   everything else

    Args:
        boost: Float, projected usage boost (0.0 - 1.0 scale).
        confidence: Float, confidence level (0.0 - 1.0).

    Returns:
        String tier label: 'primary', 'secondary', 'tertiary', or 'minimal'.
    """
    if (
        boost >= TIER_PRIMARY['min_boost']
        and confidence >= TIER_PRIMARY['min_confidence']
    ):
        return 'primary'
    if (
        boost >= TIER_SECONDARY['min_boost']
        and confidence >= TIER_SECONDARY['min_confidence']
    ):
        return 'secondary'
    if boost >= TIER_TERTIARY['min_boost']:
        return 'tertiary'
    return 'minimal'


def calculate_absorption_confidence(coaching, history_count):
    """
    Calculate confidence score for the redistribution projection based on
    the quality of coaching data and historical match count.

    Factors:
        - Coaching data quality: +0.30 if full profile, +0.15 if partial.
        - Historical events: scaled from 0.0 to 0.50 based on sample size
          (caps at 20 events for full credit).
        - Base confidence floor of 0.20 (positional logic always contributes).

    Args:
        coaching: Coaching tendencies dict (or None).
        history_count: Int, number of historical player-out events.

    Returns:
        Float confidence score between 0.20 and 1.0.
    """
    base = 0.20

    # Coaching data quality
    if coaching and coaching.get('redistribution_profile'):
        coaching_score = 0.30
    elif coaching and coaching.get('rotation_depth'):
        coaching_score = 0.15
    else:
        coaching_score = 0.0

    # Historical data contribution (capped at 20 events)
    capped_count = min(history_count, 20)
    history_score = (capped_count / 20) * 0.50

    confidence = min(base + coaching_score + history_score, 1.0)
    return round(confidence, 2)


def format_absorption_alert(player_out, primary_beneficiary):
    """
    Format a human-readable absorption alert for the scanner UI.

    Format:
        "[Star] is OUT.
         [Target] is underpriced. Boost: +X%. Confidence: Y%."

    Args:
        player_out: Dict with at least 'name' key.
        primary_beneficiary: Dict with 'name', 'boost', and 'confidence' keys.

    Returns:
        Formatted alert string.
    """
    star_name = player_out.get('name', 'Unknown')
    target_name = primary_beneficiary.get('name', 'Unknown')
    boost_pct = round(primary_beneficiary.get('boost', 0.0) * 100, 1)
    confidence_pct = round(primary_beneficiary.get('confidence', 0.0) * 100)

    return (
        f'{star_name} is OUT.\n'
        f'{target_name} is underpriced. '
        f'Boost: +{boost_pct}%. Confidence: {confidence_pct}%.'
    )


# ---------------------------------------------------------------------------
# Main endpoint
# ---------------------------------------------------------------------------


@redistribution_bp.route(
    '/calculate/<player_out_id>/<game_id>', methods=['GET']
)
def calculate_redistribution(player_out_id, game_id):
    """
    GET /calculate/<player_out_id>/<game_id>

    Calculate how usage, minutes, and production redistribute when a key
    player is ruled OUT for a given game.

    Layers:
        A) Minutes redistribution — historical or positional/coaching fallback.
        B) System-change modifiers — archetype-driven usage shifts.

    Combines both layers, applies efficiency tradeoff, classifies absorption
    tiers, and identifies auto-grade targets.

    Returns JSON:
        {
            player_out: {...},
            redistribution: [...],
            auto_grade_targets: [...],
            primary_beneficiary: {...},
            alert: "...",
            meta: {confidence, source, history_count}
        }
    """
    logger.info(
        'Redistribution request: player_out=%s game=%s',
        player_out_id,
        game_id,
    )

    # --- Gather context ---
    player_out = get_player_profile(player_out_id)
    if not player_out:
        return jsonify({
            'error': 'player_not_found',
            'message': f'No profile found for player {player_out_id}.',
        }), 404

    game_context = get_game_context(game_id)
    if not game_context:
        return jsonify({
            'error': 'game_not_found',
            'message': f'No game context found for {game_id}.',
        }), 404

    # Determine team and coaching context
    team_id = player_out.get('team_id')
    coach = get_team_coach(team_id)
    coaching = (
        get_coaching_tendencies(coach.get('coach_id'))
        if coach and coach.get('coach_id')
        else None
    )
    available_roster = get_available_roster(team_id, game_id)

    if not available_roster:
        return jsonify({
            'error': 'no_roster',
            'message': 'No available roster data for this game.',
        }), 404

    # --- Layer A: Minutes redistribution ---
    minutes_redist = calculate_minutes_redistribution(
        player_out, game_context, coaching, available_roster
    )

    # --- Layer B: System-change modifiers ---
    system_changes = calculate_system_change(
        player_out, coaching, available_roster
    )

    # --- Combine layers ---
    history = get_player_out_history(player_out_id)
    history_count = len(history) if history else 0
    confidence = calculate_absorption_confidence(coaching, history_count)

    # Build combined redistribution list keyed by player_id
    combined = {}
    for entry in minutes_redist:
        pid = entry['player_id']
        combined[pid] = {
            'player_id': pid,
            'name': entry['name'],
            'minutes_share': entry['minutes_share'],
            'usage_boost': 0.0,
            'raw_boost': 0.0,
            'source': entry['source'],
        }

    for entry in system_changes:
        pid = entry['player_id']
        if pid in combined:
            combined[pid]['usage_boost'] = entry['efficiency_adjusted_boost']
            combined[pid]['raw_boost'] = entry['raw_boost']
        else:
            combined[pid] = {
                'player_id': pid,
                'name': entry['name'],
                'minutes_share': 0.0,
                'usage_boost': entry['efficiency_adjusted_boost'],
                'raw_boost': entry['raw_boost'],
                'source': 'system_change_only',
            }

    # Classify tiers and sort
    redistribution_list = []
    for pid, data in combined.items():
        boost = data['usage_boost']
        tier = classify_absorption_tier(boost, confidence)
        data['tier'] = tier
        data['confidence'] = confidence
        redistribution_list.append(data)

    redistribution_list.sort(
        key=lambda x: x['usage_boost'], reverse=True
    )

    # --- Auto-grade targets ---
    auto_grade_targets = [
        entry
        for entry in redistribution_list
        if entry['usage_boost'] >= AUTO_GRADE_MIN_BOOST
        and entry['confidence'] >= AUTO_GRADE_MIN_CONFIDENCE
    ]

    # --- Primary beneficiary and alert ---
    primary_beneficiary = redistribution_list[0] if redistribution_list else None
    alert = None
    if primary_beneficiary:
        alert = format_absorption_alert(
            player_out,
            {
                'name': primary_beneficiary['name'],
                'boost': primary_beneficiary['usage_boost'],
                'confidence': primary_beneficiary['confidence'],
            },
        )

    return jsonify({
        'player_out': {
            'player_id': player_out.get('player_id'),
            'name': player_out.get('name'),
            'archetype': player_out.get('archetype'),
            'position': player_out.get('position'),
            'minutes_per_game': player_out.get('minutes_per_game'),
        },
        'redistribution': redistribution_list,
        'auto_grade_targets': auto_grade_targets,
        'primary_beneficiary': primary_beneficiary,
        'alert': alert,
        'meta': {
            'confidence': confidence,
            'source': 'historical' if history_count >= MIN_HISTORICAL_EVENTS
            else 'positional_coaching_fallback',
            'history_count': history_count,
        },
    })


# ---------------------------------------------------------------------------
# PATCH Item 6: MLB Lineup Shift on scratch
# ---------------------------------------------------------------------------

def calculate_mlb_lineup_shift(original_lineup, scratched_player_id, new_lineup):
    """
    MLB-specific: when a batter is scratched, lineup positions shift.
    PA multipliers, RBI context, and lineup protection all change.

    Args:
        original_lineup: List of dicts with id, name, batting_order.
        scratched_player_id: ID of the scratched player.
        new_lineup: List of dicts with id, name, batting_order after scratch.

    Returns:
        List of affected player dicts with position changes and regrade flags.
    """
    from utils.archetypes import BATTING_ORDER

    affected = []
    for player in new_lineup:
        old_pos = _find_original_position(player['id'], original_lineup)
        new_pos = player.get('batting_order')

        if old_pos and new_pos and old_pos != new_pos:
            old_mult = BATTING_ORDER.get(old_pos, {}).get('pa_mult', 1.0)
            new_mult = BATTING_ORDER.get(new_pos, {}).get('pa_mult', 1.0)

            affected.append({
                'player_id': player['id'],
                'player_name': player.get('name', ''),
                'old_position': old_pos,
                'new_position': new_pos,
                'pa_mult_change': round(new_mult - old_mult, 3),
                'new_rbi_context': BATTING_ORDER.get(new_pos, {}).get('rbi_ctx', 'unknown'),
                'needs_regrade': abs(new_mult - old_mult) > 0.02
            })

    return affected


def _find_original_position(player_id, lineup):
    """Find a player's original batting order position."""
    for p in lineup:
        if p.get('id') == player_id:
            return p.get('batting_order')
    return None


def log_todays_player_out_events(game_date):
    """
    Log player-out events from today's games for redistribution training.
    Called by nightly resolution step 15.

    Args:
        game_date: Date string (YYYY-MM-DD).
    """
    logger.info(f'[VYNDR] Logging player-out events for {game_date}')
    # In production: query injury reports + game logs to find players
    # who were listed as OUT, then log what happened to teammates' stats


def find_and_log_historical_player_outs(season):
    """
    Historical seeder: find player-out events from a past season.
    Called by scripts/seed_historical.py.

    Args:
        season: Season string (e.g., '2024-25').
    """
    logger.info(f'[VYNDR] Finding historical player-out events for {season}')
    # In production: iterate game logs, cross-reference with injury data
