import logging
import random

import pydantic

from creation_helpers import mround, sanitize_chance_output
from typing import List, Literal
from decimal import Decimal


class BattingCardRatingsModel(pydantic.BaseModel):
    battingcard_id: int
    bat_hand: Literal['R', 'L', 'S']
    vs_hand: Literal['R', 'L']
    all_hits: Decimal = Decimal(0.0)
    all_other_ob: Decimal = Decimal(0.0)
    all_outs: Decimal = Decimal(0.0)
    rem_singles: Decimal = Decimal(0.0)
    rem_xbh: Decimal = Decimal(0.0)
    rem_hr: Decimal = Decimal(0.0)
    rem_doubles: Decimal = Decimal(0.0)
    hard_rate: Decimal
    med_rate: Decimal
    soft_rate: Decimal
    pull_rate: Decimal
    center_rate: Decimal
    slap_rate: Decimal
    homerun: Decimal = Decimal(0.0)
    bp_homerun: Decimal = Decimal(0.0)
    triple: Decimal = Decimal(0.0)
    double_three: Decimal = Decimal(0.0)
    double_two: Decimal = Decimal(0.0)
    double_pull: Decimal = Decimal(0.0)
    single_two: Decimal = Decimal(0.0)
    single_one: Decimal = Decimal(0.0)
    single_center: Decimal = Decimal(0.0)
    bp_single: Decimal = Decimal(0.0)
    hbp: Decimal = Decimal(0.0)
    walk: Decimal = Decimal(0.0)
    strikeout: Decimal = Decimal(0.0)
    lineout: Decimal = Decimal(0.0)
    popout: Decimal = Decimal(0.0)
    rem_flyballs: Decimal = Decimal(0.0)
    flyout_a: Decimal = Decimal(0.0)
    flyout_bq: Decimal = Decimal(0.0)
    flyout_lf_b: Decimal = Decimal(0.0)
    flyout_rf_b: Decimal = Decimal(0.0)
    rem_groundballs: Decimal = Decimal(0.0)
    groundout_a: Decimal = Decimal(0.0)
    groundout_b: Decimal = Decimal(0.0)
    groundout_c: Decimal = Decimal(0.0)
    avg: Decimal = 0.0
    obp: Decimal = 0.0
    slg: Decimal = 0.0

    def total_chances(self):
        return Decimal(sum([
            self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_pull,
            self.single_two, self.single_one, self.single_center, self.bp_single, self.hbp, self.walk, self.strikeout,
            self.lineout, self.popout, self.flyout_a, self.flyout_bq, self.flyout_lf_b, self.flyout_rf_b,
            self.groundout_a, self.groundout_b, self.groundout_c
        ]))

    def total_hits(self):
        return Decimal(sum([
            self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_pull,
            self.single_two, self.single_one, self.single_center, self.bp_single
        ]))

    def rem_hits(self):
        return (self.all_hits -
                sum([
                    self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_pull,
                    self.single_two, self.single_one, self.single_center, self.bp_single
                ]))

    def rem_outs(self):
        return Decimal(self.all_outs -
                       sum([
                           self.strikeout, self.lineout, self.popout, self.flyout_a, self.flyout_bq, self.flyout_lf_b,
                           self.flyout_rf_b, self.groundout_a, self.groundout_b, self.groundout_c
                       ]))

    def rem_other_ob(self):
        return self.all_other_ob - self.hbp - self.walk

    def calculate_singles(self, szn_singles, szn_hits, ifh_rate: Decimal):
        tot = sanitize_chance_output(self.all_hits * Decimal((szn_singles * .8) / max(szn_hits, 1)))
        logging.debug(f'tot: {tot}')
        self.rem_singles = tot

        self.bp_single = bp_singles(self.rem_singles)
        self.rem_singles -= self.bp_single

        self.single_two = wh_singles(self.rem_singles, self.hard_rate)
        self.rem_singles -= self.single_two

        self.single_one = one_singles(self.rem_singles, ifh_rate)
        self.rem_singles -= self.single_one

        self.single_center = sanitize_chance_output(self.rem_singles)
        self.rem_singles -= self.single_center

        self.rem_xbh = self.all_hits - self.bp_single - self.single_two - self.single_one - self.single_center

    def calculate_xbh(self, szn_triples, szn_doubles, szn_hr, hr_per_fb: Decimal):
        self.triple = triples(self.rem_xbh, szn_triples, szn_doubles + szn_hr)
        self.rem_xbh -= self.triple

        tot_doubles = sanitize_chance_output(self.rem_xbh * Decimal(szn_doubles / max(szn_hr + szn_doubles, 1)))
        self.double_two = two_doubles(tot_doubles, self.soft_rate)
        self.double_pull = sanitize_chance_output(tot_doubles - self.double_two)
        self.rem_xbh -= Decimal(self.double_two + self.double_pull)

        if self.rem_xbh > Decimal(0):
            self.bp_homerun = bp_homeruns(self.rem_xbh, hr_per_fb)
            self.homerun = sanitize_chance_output(self.rem_xbh - self.bp_homerun, min_chances=0.5)
            self.rem_xbh -= Decimal(self.bp_homerun + self.homerun)

        if szn_triples > 0 and self.rem_xbh > 0:
            self.triple = sanitize_chance_output(self.rem_xbh, min_chances=0.5)
        if self.rem_xbh > 0:
            logging.error(f'Adding {self.rem_xbh} results to all other ob')
            # print(self)
            self.all_other_ob += self.rem_xbh

    def calculate_other_ob(self, szn_bb, szn_hbp):
        self.hbp = hit_by_pitch(self.all_other_ob, szn_hbp, szn_bb)
        self.walk = sanitize_chance_output(self.all_other_ob - self.hbp)

        if self.walk + self.hbp < self.all_other_ob:
            rem = self.all_other_ob - self.walk - self.hbp
            logging.error(f'Adding {rem} chances to all_outs')
            # print(self)
            self.all_outs += Decimal(rem)

    def calculate_strikeouts(self, szn_so, szn_ab, szn_hits):
        self.strikeout = strikeouts(self.all_outs, (szn_so / max(szn_ab - szn_hits, 1)))

    def calculate_other_outs(self, fb_rate, ld_rate, gb_rate, szn_gidp, szn_ab):
        self.rem_flyballs = sanitize_chance_output(self.rem_outs() * Decimal(fb_rate))
        self.flyout_a = flyout_a(self.rem_flyballs, self.hard_rate)
        self.rem_flyballs -= self.flyout_a

        self.flyout_bq = flyout_bq(self.rem_flyballs, self.soft_rate)
        self.rem_flyballs -= self.flyout_bq

        self.flyout_lf_b = flyout_b(
            self.rem_flyballs,
            pull_rate=self.pull_rate if self.bat_hand == 'R' else self.slap_rate,
            cent_rate=self.center_rate
        )
        self.rem_flyballs -= self.flyout_lf_b
        self.flyout_rf_b = sanitize_chance_output(self.rem_flyballs)
        self.rem_flyballs -= self.flyout_rf_b

        if self.rem_flyballs > 0:
            logging.debug(f'Adding {self.rem_flyballs} chances to lineouts')

        tot_oneouts = sanitize_chance_output(self.rem_outs() * Decimal(ld_rate / max(ld_rate + gb_rate, .01)))
        self.lineout = sanitize_chance_output(Decimal(random.random()) * tot_oneouts)
        self.popout = sanitize_chance_output(tot_oneouts - self.lineout)

        self.groundout_a = groundball_a(self.rem_outs(), szn_gidp, szn_ab)
        self.groundout_c = groundball_c(self.rem_outs(), self.med_rate)
        self.groundout_b = self.rem_outs()

    def calculate_rate_stats(self):
        self.avg = Decimal(round(self.total_hits() / 108, 3))
        self.obp = Decimal(round((self.total_hits() + self.hbp + self.walk) / 108, 3))
        self.slg = Decimal(round(
            self.homerun * 4 + self.triple * 3 + self.single_center + self.single_two + self.single_two +
            (self.double_two + self.double_three + self.double_two + self.bp_homerun) * 2 + self.bp_single / 2
        ))

    def custom_to_dict(self):
        return {
            'battingcard_id': self.battingcard_id,
            'vs_hand': self.vs_hand,
            'homerun': float(self.homerun),
            'bp_homerun': float(self.bp_homerun),
            'triple': float(self.triple),
            'double_three': float(self.double_three),
            'double_two': float(self.double_two),
            'double_pull': float(self.double_pull),
            'single_two': float(self.single_two),
            'single_one': float(self.single_one),
            'single_center': float(self.single_center),
            'bp_single': float(self.bp_single),
            'hbp': float(self.hbp),
            'walk': float(self.walk),
            'strikeout': float(self.strikeout),
            'lineout': float(self.lineout),
            'popout': float(self.popout),
            'flyout_a': float(self.flyout_a),
            'flyout_bq': float(self.flyout_bq),
            'flyout_lf_b': float(self.flyout_lf_b),
            'flyout_rf_b': float(self.flyout_rf_b),
            'groundout_a': float(self.groundout_a),
            'groundout_b': float(self.groundout_b),
            'groundout_c': float(self.groundout_c),
            'pull_rate': float(self.pull_rate),
            'center_rate': float(self.center_rate),
            'slap_rate': float(self.slap_rate)
        }

# def total_chances(chance_data):
#     sum_chances = 0
#     for key in chance_data:
#         if key not in ['id', 'player_id', 'cardset_id', 'vs_hand', 'is_prep']:
#             sum_chances += chance_data[key]
#
#     return mround(sum_chances)


def total_singles(all_hits, szn_singles, szn_hits):
    return sanitize_chance_output(all_hits * ((szn_singles * .8) / max(szn_hits, 1)))


def bp_singles(all_singles):
    if all_singles < 6:
        return Decimal(0)
    else:
        return Decimal(5)


def wh_singles(rem_singles, hard_rate):
    if rem_singles == 0 or hard_rate < .2:
        return 0
    elif hard_rate > .4:
        return sanitize_chance_output(rem_singles * Decimal(.666), min_chances=2)
    else:
        return sanitize_chance_output(rem_singles * Decimal(.333), min_chances=2)


def one_singles(rem_singles, ifh_rate, force_rem=False):
    if force_rem:
        return mround(rem_singles)
    elif rem_singles == 0 or ifh_rate < .05:
        return Decimal(0)
    else:
        return sanitize_chance_output(rem_singles * ifh_rate * Decimal(3), min_chances=2)


def all_homeruns(rem_hits, all_hits, hrs, hits, singles):
    if rem_hits == 0 or all_hits == 0 or hrs == 0 or hits - singles == 0:
        return 0
    else:
        return mround(min(rem_hits, all_hits * ((hrs * 1.15) / max(hits, 1))))


def nd_homeruns(all_hr, hr_rate):
    if all_hr == 0 or hr_rate == 0:
        return Decimal(0)
    elif hr_rate > .2:
        return sanitize_chance_output(all_hr * .6)
    else:
        return sanitize_chance_output(all_hr * .25)


def bp_homeruns(all_hr, hr_rate):
    if all_hr == 0 or hr_rate == 0:
        return Decimal(0)
    elif hr_rate > .2:
        return sanitize_chance_output(all_hr * Decimal(.4), rounding=1.0)
    else:
        return sanitize_chance_output(all_hr * Decimal(.8), rounding=1.0)


def triples(all_xbh, tr_count, do_count):
    if all_xbh == Decimal(0) or tr_count == Decimal(0):
        return Decimal(0)
    else:
        return sanitize_chance_output(all_xbh * Decimal(tr_count / max(tr_count + do_count, 1)), min_chances=1)


def two_doubles(all_doubles, soft_rate):
    if all_doubles == 0 or soft_rate == 0:
        return Decimal(0)
    elif soft_rate > .2:
        return sanitize_chance_output(all_doubles / 2)
    else:
        return sanitize_chance_output(all_doubles / 4)


def hit_by_pitch(other_ob, hbps, walks):
    if hbps == 0 or other_ob * Decimal(hbps / max(hbps + walks, 1)) < 1:
        return 0
    else:
        return sanitize_chance_output(other_ob * Decimal(hbps / max(hbps + walks, 1)), rounding=1.0)


def strikeouts(all_outs, k_rate):
    if all_outs == 0 or k_rate == 0:
        return Decimal(0)
    else:
        return sanitize_chance_output(all_outs * Decimal(k_rate))


def flyout_a(all_flyouts, hard_rate):
    if all_flyouts == 0 or hard_rate < .4:
        return Decimal(0)
    else:
        return Decimal(1.0)


def flyout_bq(rem_flyouts, soft_rate):
    if rem_flyouts == 0 or soft_rate < .1:
        return Decimal(0)
    else:
        return sanitize_chance_output(rem_flyouts * min(soft_rate * 3, Decimal(.75)))


def flyout_b(rem_flyouts, pull_rate, cent_rate):
    if rem_flyouts == 0 or pull_rate == 0:
        return Decimal(0)
    else:
        return sanitize_chance_output(rem_flyouts * (pull_rate + cent_rate / 2))


def popouts(rem_outs, iffb_rate):
    if rem_outs == 0 or iffb_rate * rem_outs < 1:
        return 0
    else:
        return mround(rem_outs * iffb_rate)


def groundball_a(all_groundouts, gidps, abs):
    if all_groundouts == 0 or gidps == 0:
        return Decimal(0)
    else:
        return sanitize_chance_output(Decimal(min(gidps ** 2.5, abs) / max(abs, 1)) * all_groundouts)


def groundball_c(rem_groundouts, med_rate):
    if rem_groundouts == 0 or med_rate < .4:
        return Decimal(0)
    elif med_rate > .6:
        return sanitize_chance_output(rem_groundouts)
    else:
        return sanitize_chance_output(rem_groundouts * med_rate)


def stealing(chances: int, sb2s: int, cs2s: int, sb3s: int, cs3s: int, season_pct: float):
    if chances == 0 or sb2s + cs2s == 0:
        return 0, 0, False, 0

    total_attempts = sb2s + cs2s + sb3s + cs3s
    attempt_pct = total_attempts / chances

    if attempt_pct >= .08:
        st_auto = True
    else:
        st_auto = False

    # chance_odds = [x / 36 for x in range(1, 36)]
    st_jump = 0
    for x in range(1, 37):
        if attempt_pct * 1.5 <= x / 36:
            st_jump = x / 36
            break

    st_high = mround(20 * (sb2s / (sb2s + cs2s + cs2s)))
    if st_high <= 10:
        st_auto = False

    if sb3s + cs3s < max((3 * season_pct), 1):
        st_low = 3
    else:
        st_low = mround(16 * ((sb2s + sb3s) / (sb2s + sb3s + cs2s * 2 + cs3s * 2)))
        if not st_auto:
            st_low = min(st_low, 10)

    if st_low >= st_high - 3:
        if st_high == 0:
            st_low = 0
            st_jump = 0
        elif st_high <= 3:
            st_high = 4
            st_low = 1
        else:
            st_low = st_high - 3

    # if ((st_high - 7) > st_low) and st_high > 7:
    #     st_low = st_high - 7

    return round(st_low), round(st_high), st_auto, st_jump


def stealing_line(steal_data: dict):
    sd = steal_data
    jump_chances = round(sd[3] * 36)

    if jump_chances == 0:
        good_jump = '-'
    elif jump_chances <= 6:
        if jump_chances == 6:
            good_jump = 7
        elif jump_chances == 5:
            good_jump = 6
        elif jump_chances == 4:
            good_jump = 5
        elif jump_chances == 3:
            good_jump = 4
        elif jump_chances == 2:
            good_jump = 3
        elif jump_chances == 1:
            good_jump = 2
    elif jump_chances == 7:
        good_jump = '4,5'
    elif jump_chances == 8:
        good_jump = '4,6'
    elif jump_chances == 9:
        good_jump = '3-5'
    elif jump_chances == 10:
        good_jump = '2-5'
    elif jump_chances == 11:
        good_jump = '6,7'
    elif jump_chances == 12:
        good_jump = '4-6'
    elif jump_chances == 13:
        good_jump = '2,4-6'
    elif jump_chances == 14:
        good_jump = '3-6'
    elif jump_chances == 15:
        good_jump = '2-6'
    elif jump_chances == 16:
        good_jump = '2,5-6'
    elif jump_chances == 17:
        good_jump = '3,5-6'
    elif jump_chances == 18:
        good_jump = '4-6'
    elif jump_chances == 19:
        good_jump = '2,4-7'
    elif jump_chances == 20:
        good_jump = '3-7'
    elif jump_chances == 21:
        good_jump = '2-7'
    elif jump_chances == 22:
        good_jump = '2-7,12'
    elif jump_chances == 23:
        good_jump = '2-7,11'
    elif jump_chances == 24:
        good_jump = '2,4-8'
    elif jump_chances == 25:
        good_jump = '3-8'
    elif jump_chances == 26:
        good_jump = '2-8'
    elif jump_chances == 27:
        good_jump = '2-8,12'
    elif jump_chances == 28:
        good_jump = '2-8,11'
    elif jump_chances == 29:
        good_jump = '3-9'
    elif jump_chances == 30:
        good_jump = '2-9'
    elif jump_chances == 31:
        good_jump = '2-9,12'
    elif jump_chances == 32:
        good_jump = '2-9,11'
    elif jump_chances == 33:
        good_jump = '2-10'
    elif jump_chances == 34:
        good_jump = '3-11'
    elif jump_chances == 35:
        good_jump = '2-11'
    else:
        good_jump = '2-12'

    return f'{"*" if sd[2] else ""}{good_jump}/- ({sd[1] if sd[1] else "-"}-{sd[0] if sd[0] else "-"})'


def running(extra_base_pct: str):
    if extra_base_pct == '':
        return 8
    try:
        xb_pct = float(extra_base_pct.strip("%")) / 100
    except Exception as e:
        logging.error(f'calcs_batter running - {e}')
        xb_pct = 20

    return round(8 + (10 * xb_pct))


def hit_and_run(ab_vl: int, ab_vr: int, hits_vl: int, hits_vr: int, hr_vl: int, hr_vr: int, so_vl: int, so_vr: int):
    babip = (hits_vr + hits_vl - hr_vl - hr_vr) / max(ab_vl + ab_vr - so_vl - so_vr - hr_vl - hr_vl, 1)
    if babip >= .35:
        return 'A'
    elif babip >= .3:
        return 'B'
    elif babip >= .25:
        return 'C'
    else:
        return 'D'


def get_batter_ratings(df_data) -> List[dict]:
    # Consider a sliding offense_mod based on OPS; floor of 1x and ceiling of 1.5x ?
    offense_mod = 1.2
    vl = BattingCardRatingsModel(
        battingcard_id=df_data.battingcard_id,
        bat_hand=df_data['bat_hand'],
        vs_hand='L',
        all_hits=sanitize_chance_output(108 * offense_mod * df_data['AVG_vL']),
        all_other_ob=sanitize_chance_output(108 * offense_mod *
                                            ((df_data['BB_vL'] + df_data['HBP_vL']) / df_data['PA_vL'])),
        hard_rate=df_data['Hard%_vL'],
        med_rate=df_data['Med%_vL'],
        soft_rate=df_data['Soft%_vL'],
        pull_rate=df_data['Pull%_vL'],
        center_rate=df_data['Cent%_vL'],
        slap_rate=df_data['Oppo%_vL']
    )
    vr = BattingCardRatingsModel(
        battingcard_id=df_data.battingcard_id,
        bat_hand=df_data['bat_hand'],
        vs_hand='R',
        all_hits=sanitize_chance_output(108 * offense_mod * df_data['AVG_vR']),
        all_other_ob=sanitize_chance_output(108 * offense_mod *
                                            ((df_data['BB_vR'] + df_data['HBP_vR']) / df_data['PA_vR'])),
        hard_rate=df_data['Hard%_vR'],
        med_rate=df_data['Med%_vR'],
        soft_rate=df_data['Soft%_vR'],
        pull_rate=df_data['Pull%_vR'],
        center_rate=df_data['Cent%_vR'],
        slap_rate=df_data['Oppo%_vR']
    )
    vl.all_outs = Decimal(108 - vl.all_hits - vl.all_other_ob).quantize(Decimal("0.05"))
    vr.all_outs = Decimal(108 - vr.all_hits - vr.all_other_ob).quantize(Decimal("0.05"))

    vl.calculate_singles(df_data['1B_vL'], df_data['H_vL'], Decimal(df_data['IFH%_vL']))
    vr.calculate_singles(df_data['1B_vR'], df_data['H_vR'], Decimal(df_data['IFH%_vR']))

    logging.debug(
        f'vL - All Hits: {vl.all_hits} / Other OB: {vl.all_other_ob} / All Outs: {vl.all_outs} '
        f'/ Total: {vl.all_hits + vl.all_other_ob + vl.all_outs}'
    )
    logging.debug(
        f'vR - All Hits: {vr.all_hits} / Other OB: {vr.all_other_ob} / All Outs: {vr.all_outs} '
        f'/ Total: {vr.all_hits + vr.all_other_ob + vr.all_outs}'
    )

    vl.calculate_xbh(df_data['3B_vL'], df_data['2B_vL'], df_data['HR_vL'], df_data['HR/FB_vL'])
    vr.calculate_xbh(df_data['3B_vR'], df_data['2B_vR'], df_data['HR_vR'], df_data['HR/FB_vR'])

    logging.debug(f'all_hits: {vl.all_hits} / sum of hits: {vl.total_chances()}')
    logging.debug(f'all_hits: {vr.all_hits} / sum of hits: {vr.total_chances()}')

    vl.calculate_other_ob(df_data['BB_vL'], df_data['HBP_vL'])
    vr.calculate_other_ob(df_data['BB_vR'], df_data['HBP_vR'])

    logging.debug(f'all on base: {vl.hbp + vl.walk + vl.total_hits()} / all chances: {vl.total_chances()}'
                  f'{"*******ERROR ABOVE*******" if vl.hbp + vl.walk + vl.total_hits() != vl.total_chances()  else ""}')
    logging.debug(f'all on base: {vr.hbp + vr.walk + vr.total_hits()} / all chances: {vr.total_chances()}'
                  f'{"*******ERROR ABOVE*******" if vr.hbp + vr.walk + vr.total_hits() != vr.total_chances()  else ""}')

    vl.calculate_strikeouts(df_data['SO_vL'], df_data['AB_vL'], df_data['H_vL'])
    vr.calculate_strikeouts(df_data['SO_vR'], df_data['AB_vR'], df_data['H_vR'])

    logging.debug(f'K rate vL: {round(vl.strikeout / vl.all_outs, 2)} / '
                  f'K rate vR: {round(vr.strikeout / vr.all_outs, 2)}')

    vl.calculate_other_outs(
        df_data['FB%_vL'], df_data['LD%_vL'], df_data['GB%_vL'], df_data['GDP_vL'], df_data['AB_vL']
    )
    vr.calculate_other_outs(
        df_data['FB%_vR'], df_data['LD%_vR'], df_data['GB%_vR'], df_data['GDP_vR'], df_data['AB_vR']
    )

    # Correct total chance errors
    for x in [vl, vr]:
        if x.total_chances() < 108:
            diff = Decimal(108) - x.total_chances()
            logging.error(f'Adding {diff} strikeouts to close gap')
            x.strikeout += diff
        elif x.total_chances() > 108:
            diff = x.total_chances() - Decimal(108)
            logging.error(f'Have surplus of {diff} chances')
            if x.strikeout + 1 > diff:
                logging.error(f'Subtracting {diff} strikeouts to close gap')
                x.strikeout -= diff
            elif x.lineout + 1 > diff:
                logging.error(f'Subtracting {diff} lineouts to close gap')
                x.lineout -= diff
            elif x.groundout_a + 1 > diff:
                logging.error(f'Subtracting {diff} gbA to close gap')
                x.groundout_a -= diff
            elif x.groundout_b + 1 > diff:
                logging.error(f'Subtracting {diff} gbB to close gap')
                x.groundout_b -= diff
            elif x.groundout_c + 1 > diff:
                logging.error(f'Subtracting {diff} gbC to close gap')
                x.groundout_c -= diff

    vl_total_chances = vl.total_chances()
    vr_total_chances = vr.total_chances()
    if vl_total_chances != 108:
        logging.error(f'total chances for {df_data.name} come to {vl_total_chances}')
    else:
        logging.debug(f'total chances: {vl_total_chances}')
    if vr_total_chances != 108:
        logging.error(f'total chances for {df_data.name} come to {vr_total_chances}')
    else:
        logging.debug(f'total chances: {vr_total_chances}')

    return [vl.custom_to_dict(), vr.custom_to_dict()]
