import copy
import inspect
import logging
from dataclasses import dataclass
from typing import Optional

import pandas as pd

from player_performance_ratings.data_structures import ColumnNames, Match
from player_performance_ratings.predictor.match_predictor import MatchPredictor
from player_performance_ratings.ratings.enums import RatingColumnNames
from player_performance_ratings.ratings.opponent_adjusted_rating.start_rating_generator import \
    StartRatingGenerator
from player_performance_ratings.scorer.score import BaseScorer, LogLossScorer


@dataclass
class LeagueH2H:
    league: str
    opp_league: str
    weight: float
    mean_rating_change: float


class StartLeagueRatingOptimizer():

    def __init__(self, column_names: ColumnNames,
                 match_predictor: MatchPredictor,
                 max_iterations: int = 20,
                 learning_step: int = 20,
                 scorer: Optional[BaseScorer] = None,
                 weight_div: int = 500,
                 indirect_weight: float = 1.5,
                 verbose: int = 1,
                 ):
        self.match_predictor = match_predictor
        self.max_iterations = max_iterations
        self.learning_step = learning_step
        self.weight_div = weight_div
        self.indirect_weight = indirect_weight
        self.column_names = column_names
        self.verbose = verbose
        self._scores = []
        self._league_ratings_iterations = []
        self.scorer = scorer or LogLossScorer(target=self.match_predictor.predictor.target,
                                              pred_column=self.match_predictor.predictor.pred_column)

    def optimize(self, df: pd.DataFrame, matches: Optional[list[Match]] = None) -> dict[str, float]:
        start_rating_generator = self.match_predictor.rating_generator.team_rating_generator.player_rating_generator.start_rating_generator
        league_ratings = start_rating_generator.league_ratings.copy()
        start_rating_params = list(
            inspect.signature(start_rating_generator.__class__.__init__).parameters.keys())[1:]
        non_league_ratings_params = {attr: getattr(start_rating_generator, attr) for attr in
                                     dir(start_rating_generator) if
                                     attr in start_rating_params and attr != 'league_ratings'}

        for iteration in range(self.max_iterations + 1):

            start_rating_generator = StartRatingGenerator(league_ratings=league_ratings, **non_league_ratings_params)

            if iteration == self.max_iterations:
                min_idx = self._scores.index(min(self._scores))
                logging.info(f"best start rating params {self._league_ratings_iterations[min_idx]}")
                return self._league_ratings_iterations[min_idx]

            match_predictor = copy.deepcopy(self.match_predictor)
            match_predictor.rating_generator.team_rating_generator.player_rating_generator.start_rating_generator = start_rating_generator
            df = match_predictor.generate_historical(df=df, store_ratings=False)
            score = self.scorer.score(df)

            if not league_ratings:
                league_ratings = match_predictor.rating_generator.team_rating_generator.player_rating_generator.start_rating_generator.league_ratings

            self._league_ratings_iterations.append(league_ratings)
            self._scores.append(score)
            if self.verbose:
                logging.info(f"iteration {iteration} finished. Score: {score}. best startings {league_ratings}")

            league_rating_changes = (df
            .groupby([RatingColumnNames.PLAYER_LEAGUE, RatingColumnNames.OPPONENT_LEAGUE])
            .agg(
                {
                    RatingColumnNames.PLAYER_RATING_CHANGE: 'mean',
                    self.column_names.player_id: 'count'
                }
            )
            .reset_index()
            .rename(
                columns={
                    RatingColumnNames.PLAYER_RATING_CHANGE: 'mean_rating_change',
                    self.column_names.player_id: 'count'
                })
            )
            leagues = league_rating_changes[RatingColumnNames.PLAYER_LEAGUE].unique().tolist()

            league_opp_league_h2hs: dict[str, dict[str, LeagueH2H]] = {}
            league_to_played_against_leagues = {}

            for league in leagues:
                league_to_played_against_leagues[league] = \
                    league_rating_changes[league_rating_changes[RatingColumnNames.PLAYER_LEAGUE] == league][
                        RatingColumnNames.OPPONENT_LEAGUE].unique().tolist()

                league_opp_league_h2hs[league] = {}
                for opp_league in leagues:
                    if league == opp_league:
                        continue

                    rows = league_rating_changes[(league_rating_changes[RatingColumnNames.PLAYER_LEAGUE] == league) &
                                                 (league_rating_changes[
                                                      RatingColumnNames.OPPONENT_LEAGUE] == opp_league)]
                    if len(rows) == 0:
                        weight = 0
                        mean_rating_change = 0
                    else:
                        count = rows['count'].iloc[0]
                        weight = min(1, count / self.weight_div)
                        mean_rating_change = rows['mean_rating_change'].iloc[0]

                    league_h2h = LeagueH2H(
                        weight=weight,
                        mean_rating_change=mean_rating_change,
                        league=league,
                        opp_league=opp_league,
                    )
                    league_opp_league_h2hs[league][opp_league] = league_h2h

            final_league_opp_league_h2hs: dict[str, dict[str, LeagueH2H]] = {}

            league_sum_final_weights = {}
            for league, opp_league_h2h in league_opp_league_h2hs.items():

                league_sum_final_weights[league] = 0
                final_league_opp_league_h2hs[league] = {}
                for opp_league, h2h in opp_league_h2h.items():

                    if h2h.weight < 1:
                        league_against = league_to_played_against_leagues[league]
                        opp_played_against = league_to_played_against_leagues[opp_league]
                        shared_leagues = [l for l in league_against if
                                          l in opp_played_against and l not in (league, opp_league)]

                        relative_shared_weighted_mean = 0
                        shared_weight = 0
                        for shared_league in shared_leagues:
                            opp_vs_shared_h2h = league_opp_league_h2hs[opp_league][shared_league]
                            league_vs_shared_h2h = league_opp_league_h2hs[league][shared_league]

                            sum_rating_change = league_vs_shared_h2h.mean_rating_change - opp_vs_shared_h2h.mean_rating_change

                            relative_shared_weighted_mean += min(
                                opp_vs_shared_h2h.weight, league_vs_shared_h2h.weight) * sum_rating_change
                            shared_weight += min(
                                opp_vs_shared_h2h.weight, league_vs_shared_h2h.weight)

                    weighted_mean = h2h.mean_rating_change * h2h.weight + min(1,
                                                                              shared_weight * self.indirect_weight) * relative_shared_weighted_mean
                    final_weight = min(1, h2h.weight + shared_weight)
                    final_league_h2h = LeagueH2H(
                        weight=final_weight,
                        mean_rating_change=weighted_mean,
                        league=league,
                        opp_league=opp_league,
                    )
                    final_league_opp_league_h2hs[league][opp_league] = final_league_h2h
                    league_sum_final_weights[league] += final_weight

            for league, final_opp_league_h2h in final_league_opp_league_h2hs.items():

                if league not in league_ratings:
                    continue
                start_rating_rating_change = 0

                for opp_league, final_h2h in final_opp_league_h2h.items():
                    start_rating_rating_change += final_h2h.weight / league_sum_final_weights[
                        league] * final_h2h.mean_rating_change * self.learning_step

                league_ratings[league] += start_rating_rating_change

        return league_ratings
