"""
Module used for all CR operations, based on logic defined in 
DND 5e(2014) DMG p.81-85
"""
import os
from math import inf
from typing import List, Dict, Tuple
from utils.utils import list_json_files_in_subdir, prepare_json_table
from dm_utils.utils import get_elem_in_range_from_json_table

TABLES_SUBDIR = "dm_tables_json.encounter_planning"
TABLE_FILENAMES = list_json_files_in_subdir(
    TABLES_SUBDIR
)

CWD = os.getcwd()
TABLE_FILEPATHS = dict()
for filename in TABLE_FILENAMES:
    file_key = filename.split('.')[0]
    full_path_to_file = os.path.join(
        CWD,
        TABLES_SUBDIR.replace('.', '/'),
        filename
    )
    TABLE_FILEPATHS[file_key] = full_path_to_file
    
XP_THRESHOLDS_TABLE = prepare_json_table(
    TABLE_FILEPATHS['xp_thresholds_by_character_level']
)
ENCOUNTER_MULTIPLIERS_TABLE = prepare_json_table(
    TABLE_FILEPATHS['encounter_multipliers']
)
ADVENTURING_DAY_XP_TABLE = prepare_json_table(
    TABLE_FILEPATHS['adventuring_day_xp_by_level']
)


#TODO: add some dataclasses, don't be a pussy
def determine_party_xp_thresholds(party_levels: List[int]) -> List[int]:
    threshold_values = {
        "easy": {
            "sum": 0,
            "xp_thresholds": []
        },
        "medium": {
            "sum": 0,
            "xp_thresholds": []
        },
        "hard": {
            "sum": 0,
            "xp_thresholds": []
        },
        "deadly": {
            "sum": 0,
            "xp_thresholds": []
        },
    }

    for level in party_levels:
        thresholds_level_row = XP_THRESHOLDS_TABLE[str(level)]

        easy_threshold = thresholds_level_row["easy"]
        threshold_values["easy"]["xp_thresholds"].append(easy_threshold)
        threshold_values["easy"]["sum"] += easy_threshold
        
        medium_threshold = thresholds_level_row["medium"]
        threshold_values["medium"]["xp_thresholds"].append(medium_threshold)
        threshold_values["medium"]["sum"] += medium_threshold
        
        hard_threshold = thresholds_level_row["hard"]
        threshold_values["hard"]["xp_thresholds"].append(hard_threshold)
        threshold_values["hard"]["sum"] += hard_threshold
        
        deadly_threshold = thresholds_level_row["deadly"]
        threshold_values["deadly"]["xp_thresholds"].append(deadly_threshold)
        threshold_values["deadly"]["sum"] += deadly_threshold
        
    return threshold_values
        
    
# TODO: define interface for monsters
def get_total_monster_xp(monsters: List[Dict]) -> int:
    sum_monster_xp = 0
    # assume someting like: {name: {..., xp:int,...},...}
    for monster in monsters:
        sum_monster_xp += monster['xp']
    
    return sum_monster_xp


def adjust_xp_multiplier_by_party_size(
    current_multiplier_table_index: float,
    higher_xp_threshold: bool = True
) -> int:
    if higher_xp_threshold:
        closest_multiplier_row = \
            ENCOUNTER_MULTIPLIERS_TABLE[
                current_multiplier_table_index + 1
            ]
    else:
        closest_multiplier_row = \
            ENCOUNTER_MULTIPLIERS_TABLE[
                current_multiplier_table_index - 1
            ]
    return closest_multiplier_row["multiplier"]

    
def get_encounter_multiplier(party_size: int, num_monsters: int) -> float:
    encounter_multiplier_row, table_index = get_elem_in_range_from_json_table(
        ENCOUNTER_MULTIPLIERS_TABLE, 
        num_monsters
    )
    encounter_multiplier = encounter_multiplier_row['multiplier']
    # adjust, according to party size
    # DMG defines default in range of 3-5 party members
    default_party_size_range = range(3, 6)
    
    if party_size not in default_party_size_range:
        # interesting syntax
        if party_size < default_party_size_range[0]:
            adjust_xp_multiplier_by_party_size(
                table_index,
                higher_xp_threshold=True
            )
        elif num_monsters == 1:
            return 0.5
        else:
            adjust_xp_multiplier_by_party_size(
                table_index,
                higher_xp_threshold=False
            )
    
    return encounter_multiplier
    
    
def get_closest_difficulty_threshold(
    xp_thresholds: Dict,
    monster_xp: int
) -> Tuple[str, int]:
    """
    {
        "closest_difficulty": str[easy, medium, hard],
        "monster_xp":
    }
    """
    closest_difficulty = ""
    closest_difference = inf

    for difficulty, threshold_values in xp_thresholds.items():
        xp_thresholds_sum = threshold_values["sum"]
        difference = abs(monster_xp - xp_thresholds_sum)
        
        if difference < closest_difference:
            closest_difference = difference
            closest_difficulty = difficulty

    return closest_difficulty, closest_difference

    
def get_daily_xp_limit_per_character_level(
    character_level: int
) -> int:
    return ADVENTURING_DAY_XP_TABLE[character_level]


def get_adventuring_day_xp_thresholds(
    party_levels: List[int]
) -> int:
    """
    Assuming the party will take at most two short rests between encounters

    Args:
        party_levels (List[int]): list of int values, representing character 
        levels in the party

    Returns:
        int: the upper bound of total xp that is recommended the party to
        receive from defeating monsters
    """
    party_xp_sum = 0
    for character_level in party_levels:
        character_xp_limit = \
            get_daily_xp_limit_per_character_level(character_level)
        party_xp_sum += character_xp_limit
    return party_xp_sum
    

def get_encounter_difficulty_data(
    party_levels: List[int],
    encounter_monsters: List[Dict]
) -> Dict:
    """
    Main driver function to calculate encounter CR
    Based on logic in 5e(2014) DMG p.82
    """
    # TODO: check the calling scope. May make more sense to 
    # already have the xp_thresolds, total_monster_xp calculated 
    # already and just inject them
    xp_thresholds: Dict = determine_party_xp_thresholds(party_levels)
    total_monster_xp = get_total_monster_xp(encounter_monsters)        
    encounter_multiplier = get_encounter_multiplier(
        len(party_levels),
        len(encounter_monsters)
    )
    total_monster_xp *= encounter_multiplier
    
    # place total_monster_xp in xp_thresholds
    closest_difficulty = get_closest_difficulty_threshold(
        xp_thresholds,
        total_monster_xp
    )
    
    # TODO: this shit needs type definiton via dataclasses
    encounter_calculation_data = {
        "encounter_difficulty": closest_difficulty,
        "total_monster_xp": total_monster_xp,
        "encounter_multiplier": encounter_multiplier,
        "xp_thresholds": xp_thresholds
    }
    
    return encounter_calculation_data


# TODO: move this to tests later
if __name__ == "__main__":
    # example from p.83
    character_levels = [3, 3, 3, 2]
    monsters = [
        {
            "name": "Bugbear",
            "xp": 200
        },
        {
            "name": "Hobgoblin",
            "xp": 100
        },
        {
            "name": "Hobgoblin",
            "xp": 100
        },
        {
            "name": "Hobgoblin",
            "xp": 100
        },
    ]
    # expect encounter_difficulty to be "hard"
    calculated_difficulty = get_encounter_difficulty_data(
        character_levels,
        monsters
    )
    print(f'DEBUG: {calculated_difficulty}')