import math
from typing import Any, Dict, List, Tuple
import pandas as pd
import requests
from fantasy_data.services import load_players_data, load_teams_data,load_team_upcoming_fix_difficulty_data,load_info
from players.services import get_player_stats
import random
from squad.tr_v3 import get_ai_transfer_suggestions, get_transfer_suggestions_for_players

import logging
logger = logging.getLogger("fpl_data") 

SELECTION = {
    "GK": 2,
    "DEF": 5,
    "MID": 5,
    "FWD": 3,
}
FORMATIONS = [
    "5-4-1",
    "5-3-2",
    "5-2-3",
    "4-5-1",
    "4-4-2",
    "4-3-3",
    "3-4-3",
    "3-5-2",
]
STATUS = {
    "a": 1,
    "d": 0.5,
    "i": 0,
    "s": 0,
    "u": 0,
    "n": 0,
}

SELECTION_WEIGHTS = {
    "FWD": [1, 0.75, 0.25, 1.15, 0.25, 0, 0.75, 0.15, 0, 0, 0.05, 0.15, 0.1 ,-0.75],
    "MID": [1, 0.75, 0.25, 0.55, 0.85, 0, 0.5, 0.75, 0, 0,0.15, 0.15, 0.15 , -0.75],
    "DEF": [1, 0.75, 0.25, 0.15, 0.15, -0.05, 0.05, 0.05, 0.75, 0,0.2, 0.05, 0.15 , -0.75],
    "GK": [1, 0.75, 0.25, 0, 0.01, -0.15, 0, 0.01, 1.5, 1.25, 0,0,0, -0.75],
    "MNG": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0,0,0],
}
SELECTION_COLS = [
    "points_per_game",
    "form",
    "ict_index",
    "expected_goals",
    "expected_assists",
    "expected_goals_conceded",
    "goals_scored",
    "assists",
    "clean_sheets",
    "saves",
    "corners_and_indirect_freekicks_order",
    "penalties_order",
    "direct_freekicks_order",
    "Difficulty",
]
BUDGET_MIN = 98
BUDGET_MAX = 100
MAX_PLAYERS_PER_TEAM = 3

TEAMS = load_teams_data()

get_mmg_squad_url = "https://fantasy.premierleague.com/api/entry/{mng}/event/{gw}/picks/"
DREAM_TEAM_URL = "https://fantasy.premierleague.com/api/dream-team/"

def get_team_name(team_id):
    """
    Returns the team name for a given team ID.
    """
    team = TEAMS[team_id - 1]
    return {"id": team["id"], "name": team["short_name"], "code": team["code"]}


def validate_team(team):
    """
    Validates overall team constraints:
      - Total price within the budget range.
    """
    total_price = sum(player["now_cost"] for player in team)
    if total_price < BUDGET_MIN or total_price > BUDGET_MAX:
        return False

    return True


def generate_team_greedy_randomized(players, max_attempts=10000, rcl_percent=0.15):
    """
    Generates a team using greedy randomized selection:
      - For each position, sort players by a greedy metric ('form') or (Total points).
      - Create a Restricted Candidate List (RCL) from the top rcl_percent of players.
      - Randomly select players from the RCL while enforcing the maximum team constraint.
      - Validate overall constraints (budget and team composition).
    """
    # Group players by position.
    position_groups = {pos: [] for pos in SELECTION.keys()}
    for p in players:
        if p["element_type"] in position_groups:
            position_groups[p["element_type"]].append(p)

    # Try multiple attempts to form a valid team.
    for attempt in range(max_attempts):
        team = []
        team_counts = {}  # Tracks the count of players per real-life team.
        valid = True

        # For each required position, select the needed number of players.
        for pos, count in SELECTION.items():
            candidates = position_groups.get(pos, [])

            # Build the Restricted Candidate List (RCL): top rcl_percent of candidates.
            rcl_size = max(1, int(len(candidates) * rcl_percent))
            rcl = candidates[:rcl_size]

            selected_for_position = []
            attempts_for_position = 0
            # Try to select 'count' players for this position.
            while len(selected_for_position) < count and attempts_for_position < 250:
                candidate = random.choice(rcl)
                if not (candidate in selected_for_position):  # Not Allowing Duplicates
                    current_team_count = team_counts.get(candidate["team"], 0)
                    # Check that adding this candidate does not exceed team limits.
                    if current_team_count < MAX_PLAYERS_PER_TEAM:
                        selected_for_position.append(candidate)
                        team_counts[candidate["team"]] = current_team_count + 1
                attempts_for_position += 1

            # If unable to select the required number of players for this position, mark as invalid.
            if len(selected_for_position) < count:
                valid = False
                break

            team.extend(selected_for_position)

        # Check overall team constraints.
        if valid and validate_team(team):
            return team  # Successfully generated a valid team.

    return None  # Failed to generate a valid team after max_attempts.


def create_team_XI(formation,scores, team):
    xi_score = 0
    bench_score = 0
    stats = {}

    players_per_position = list(SELECTION.values())
    team_players = [
                get_player_stats(id)
                for id in team
            ]

    team_layout = {"XI": [], "Subs": []}
    players_count = [1]
    players_count.extend([int(players_num) for players_num in formation.split("-")])
    for i,players_no in enumerate(players_per_position) :
        scores_by_position = scores[:players_no]
        players_by_position = team_players [:players_no]
        for j ,score in enumerate(scores_by_position):
            curent_captain = stats.get("C",0) # This will be a tuple (id,score)
            curent_vice = stats.get("VC",0)
            if curent_captain != 0:
                if curent_captain[1] < score:
                    stats["VC"] = stats["C"]
                    stats["C"] = (players_by_position[j]["id"],score)
                    
                elif curent_vice == 0 or curent_vice[1] < score :
                    stats["VC"] = (players_by_position[j]["id"],score)
            else:
                stats["C"] = (players_by_position[j]["id"],score)
            
        
        scores_by_position.sort(reverse=True)
        players_by_position = sorted(players_by_position,key= lambda player : player["XP"],reverse=True)
        players_by_position = format_squad(players_by_position)
        print(players_by_position)
        team_layout["XI"].extend([player for player in players_by_position[:players_count[i]]])
        xi_score += sum(scores_by_position[:players_count[i]])
        team_layout["Subs"].extend([player for player in players_by_position[players_count[i]:]])
        bench_score += sum(scores_by_position[players_count[i]:])
        del scores[:players_no]
        del team_players[:players_no]
        
    xi_score += stats["C"][1] 
    return team_layout,stats,xi_score,bench_score


def suggest_team_formation(team):
    """
    Suggest a team formation using greedy selection:
      - For each position, choose the best players by a greedy metric ('form') or (Total points) to make the best XI for the given foramtion and team.
      - Assign each formation a score Calculated fto the greedy metric
      - Choose The Formation with Heighst score
    """
    equal_scored_forms = []

    score_per_formation = {}
    heighst_score = 0
    selected_formation = ""
    players_per_position = list(SELECTION.values())
    position_candidates = [
                get_player_stats(id)
                for id in team
            ]
    players_expected_scores = []
    for candidate in position_candidates:
        players_expected_scores.append(candidate["XP"])
    for formation in FORMATIONS:
        scores = players_expected_scores[:]
        players_count = [1]
        metric_score = 0
        players_count.extend([int(players_num) for players_num in formation.split("-")])
        for i,players_no in enumerate(players_per_position) :
            scores_by_position = sorted(scores[:players_no],reverse=True)
            metric_score += sum(
                score for score in scores_by_position[:players_count[i]]
            )
            del scores[:players_no]
        score_per_formation[formation] = metric_score
        if metric_score > heighst_score:
            heighst_score = metric_score
            selected_formation = formation
            equal_scored_forms.clear()
            equal_scored_forms.append(formation)
        elif metric_score == heighst_score:
            equal_scored_forms.append(formation)

    return random.choice(equal_scored_forms) , players_expected_scores


def format_squad(squad):
    df = pd.DataFrame(squad)
    df = df[
        [
            "id",
            "web_name",
            "element_type",
            "team",
            "now_cost",
            "form",
            "total_points",
            "status",
            "news",
            "opta_code",
            "XP"
        ]
    ]
    df["team"] = df["team"].apply(lambda x: get_team_name(x))

    return df.to_dict(orient="records")
def format_transfer(player):
    df = pd.DataFrame([player])
    df = df[
        [
            "id",
            "web_name",
            "element_type",
            "team",
            "now_cost",
            "total_points",
            "status",
            "opta_code",
            "score",
            "Difficulty",
            "XP",
        ]
    ]
    df["team"] = df["team"].apply(lambda x: get_team_name(x))

    return df.to_dict(orient="records")


def selectable_players_data(use_xp=False) -> List[Dict[str, Any]]:
    players = create_player_selection_pool()
    sorting_criteria = ["XP","score", "total_points"] if use_xp else ["score", "total_points"]
    players = players.sort_values(
        by=sorting_criteria, ascending=False
    )
    return players.to_dict(orient="records")


def calculate_player_composite_score(row):
    pos = row["element_type"]
    weights = SELECTION_WEIGHTS[pos]

    weighted_sum = 0
    for i, col in enumerate(SELECTION_COLS):
        score =  0
        if col in ["corners_and_indirect_freekicks_order","penalties_order","direct_freekicks_order"] :
            score = float( calculate_setpiece_rank_score(row[col])) * weights[i]
        else :
            score = float(row[col]) * weights[i]
        weighted_sum += score
    return round(weighted_sum * row["availability"] * round(row["chance_of_playing_this_round"] / 100 ,2), 1)

def calculate_setpiece_rank_score(rank):
    if rank <= 0:
        return 0
    elif rank == 1:
        return 1
    else:
        return 0.85 ** (rank - 1) 
def create_player_selection_pool() -> pd.DataFrame:
    players = pd.DataFrame(load_players_data())
    #players = players[players["can_select"]]
    player_selection = players[
        [
            "id",
            "web_name",
            "now_cost",
            "status",
            "total_points",
            "team",
            "element_type",
            "news",
            "chance_of_playing_this_round",
            "opta_code",
            'XP',
        ]
        + SELECTION_COLS[:-1]
    ]
    player_selection["availability"] = player_selection["status"].map(
        lambda elem: STATUS[elem]
    )

    teams = pd.DataFrame(load_team_upcoming_fix_difficulty_data())[["id", "Difficulty"]]

    teams = teams.rename(columns={"id": "team"})
    player_selection = pd.merge(player_selection, teams, on="team", how="inner")

    player_selection["score"] = player_selection.apply(
        calculate_player_composite_score, axis=1
    )
    return player_selection

def validate_trasfer_selection(player, old_XP, budget,ids,count,is_availble = True):
    """
    Validates a transfer selection:
      - Checks if the player Xp is not less than the Old.
      - Checks if the player cost is within the budget.
    """
    # Add Check For Duplicate Player
    if player["id"] in ids or count.get(player["team"], 0) >= MAX_PLAYERS_PER_TEAM:
        return False

    #flexability = 1 if is_availble else 2
    if player["XP"] < old_XP :

        # raise ValueError(f"Player {player['web_name']} is not in the transfer list.")
        return False
    if player["now_cost"] > budget:
        #raise ValueError(f"Player {player['web_name']} exceeds the budget.")
        return False
    if player["availability"] != 1:
        #raise ValueError(f"Player {player['web_name']} exceeds the budget.")
        return False

    return True

def generate_transfers_greedy_randomized(all_players:pd.DataFrame,player_change:List[Any],team,team_count,budget:float, max_attempts=1000, rcl_percent=0.33):
    """
    Generates transfer suggestion using greedy randomized selection:
      - For each position, sort players by a XP and Composite Score.
      - Create a Restricted Candidate List (RCL) from the top rcl_percent of players.
      - Randomly select players from the RCL while enforcing the  constraint.
      - Validate overall constraints (budget).
    """
    # Group players by position.
    position_groups = {pos: [] for pos in SELECTION.keys()}
    suggested_players_ids = team.copy()  # Start with the current team players to avoid duplicates
    for p in all_players:
        if p["element_type"] in position_groups:
            position_groups[p["element_type"]].append(p)

    # This will hold the transfer suggestions
    transfer_suggestions = {}
    suggest = 1
    # Stop after 3 valid transfer suggestions   
    while len(transfer_suggestions) < 3 :  
        team_player_count = team_count.copy()  # Reset team count for each suggestion

        # Try multiple attempts to form a valid transfer Sugestion.
        player_changes = player_change.copy()
        no_changes =  len(player_changes)
        selection = {}
        bank = budget  # Convert budget to the same unit as player costs

        # Loop until we have no more positions to change
        while len(player_changes) > 0:
            current_player_to_chnage = random.choice(player_changes)  # Select a random element

            #tr_positions.remove(current_tr_position)
            # For each required position, select player to replace.
            candidates = position_groups.get(current_player_to_chnage['element_type'], [])
            # Build the Restricted Candidate List (RCL): top rcl_percent of candidates.
            rcl_size = max(1, int(len(candidates) * rcl_percent))
            rcl = candidates[:rcl_size]
            for attempt in range(max_attempts):
                selected_candidate = random.choice(rcl)
                # Check if the selected candidate is valid for transfer.
                if validate_trasfer_selection(selected_candidate, current_player_to_chnage["XP"], bank,suggested_players_ids,team_player_count,current_player_to_chnage['status'] in "ad"):
                    # Check if the candidate is already selected.
                    # If valid, add to the selection.
                    selection[current_player_to_chnage['web_name']] = format_transfer(selected_candidate)
                    suggested_players_ids.append(selected_candidate['id'])
                    team_player_count[selected_candidate["team"]] = team_player_count.get(selected_candidate["team"], 0) + 1

                    bank -= selected_candidate["now_cost"]

                    # Remove the position from the list to avoid selecting it again.
                    player_changes.remove(current_player_to_chnage)
                    break  # Exit the attempt loop since we found a valid candidate.
            else:
                # If we exhaust attempts without finding a valid candidate, break the while loop.
                logger.warning(f"No valid candidate found for position {current_player_to_chnage['element_type']} after {max_attempts} attempts with budget {bank}.")
                break
        transfer_suggestions[f"TR{suggest}"] = selection if len(selection) == no_changes else None
        if transfer_suggestions[f"TR{suggest}"] is not None:
            transfer_suggestions[f"TR{suggest}"]["rem_balance"] = round(bank,1)
        # Successfully generated a valid Transfer Suggestion.
        suggest += 1

    return transfer_suggestions


 
def get_current_gw_opponent_diffuclty(team_id:int)-> int:
    team_fix = load_team_upcoming_fix_difficulty_data()
    return team_fix[team_id - 1]["GW_1"]
def select_random_squad():
    playersList = selectable_players_data()
    team = generate_team_greedy_randomized(playersList)
    return {
        "Total Team Price": sum(player["now_cost"] for player in team),
        "Team": format_squad(team),
    }

def format_dream_team(player):
    df = pd.DataFrame(player)
    df = df[
        [
            "id",
            "web_name",
            "element_type",
            "team",
            "opta_code",
            "now_cost",
            "event_points",
        ]
    ]
    df["team"] = df["team"].apply(lambda x: get_team_name(x))

    return df.to_dict(orient="records")

def get_squad_from_picks(picks:List[Dict[str, Any]]):
    fwd_counts,mid_count,def_count = ( 0,0,0)
    xi=[]
    subs = []
    xi_score = 0
    bench_score = 0
    stats = {}
    team_layout = {"XI": [], "Subs": []}

    
    for pick in picks:
        player=get_player_stats(pick["element"])
        if pick["position"] <12:
            if pick["is_captain"]:
                stats["C"] = (player["id"], player["XP"])
                xi_score += player["XP"]
            elif pick["is_vice_captain"]:
                stats["VC"] = (player["id"], player["XP"])
            fwd_counts += 1 if player["element_type"] == "FWD" else 0
            mid_count += 1 if player["element_type"] == "MID" else 0
            def_count += 1 if player["element_type"] == "DEF" else 0
          
            xi.append(player)
            xi_score += player["XP"]
        else:
            subs.append(player)
            bench_score += player["XP"]
    team_layout["XI"].extend([player for player in format_squad(xi)])
    team_layout["Subs"].extend([player for player in format_squad(subs)])
    return team_layout,stats,xi_score,bench_score, f"{def_count}-{mid_count}-{fwd_counts}"



def get_dream_team(picks:List[Dict[str, Any]]):
    fwd_counts,mid_count,def_count = ( 0,0,0)
    score = 0
    team = []
    
    for pick in picks:
        player=get_player_stats(pick["element"])
        fwd_counts += 1 if player["element_type"] == "FWD" else 0
        mid_count += 1 if player["element_type"] == "MID" else 0
        def_count += 1 if player["element_type"] == "DEF" else 0
        team.append(player)
        score += pick["points"]
    team = [player for player in format_dream_team(team)]
    return team,score, f"{def_count}-{mid_count}-{fwd_counts}"

def get_ai_dream_team(picks:List[int], formation:str):
    def_count,mid_count,fwd_counts = tuple(formation.split("-"))
    fwd_counts,mid_count,def_count = int(fwd_counts),int(mid_count),int(def_count)
    team = []
    xi=[]
    score = 0
    team.append(picks[0])

    team.extend(picks[2:2+def_count])

    team.extend(picks[7:7+mid_count])

    team.extend(picks[12:12+fwd_counts])
    for pick in team:
        player=get_player_stats(pick)
        xi.append(player)
        score += player["XP"]
    team = [player for player in format_dream_team(xi)]
    return team,score

def pick_squad(team):
    formation,players_scores = suggest_team_formation(team)
    team_layout,captain,xi_score,subs_score =  create_team_XI(formation,players_scores, team)
    
    return {"Formation": formation, "Team": team_layout,"Xi Expected Score": xi_score,"Subs Expected Score": subs_score,"Captain": captain}
def suggest_transfers(players:List[int],team:List[int],bank_balance:float = 0,free_transfers:int = 2) :
    '''
    Suggest transfers up to three given the players to be changed and the current balance

    @param player: List of players id to be changed
    @param free_transfers: Number of free transfers available
    @param bank_balance: Current bank balance
    @return: Dictionary with suggested transfers , remaining balance ,and negative pomits if transfers exceeds the free transfers
    '''
    players_to_be_changed = []
    team_count = {}
    budget = bank_balance
    old_expected_points = 0
    for player in team:
        player_stats = get_player_stats(player)

        if player  in players:
            player_selling_price = player_stats["now_cost"]
            player_price_change = player_stats["cost_change_start"]

            if player_price_change > 0:
                
                player_selling_price = ((player_selling_price * 10 - player_price_change ) + math.floor(player_selling_price * 0.5)) / 10

            players_to_be_changed.append(player_stats)
            old_expected_points +=player_stats["XP"]
            budget += player_selling_price
        else:
            team_count[player_stats["team"]] = team_count.get(player_stats["team"], 0) + 1
        
    players_pool  = selectable_players_data(use_xp=True)
    #suggestions =  generate_transfers_greedy_randomized(players_pool,players_to_be_changed,team,team_count,budget)
    suggestions =  get_transfer_suggestions_for_players(players_to_be_changed,players_pool,team,team_count,budget)
    minus = 0 if free_transfers >= len(players_to_be_changed) else 4 *  (free_transfers - len(players_to_be_changed))
    return {"suggested_transfers":suggestions,"minus_pts":minus}

def ai_suggest_transfer(team:List[int],bank_balance:float = 0,free_transfers:int = 2) :
    
    players = []
    team_count = {}
    budget = bank_balance
    for player in team:
        player_stats = get_player_stats(player)
            
        players.append(player_stats)
        team_count[player_stats["team"]] = team_count.get(player_stats["team"], 0) + 1
        
    players_pool  = selectable_players_data(use_xp=True)
    players.sort(key=lambda x: x["XP"])
    suggestions =  get_ai_transfer_suggestions(players,players_pool,team,team_count,budget)
    return {"ai_suggestions":suggestions,}



def load_squad_using_manager_id(manager_id):
    """
    Load a squad from a given manager ID.
    """
    info = load_info()
    logger.info(info)

    curr_gw = info["gw"] if info else 1

    gw = 1 if curr_gw == 1 else curr_gw - 1
    logger.info(f"Loading Squad for Manager ID: {manager_id} for GW: {gw} but current GW is {curr_gw}")
    respnse  = requests.get(get_mmg_squad_url.format(mng=manager_id,gw=gw)).json()

    try:
        bank = respnse["entry_history"]["bank"] / 10
        picks = respnse["picks"]
        team_layout,captain,xi_score,subs_score,formation =  get_squad_from_picks(picks)
        return {"Formation": formation, "Team": team_layout,"Xi Expected Score": xi_score,"Subs Expected Score": subs_score,"Captain": captain, "bank": bank}
    except :
        raise ValueError(f"Could not load the squad with the given ID {manager_id}.")

def load_dream_team(gw:int = 1):
    """
    Load the dream team from the FPL API.
    """
    response = requests.get(f"{DREAM_TEAM_URL}{gw}").json()
    
    team = response["team"]
    team,score,formation =  get_dream_team(team)
    return {"Formation": formation, "team": team,"total": score,"gw": gw,}


def load_ai_dream_team(gw:int = 1):
    """
    Load the dream team from the FPL API.
    """
    all_players = pd.DataFrame(load_players_data()).sort_values(by=["element_type","chance_of_playing_this_round","XP"], ascending=False)
    top_15 = []
    for pos,number in SELECTION.items():
        players_per_pos = all_players[all_players["element_type"] == pos].head(number)
        top_15.extend(players_per_pos.loc[:,"id"].tolist())
    formation , _ = suggest_team_formation(top_15)
    team,score = get_ai_dream_team(top_15,formation)
    return {"Formation": formation, "team": team,"total": score,"gw": gw,}

