"""
VYNDR Coaching Tendency Database — tactical fingerprinting for every coach.
Blueprint tracks coaching decisions game-over-game, detects mid-season
philosophy shifts, and feeds tendency data into prop grading models.
Supports both NBA and MLB with sport-specific field sets.
"""

import logging
from collections import Counter
from datetime import datetime, date, timedelta

from flask import Blueprint, request, jsonify

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

logger = logging.getLogger('vyndr')
coaching_bp = Blueprint('coaching', __name__)

# ---------------------------------------------------------------------------
# Field definitions per sport
# ---------------------------------------------------------------------------

COACHING_FIELDS = {
    'nba': {
        'pace_preference': {
            'type': 'float',
            'description': 'Possessions per 48 minutes — fast (100+) vs grind-it-out (<95)',
        },
        'three_point_rate': {
            'type': 'float',
            'description': 'Fraction of field goal attempts from three-point range',
        },
        'isolation_frequency': {
            'type': 'float',
            'description': 'Percentage of possessions ending in isolation plays',
        },
        'pick_roll_usage': {
            'type': 'float',
            'description': 'Percentage of possessions using pick-and-roll actions',
        },
        'bench_rotation_depth': {
            'type': 'int',
            'description': 'Number of players receiving 10+ minutes per game',
        },
        'fouling_philosophy_late': {
            'type': 'str',
            'description': 'Late-game fouling tendency: aggressive, selective, passive',
        },
        'score_state_rotations': {
            'type': 'dict',
            'description': 'Lineup groups by score differential bucket (blowout/close/trailing)',
        },
        'late_game_possession_player': {
            'type': 'str',
            'description': 'Player who most often gets the ball in crunch time (last 2 min, within 5 pts)',
        },
        'second_unit_usage_pattern': {
            'type': 'str',
            'description': 'When and how the second unit is deployed — stagger vs full-bench',
        },
        'usage_redistribution_profile': {
            'type': 'dict',
            'description': 'How usage shifts when a starter sits — who absorbs touches',
        },
        'shot_location_allowances': {
            'type': 'dict',
            'description': 'Defensive scheme — rim protection vs perimeter switching emphasis',
        },
        'timeout_tendency': {
            'type': 'str',
            'description': 'Timeout calling pattern — early to stop runs, or ride momentum',
        },
    },
    'mlb': {
        'starter_hook_tendency': {
            'type': 'float',
            'description': 'Average innings before pulling the starter',
        },
        'quick_hook_threshold': {
            'type': 'float',
            'description': 'ERA / pitch-count threshold that triggers early pull',
        },
        'bullpen_usage_philosophy': {
            'type': 'str',
            'description': 'Matchup-based, innings-based, or closer-only mentality',
        },
        'intentional_walk_rate': {
            'type': 'float',
            'description': 'Intentional walks per 9 innings managed',
        },
        'pinch_hit_frequency': {
            'type': 'float',
            'description': 'Pinch-hit substitutions per game average',
        },
        'bunt_tendency': {
            'type': 'float',
            'description': 'Sacrifice bunts per game average',
        },
        'save_situation_closer_only': {
            'type': 'bool',
            'description': 'Whether manager uses closer exclusively in save situations',
        },
        'platoon_tendency': {
            'type': 'float',
            'description': 'Rate of platoon-advantaged lineup construction',
        },
        'lineup_consistency': {
            'type': 'float',
            'description': 'Percentage of games with identical top-6 batting order',
        },
        'challenge_aggressiveness': {
            'type': 'float',
            'description': 'Replay challenges per game average',
        },
        'high_leverage_hook_tendency': {
            'type': 'float',
            'description': 'How quickly manager pulls starter with runners on. '
                           'Low = lets starter work through trouble (higher K ceiling). '
                           'High = quick hook (reduced K ceiling, more bullpen exposure).',
        },
    },
}

# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------


@coaching_bp.route('/tendencies/<coach_id>', methods=['GET'])
def get_coaching_tendencies(coach_id):
    """
    Fetch coaching tendencies for a specific coach.

    Query params:
        sport (str): 'nba' or 'mlb'. Required.

    Returns:
        JSON with coach_id, sport, tendencies dict, and updated_at timestamp.
    """
    sport = request.args.get('sport', '').lower()
    if sport not in ('nba', 'mlb'):
        return jsonify({'error': 'sport query param required — nba or mlb'}), 400

    cache_key = f'coaching_tendencies:{sport}:{coach_id}'

    def _fetch_tendencies():
        """Pull coaching tendencies from Supabase."""
        client = get_supabase_client()
        if not client:
            logger.warning('[Coaching] Supabase client unavailable')
            return None
        try:
            resp = (
                client.table('coaching_tendencies')
                .select('*')
                .eq('coach_id', coach_id)
                .eq('sport', sport)
                .order('updated_at', desc=True)
                .limit(1)
                .execute()
            )
            if resp.data:
                return resp.data[0]
            return None
        except Exception as exc:
            logger.error(f'[Coaching] Failed to fetch tendencies for {coach_id}: {exc}')
            return None

    data = fetch_with_cache(cache_key, _fetch_tendencies, data_type='player_stats')
    if not data:
        return jsonify({'error': 'No coaching tendencies found', 'coach_id': coach_id}), 404

    return jsonify({
        'coach_id': coach_id,
        'sport': sport,
        'tendencies': data.get('tendencies', {}),
        'updated_at': data.get('updated_at'),
    })


@coaching_bp.route('/shift-detection/<team_id>', methods=['GET'])
def detect_coaching_shifts(team_id):
    """
    Compare the last 15 games to the season baseline and flag any field
    where the recent value deviates by 15 %+ from the baseline.

    Query params:
        sport (str): 'nba' or 'mlb'. Required.

    Returns:
        JSON with team_id, sport, and a list of detected shifts. Each shift
        contains field, baseline, recent, change_pct, and direction.
    """
    sport = request.args.get('sport', '').lower()
    if sport not in ('nba', 'mlb'):
        return jsonify({'error': 'sport query param required — nba or mlb'}), 400

    baseline = get_season_baseline(team_id, sport)
    recent = get_recent_tendencies(team_id, sport, window=15)

    if not baseline or not recent:
        return jsonify({
            'error': 'Insufficient data for shift detection',
            'team_id': team_id,
        }), 404

    shifts = []
    numeric_fields = [
        f for f, meta in COACHING_FIELDS.get(sport, {}).items()
        if meta['type'] in ('float', 'int')
    ]

    for field in numeric_fields:
        base_val = baseline.get(field)
        recent_val = recent.get(field)
        if base_val is None or recent_val is None:
            continue
        try:
            base_val = float(base_val)
            recent_val = float(recent_val)
        except (TypeError, ValueError):
            continue
        if base_val == 0:
            continue

        change_pct = abs(recent_val - base_val) / abs(base_val) * 100
        if change_pct >= 15.0:
            direction = 'up' if recent_val > base_val else 'down'
            shifts.append({
                'field': field,
                'baseline': round(base_val, 4),
                'recent': round(recent_val, 4),
                'change_pct': round(change_pct, 2),
                'direction': direction,
            })

    shifts.sort(key=lambda s: s['change_pct'], reverse=True)

    return jsonify({
        'team_id': team_id,
        'sport': sport,
        'window': 15,
        'threshold_pct': 15.0,
        'shifts': shifts,
    })


# ---------------------------------------------------------------------------
# Season baseline & recent tendencies helpers
# ---------------------------------------------------------------------------


def get_season_baseline(team_id, sport):
    """
    Retrieve the full-season average coaching tendencies for a team.

    Args:
        team_id: Team identifier string.
        sport: 'nba' or 'mlb'.

    Returns:
        Dict of field -> averaged value across all games this season,
        or None if data unavailable.
    """
    client = get_supabase_client()
    if not client:
        return None
    try:
        resp = (
            client.table('coaching_tendencies')
            .select('tendencies')
            .eq('team_id', team_id)
            .eq('sport', sport)
            .execute()
        )
        if not resp.data:
            return None
        return _average_tendency_rows(resp.data, sport)
    except Exception as exc:
        logger.error(f'[Coaching] Season baseline fetch failed for {team_id}: {exc}')
        return None


def get_recent_tendencies(team_id, sport, window=15):
    """
    Retrieve coaching tendencies from the most recent N games.

    Args:
        team_id: Team identifier string.
        sport: 'nba' or 'mlb'.
        window: Number of recent games to include.

    Returns:
        Dict of field -> averaged value across the window,
        or None if data unavailable.
    """
    client = get_supabase_client()
    if not client:
        return None
    try:
        resp = (
            client.table('coaching_tendencies')
            .select('tendencies')
            .eq('team_id', team_id)
            .eq('sport', sport)
            .order('game_date', desc=True)
            .limit(window)
            .execute()
        )
        if not resp.data:
            return None
        return _average_tendency_rows(resp.data, sport)
    except Exception as exc:
        logger.error(f'[Coaching] Recent tendencies fetch failed for {team_id}: {exc}')
        return None


def _average_tendency_rows(rows, sport):
    """
    Average numeric tendency fields across multiple game rows.

    Args:
        rows: List of dicts, each containing a 'tendencies' dict.
        sport: 'nba' or 'mlb'.

    Returns:
        Dict of field -> averaged numeric value. Non-numeric fields use
        the most recent value.
    """
    if not rows:
        return None

    numeric_fields = [
        f for f, meta in COACHING_FIELDS.get(sport, {}).items()
        if meta['type'] in ('float', 'int')
    ]

    sums = {f: 0.0 for f in numeric_fields}
    counts = {f: 0 for f in numeric_fields}
    result = {}

    for row in rows:
        tendencies = row.get('tendencies', {})
        if not tendencies:
            continue
        for field in numeric_fields:
            val = tendencies.get(field)
            if val is not None:
                try:
                    sums[field] += float(val)
                    counts[field] += 1
                except (TypeError, ValueError):
                    pass

    for field in numeric_fields:
        if counts[field] > 0:
            result[field] = round(sums[field] / counts[field], 4)

    # For non-numeric fields, take the most recent value
    most_recent = rows[0].get('tendencies', {}) if rows else {}
    non_numeric = [
        f for f, meta in COACHING_FIELDS.get(sport, {}).items()
        if meta['type'] not in ('float', 'int')
    ]
    for field in non_numeric:
        val = most_recent.get(field)
        if val is not None:
            result[field] = val

    return result


# ---------------------------------------------------------------------------
# Nightly update pipeline
# ---------------------------------------------------------------------------


def update_coaching_tendencies(game_date):
    """
    Nightly job: iterate all completed games for the given date,
    parse coaching decisions from both sides, and upsert to Supabase.

    Args:
        game_date: date object or ISO string (YYYY-MM-DD) for the target day.
    """
    if isinstance(game_date, str):
        game_date = datetime.strptime(game_date, '%Y-%m-%d').date()

    logger.info(f'[Coaching] Running nightly update for {game_date.isoformat()}')

    # Fetch completed games for the date
    nba_games = _fetch_completed_games(game_date, 'nba')
    mlb_games = _fetch_completed_games(game_date, 'mlb')

    processed = 0

    for game in nba_games:
        for side in ('home', 'away'):
            try:
                tendencies = parse_nba_coaching_decisions(game, side)
                if tendencies:
                    upsert_coaching_tendencies(
                        coach_id=tendencies.pop('coach_id', None),
                        team_id=tendencies.pop('team_id', None),
                        sport='nba',
                        game_id=game.get('game_id'),
                        game_date=game_date,
                        tendencies=tendencies,
                    )
                    processed += 1
            except Exception as exc:
                logger.error(
                    f'[Coaching] NBA parse failed game={game.get("game_id")} '
                    f'side={side}: {exc}'
                )

    for game in mlb_games:
        for side in ('home', 'away'):
            try:
                tendencies = parse_mlb_coaching_decisions(game, side)
                if tendencies:
                    upsert_coaching_tendencies(
                        coach_id=tendencies.pop('coach_id', None),
                        team_id=tendencies.pop('team_id', None),
                        sport='mlb',
                        game_id=game.get('game_id'),
                        game_date=game_date,
                        tendencies=tendencies,
                    )
                    processed += 1
            except Exception as exc:
                logger.error(
                    f'[Coaching] MLB parse failed game={game.get("game_id")} '
                    f'side={side}: {exc}'
                )

    logger.info(f'[Coaching] Nightly update complete — {processed} entries upserted')
    return processed


def _fetch_completed_games(game_date, sport):
    """
    Retrieve completed games for a given date and sport from the data warehouse.

    Args:
        game_date: date object.
        sport: 'nba' or 'mlb'.

    Returns:
        List of game dicts with box score / play-by-play data attached.
    """
    cache_key = f'completed_games:{sport}:{game_date.isoformat()}'

    def _fetch():
        client = get_supabase_client()
        if not client:
            return []
        try:
            resp = (
                client.table('games')
                .select('*')
                .eq('sport', sport)
                .eq('game_date', game_date.isoformat())
                .eq('status', 'completed')
                .execute()
            )
            return resp.data or []
        except Exception as exc:
            logger.error(f'[Coaching] Game fetch failed for {sport} {game_date}: {exc}')
            return []

    return fetch_with_cache(cache_key, _fetch, data_type='player_stats') or []


# ---------------------------------------------------------------------------
# NBA coaching decision parsing
# ---------------------------------------------------------------------------


def parse_nba_coaching_decisions(game, side):
    """
    Extract coaching tendency signals from an NBA game's box score
    and play-by-play data for one side (home or away).

    Args:
        game: Game dict with nested box score and play-by-play.
        side: 'home' or 'away'.

    Returns:
        Dict of coaching tendency fields, or None if data insufficient.
    """
    box = game.get(f'{side}_box', {})
    pbp = game.get('play_by_play', [])
    team_id = game.get(f'{side}_team_id')
    coach_id = game.get(f'{side}_coach_id')

    if not box or not team_id:
        return None

    players = box.get('players', [])
    if not players:
        return None

    # Rotation depth: players with 10+ minutes
    rotation_depth = sum(1 for p in players if (p.get('minutes', 0) or 0) >= 10)

    # Late game possession player (last 2 min, within 5 pts)
    late_game_player = _find_late_game_possession_player(pbp, team_id)

    # Pace: possessions per 48 from box score
    pace = box.get('pace', None)

    # Three-point rate
    three_rate = calculate_three_rate(players)

    # Score-state lineups
    score_state = extract_score_state_lineups(pbp, team_id)

    tendencies = {
        'coach_id': coach_id,
        'team_id': team_id,
        'bench_rotation_depth': rotation_depth,
        'late_game_possession_player': late_game_player,
        'pace_preference': pace,
        'three_point_rate': three_rate,
        'score_state_rotations': score_state,
    }

    # Additional fields parsed from play-by-play when available
    iso_freq = box.get('isolation_frequency')
    if iso_freq is not None:
        tendencies['isolation_frequency'] = iso_freq

    pr_usage = box.get('pick_roll_usage')
    if pr_usage is not None:
        tendencies['pick_roll_usage'] = pr_usage

    return tendencies


def _find_late_game_possession_player(pbp, team_id):
    """
    Identify the player who most frequently has the ball in crunch time
    (last 2 minutes of 4th quarter / OT, score within 5 points).

    Args:
        pbp: List of play-by-play event dicts.
        team_id: Team identifier to filter possessions.

    Returns:
        Player name string or None.
    """
    crunch_possessions = []
    for event in pbp:
        period = event.get('period', 0)
        clock = event.get('clock', '')
        margin = abs(event.get('score_margin', 999))
        event_team = event.get('team_id')

        if event_team != team_id:
            continue
        if period < 4:
            continue
        if margin > 5:
            continue

        # Parse clock — expect "MM:SS" or seconds remaining
        remaining = _parse_clock(clock)
        if remaining is not None and remaining <= 120:
            player = event.get('player_name') or event.get('player_id')
            if player:
                crunch_possessions.append(player)

    return most_common_player(crunch_possessions)


def _parse_clock(clock):
    """
    Parse game clock string into seconds remaining.

    Args:
        clock: String like '1:45' or numeric seconds.

    Returns:
        Float seconds remaining, or None if unparseable.
    """
    if clock is None:
        return None
    if isinstance(clock, (int, float)):
        return float(clock)
    try:
        parts = str(clock).split(':')
        if len(parts) == 2:
            return int(parts[0]) * 60 + float(parts[1])
        return float(clock)
    except (ValueError, TypeError):
        return None


# ---------------------------------------------------------------------------
# MLB coaching decision parsing
# ---------------------------------------------------------------------------


def parse_mlb_coaching_decisions(game, side):
    """
    Extract coaching tendency signals from an MLB game for one side.

    Args:
        game: Game dict with box score and play-by-play data.
        side: 'home' or 'away'.

    Returns:
        Dict of coaching tendency fields, or None if data insufficient.
    """
    box = game.get(f'{side}_box', {})
    pbp = game.get('play_by_play', [])
    team_id = game.get(f'{side}_team_id')
    coach_id = game.get(f'{side}_coach_id')

    if not box or not team_id:
        return None

    pitching = box.get('pitching', {})
    batting = box.get('batting', {})

    # Starter hook tendency — innings pitched by the starter
    starter = pitching.get('starter', {})
    starter_ip = starter.get('innings_pitched', None)

    # Pinch-hit frequency
    pinch_hits = count_pinch_hits(pbp, team_id)

    # Bunt tendency
    sac_bunts = count_sacrifice_bunts(pbp, team_id)

    # Challenge aggressiveness
    challenges = box.get('challenges_used', 0) or 0

    tendencies = {
        'coach_id': coach_id,
        'team_id': team_id,
        'starter_hook_tendency': float(starter_ip) if starter_ip is not None else None,
        'pinch_hit_frequency': pinch_hits,
        'bunt_tendency': sac_bunts,
        'challenge_aggressiveness': challenges,
    }

    # Intentional walks from pitching data
    ibb = pitching.get('intentional_walks', None)
    if ibb is not None:
        tendencies['intentional_walk_rate'] = float(ibb)

    return tendencies


# ---------------------------------------------------------------------------
# Shared helper functions
# ---------------------------------------------------------------------------


def most_common_player(player_list):
    """
    Return the most frequently occurring player name from a list.

    Args:
        player_list: List of player name strings.

    Returns:
        Most common player name, or None if list is empty.
    """
    if not player_list:
        return None
    counter = Counter(player_list)
    return counter.most_common(1)[0][0]


def extract_score_state_lineups(pbp, team_id):
    """
    Group on-court lineups by score-state buckets for a given team.

    Score-state buckets:
        - blowout_ahead: team leading by 15+
        - comfortable: team leading by 6-14
        - close: margin within 5
        - trailing: team down by 6-14
        - blowout_behind: team down by 15+

    Args:
        pbp: Play-by-play event list.
        team_id: Team identifier.

    Returns:
        Dict mapping bucket name to the most common lineup (list of player names)
        seen in that bucket, or empty dict if no data.
    """
    buckets = {
        'blowout_ahead': [],
        'comfortable': [],
        'close': [],
        'trailing': [],
        'blowout_behind': [],
    }

    for event in pbp:
        if event.get('team_id') != team_id:
            continue
        lineup = event.get('lineup', [])
        if not lineup:
            continue

        margin = event.get('score_margin', 0) or 0
        lineup_key = tuple(sorted(lineup))

        if margin >= 15:
            buckets['blowout_ahead'].append(lineup_key)
        elif margin >= 6:
            buckets['comfortable'].append(lineup_key)
        elif margin >= -5:
            buckets['close'].append(lineup_key)
        elif margin >= -14:
            buckets['trailing'].append(lineup_key)
        else:
            buckets['blowout_behind'].append(lineup_key)

    result = {}
    for bucket, lineups in buckets.items():
        if lineups:
            counter = Counter(lineups)
            most_common = counter.most_common(1)[0][0]
            result[bucket] = list(most_common)

    return result


def calculate_three_rate(players):
    """
    Calculate three-point attempt rate from player box score data.

    Args:
        players: List of player box score dicts with 'fga' and 'fg3a' fields.

    Returns:
        Float three-point rate (0.0-1.0), or None if no FGA data.
    """
    total_fga = 0
    total_fg3a = 0
    for p in players:
        fga = p.get('fga', 0) or 0
        fg3a = p.get('fg3a', 0) or 0
        total_fga += fga
        total_fg3a += fg3a

    if total_fga == 0:
        return None
    return round(total_fg3a / total_fga, 4)


def count_pinch_hits(pbp, team_id):
    """
    Count pinch-hit substitutions for a team from play-by-play data.

    Args:
        pbp: Play-by-play event list.
        team_id: Team identifier.

    Returns:
        Integer count of pinch-hit appearances.
    """
    count = 0
    for event in pbp:
        if event.get('team_id') != team_id:
            continue
        event_type = (event.get('event_type') or '').lower()
        description = (event.get('description') or '').lower()
        if 'pinch' in event_type or 'pinch hit' in description:
            count += 1
    return count


def count_sacrifice_bunts(pbp, team_id):
    """
    Count sacrifice bunt attempts for a team from play-by-play data.

    Args:
        pbp: Play-by-play event list.
        team_id: Team identifier.

    Returns:
        Integer count of sacrifice bunts.
    """
    count = 0
    for event in pbp:
        if event.get('team_id') != team_id:
            continue
        event_type = (event.get('event_type') or '').lower()
        description = (event.get('description') or '').lower()
        if 'sacrifice' in event_type and 'bunt' in event_type:
            count += 1
        elif 'sac bunt' in description or 'sacrifice bunt' in description:
            count += 1
    return count


# ---------------------------------------------------------------------------
# Supabase persistence
# ---------------------------------------------------------------------------


def upsert_coaching_tendencies(coach_id, team_id, sport, game_id, game_date, tendencies):
    """
    Upsert coaching tendency data into the Supabase coaching_tendencies table.

    Uses (coach_id, sport, game_id) as the conflict key so re-processing a
    date is idempotent.

    Args:
        coach_id: Coach identifier string.
        team_id: Team identifier string.
        sport: 'nba' or 'mlb'.
        game_id: Unique game identifier.
        game_date: date object for the game.
        tendencies: Dict of tendency field -> value.

    Returns:
        True on success, False on failure.
    """
    client = get_supabase_client()
    if not client:
        logger.warning('[Coaching] Cannot upsert — Supabase client unavailable')
        return False

    if isinstance(game_date, date):
        game_date_str = game_date.isoformat()
    else:
        game_date_str = str(game_date)

    row = {
        'coach_id': coach_id,
        'team_id': team_id,
        'sport': sport,
        'game_id': game_id,
        'game_date': game_date_str,
        'tendencies': tendencies,
        'updated_at': datetime.utcnow().isoformat(),
    }

    try:
        client.table('coaching_tendencies').upsert(
            row, on_conflict='coach_id,sport,game_id'
        ).execute()
        logger.info(
            f'[Coaching] Upserted tendencies coach={coach_id} game={game_id}'
        )
        return True
    except Exception as exc:
        logger.error(
            f'[Coaching] Upsert failed coach={coach_id} game={game_id}: {exc}'
        )
        return False


# ---------------------------------------------------------------------------
# PATCH Item 15: Historical seeding wrappers
# ---------------------------------------------------------------------------

def parse_nba_coaching_from_game_id(game_id):
    """
    Wrapper for historical seeding — fetches NBA game data then parses.
    Called by scripts/seed_historical.py.

    Args:
        game_id: NBA game ID string.
    """
    import time
    time.sleep(0.6)
    try:
        from nba_api.stats.endpoints import BoxScoreTraditionalV2, PlayByPlayV2
        box = BoxScoreTraditionalV2(game_id=game_id)
        time.sleep(0.6)
        pbp = PlayByPlayV2(game_id=game_id)

        box_dfs = box.get_data_frames()
        pbp_df = pbp.get_data_frames()[0]

        game_data = {
            'boxscore': _format_box_for_coaching(box_dfs),
            'play_by_play': _format_pbp_for_coaching(pbp_df),
            'game_date': None
        }

        for side in ['home', 'away']:
            tendencies = parse_nba_coaching_decisions(game_data, side)
            coach_id = game_data.get(f'{side}_coach_id', f'unknown_{side}')
            team_id = game_data.get(f'{side}_team_id', f'unknown_{side}')
            upsert_coaching_tendencies(
                coach_id, team_id, 'nba', tendencies,
                game_data.get('game_date'), game_id
            )
    except Exception as e:
        logger.warning(f'[Coaching] NBA historical parse failed for {game_id}: {e}')


def parse_mlb_coaching_from_game_id(game_id):
    """
    Wrapper for historical seeding — fetches MLB game data then parses.
    Called by scripts/seed_historical.py.

    Args:
        game_id: MLB game ID (gamePk).
    """
    try:
        import statsapi
        game_data = statsapi.get('game', {'gamePk': game_id})

        for side in ['home', 'away']:
            tendencies = parse_mlb_coaching_decisions(game_data, side)
            team_data = game_data.get('gameData', {}).get('teams', {}).get(side, {})
            coach_id = str(team_data.get('id', f'unknown_{side}'))
            team_id = str(team_data.get('id', f'unknown_{side}'))
            game_date = game_data.get('gameData', {}).get('datetime', {}).get('officialDate')
            upsert_coaching_tendencies(
                coach_id, team_id, 'mlb', tendencies, game_date, str(game_id)
            )
    except Exception as e:
        logger.warning(f'[Coaching] MLB historical parse failed for {game_id}: {e}')


def _format_box_for_coaching(box_dfs):
    """Format BoxScoreTraditionalV2 DataFrames for coaching parser."""
    return {}


def _format_pbp_for_coaching(pbp_df):
    """Format PlayByPlayV2 DataFrame for coaching parser."""
    return []
