from typing import Dict, Tuple, List, Iterable, Set, Any, DefaultDict, Optional, TYPE_CHECKING
from collections import Counter, defaultdict
from difflib import SequenceMatcher
from itertools import combinations, chain, permutations
from pydfs_lineup_optimizer.settings import LineupPosition
from pydfs_lineup_optimizer.exceptions import LineupOptimizerException


if TYPE_CHECKING:
    from pydfs_lineup_optimizer.player import Player, LineupPlayer


def list_intersection(first_list: Iterable[Any], second_list: Iterable[Any]) -> bool:
    for el in first_list:
        if el in second_list:
            return True
    return False


def ratio(search_string: str, possible_match: str) -> float:
    search_string = search_string.lower()
    possible_match = possible_match.lower()
    if len(search_string) >= len(possible_match):
        parts = [possible_match]
    else:
        shorter_length = len(search_string)
        num_of_parts = len(possible_match) - shorter_length
        parts = [possible_match[i:i + shorter_length] for i in range(num_of_parts + 1)]
    return max([SequenceMatcher(None, search_string, part).ratio() for part in parts])


def get_positions_for_optimizer(
        positions_list: List[LineupPosition],
        multi_positions_combinations: Optional[Set[Tuple[str, ...]]] = None
) -> Dict[Tuple[str, ...], int]:
    """
    Convert positions list into dict for using in optimizer.
    """
    positions = {}
    positions_counter = Counter([tuple(sorted(p.positions)) for p in positions_list])
    for key in positions_counter.keys():
        min_value = positions_counter[key] + len(list(filter(
            lambda p: len(p.positions) < len(key) and list_intersection(key, p.positions), positions_list
        )))
        positions[key] = min_value
    if not multi_positions_combinations:
        return positions
    #  Create list of required combinations for consistency of multi-positions
    for i in range(2, len(multi_positions_combinations)):
        total_combinations = len(multi_positions_combinations)
        for positions_tuple in combinations(multi_positions_combinations, i):
            flatten_positions = tuple(sorted(set(chain.from_iterable(positions_tuple))))
            multi_positions_combinations.add(flatten_positions)
        if total_combinations == len(multi_positions_combinations):
            break
    multi_positions_combinations.update(positions.keys())
    for i in range(2, len(positions)):
        for positions_tuple in combinations(positions_counter.keys(), i):
            flatten_positions = tuple(sorted(set(chain.from_iterable(positions_tuple))))
            if flatten_positions in positions or flatten_positions not in multi_positions_combinations:
                continue
            min_value = sum(positions[pos] for pos in positions_tuple)
            positions[flatten_positions] = min_value
    return positions


def link_players_with_positions(
        players: List['Player'],
        positions: List[LineupPosition]
) -> Dict['Player', LineupPosition]:
    """
    This method tries to set positions for given players, and raise error if can't.
    """
    positions = positions[:]
    players_with_positions = {}  # type: Dict['Player', LineupPosition]
    players = sorted(players, key=get_player_priority)
    for position in positions:
        players_for_position = [p for p in players if list_intersection(position.positions, p.positions)]
        if len(players_for_position) == 1:
            players_with_positions[players_for_position[0]] = position
            positions.remove(position)
            players.remove(players_for_position[0])
    for players_permutation in permutations(players):
        is_correct = True
        remaining_positions = positions[:]
        for player in players_permutation:
            for position in remaining_positions:
                if list_intersection(player.positions, position.positions):
                    players_with_positions[player] = position
                    remaining_positions.remove(position)
                    break
            else:
                is_correct = False
                break
        if is_correct:
            break
    else:
        raise LineupOptimizerException('Unable to build lineup')
    return players_with_positions


def get_remaining_positions(
        positions: List[LineupPosition],
        unswappable_players: List['LineupPlayer']
) -> List[LineupPosition]:
    """
    Remove unswappable players positions from positions list
    """
    positions = positions[:]
    for player in unswappable_players:
        for position in positions:
            if position.name == player.lineup_position:
                positions.remove(position)
                break
    return positions


def get_players_grouped_by_teams(
        players: Iterable['Player'],
        for_teams: Optional[List[str]] = None,
        for_positions: Optional[List[str]] = None,
) -> DefaultDict[str, List['Player']]:
    players_by_teams = defaultdict(list)  # type: DefaultDict[str, List['Player']]
    for player in players:
        if for_teams is not None and player.team not in for_teams:
            continue
        if for_positions is not None and not list_intersection(player.positions, for_positions):
            continue
        players_by_teams[player.team].append(player)
    return players_by_teams


def process_percents(percent: Optional[float]) -> Optional[float]:
    return percent / 100 if percent and percent > 1 else percent


def get_player_priority(player: 'Player') -> Tuple[int, float]:
    game_starts_at = player.game_info.starts_at.timestamp() if player.game_info and player.game_info.starts_at else 0
    return (-player.rank.value, game_starts_at)