import os
import shutil
import sys
from datetime import datetime
from pathlib import Path
from collections import Counter
import copy

from em_db_access import EmDBAccessLayerStub, EmDBAccessLayer
from tz_objects.gmt_offset import GmtOffset
from tz_objects.time_zone_dat_line import TimeZoneDatLine
from tz_objects.timezone_dat import TimeZoneDat
from tz_objects.timezone_json import TimeZoneJson, TBD_ID_STR
from tz_objects.timezone_json_data import TimeZoneJsonData
from tz_objects.tz_date_time import TzDateTime
from utils.free_codes_calculator import FreeCodesCalculator
from utils.mapping_file_util import MappingFileUtil
from utils.my_agrument_parser import MyArgumentParser

TIME_ZONE_DAT_MANAGED_FOR_TEST = ["test_files", "TimeZoneManagedForTest.txt"]
TIME_ZONE_DAT_FILE_DEBUG = ["test_files", "TimeZoneForTest.dat"]
EM_STANDARD_IANA_TIMEZONE_FOR_TEST = ["test_files", "EM_Standard_Iana_TimeZoneForTest.json"]
TIME_ZONE_JSON_FILE_PATH_DEBUG = ["test_files", "TimeZoneForTest.json"]
CONTROLM_IS_MANAGED_MSG = "The Control-M Environment is Time Zone Managed"
CONTROLM_IS_NOT_MANAGED_MSG = "The Control-M Environment is not Time Zone Managed"
MINIMAL_EM_COMPATIBILITY_VERSION_TO_ACTIVATE = '9.0.22.000'
MINIMAL_CONNECTED_CTM_VERSION_IN_DB = '922'
MINIMAL_CONNECTED_CTM_VERSION_FOR_USER_DISPLAY = '9.0.22.000'
is_develop = False
em_data = None
is_verbose = False

def generate_timezone_mapping_file_name():
    now = datetime.now()

    # Format the timestamp
    timestamp = now.strftime("%y%m%d_%H%M%S")
    mapping_file_name = f"timezone_mapping_{timestamp}.dat"
    return mapping_file_name

generated_timezone_mapping_file_name = generate_timezone_mapping_file_name()

def get_time_zone_managed_for_test():
    base_path = os.path.dirname(os.path.abspath(__file__))
    return os.path.join(base_path, *TIME_ZONE_DAT_MANAGED_FOR_TEST)


def get_time_zone_dat_path(base_path):
    if is_develop:
        base_path = os.path.dirname(os.path.abspath(__file__))
        return os.path.join(base_path, *TIME_ZONE_DAT_FILE_DEBUG)
    return os.path.join(base_path, "TimeZone.dat")


def get_time_zone_json_path(output_directory_name):
    return os.path.join(".", output_directory_name, "TimeZone.json")


def get_standard_iana_tz_path():
    base_path = os.path.dirname(os.path.abspath(__file__))
    return os.path.join(base_path, 'EM_Standard_Iana_TimeZone.json')


def get_time_zone_data(time_zone_data_file):
    time_zones = []
    with open(time_zone_data_file, 'r') as f:
        for line in f:
            tz_info = line.strip()
            if len(tz_info) > 0:
                try:
                    time_zones.append(TimeZoneDatLine.parse_line(tz_info))
                except ValueError:
                    raise Exception(f"Invalid timezone format for line:\n{tz_info}")

    # time_zone_dicts = [x.code for x in time_zones]
    # create dictionary which is mapped by CTM_CODE
    time_zone_dicts = {obj.code: obj for obj in time_zones}

    return time_zone_dicts


# Converting the list to a dictionary mapped by CTMCode


def format_gmt_offset(hours, minutes, sign):
    formatted_string = "GMT{}{:02d}:{:02d}".format(sign, abs(hours), abs(minutes))
    return formatted_string


def format_gmt_offset(hours, minutes, sign):
    formatted_string = "GMT{}{:02d}:{:02d}".format(sign, int(hours), int(minutes))
    return formatted_string


def suggest_IANA_tz_and_choose_by_TZ_GMT(tz: TimeZoneDatLine) -> TimeZoneJsonData:
    timezone_json = TimeZoneJson.from_file(get_standard_iana_tz_path())
    matching_json_timezones = timezone_json.get_timezones_by_gmt(tz.gmt_offset.hour, tz.gmt_offset.minutes,
                                                                 tz.gmt_offset.sign)

    print(
        f"Suggested timezones for [{tz.code}] ({format_gmt_offset(tz.gmt_offset.hour, tz.gmt_offset.minutes, tz.gmt_offset.sign)})")
    counter = 1
    for matching_json_timezone in matching_json_timezones:
        print(f"{counter}. {matching_json_timezone.id}")
        counter += 1

    choice = get_user_choice(len(matching_json_timezones))
    chosen_timezone_data = matching_json_timezones[choice - 1]
    print(f"You chose [{chosen_timezone_data.id}] for [{tz.code}]")

    return chosen_timezone_data

def get_input(text):
    return input(text)

def get_user_choice(max_size):
    while True:
        try:
            # Prompt the user to enter their choice
            choice = int(get_input("Please enter your choice: \n"))
            if choice > max_size or choice < 1:
                print("You must choose one of the followings")
            else:
                return choice
        except ValueError:
            print("Invalid input. Please enter a valid integer.")


def get_time_zone_codes_from_dat_file():
    return list(get_time_zone_data(get_em_time_zone_dat_path()).keys())


def get_time_zone_codes(db_access):
    has_mvs_server = db_access.has_mvs_server()
    if is_verbose:
        print(f"taking ctm codes from  = {'TimeZone.dat file' if has_mvs_server else 'database'}")
    if has_mvs_server:
        # in case of MVS (MF) server we are using TimeZone.dat file and not the database
        # since there is no information about time zones in the database for MVS (MF) jobs
        return get_time_zone_codes_from_dat_file()
    else:
        return db_access.get_time_zones_codes_in_use_by_job()

def inner_map_managed_code_to_IANA_Id(managed_time_zones_codes, em_time_zone_dat_path_getter):
    if len(managed_time_zones_codes) == 0:
        MappingFileUtil.create_empty_file(generated_timezone_mapping_file_name)
        print('No Time Zone jobs exist in the database.')
        print(f'Created mapping file [{generated_timezone_mapping_file_name}].')

        sys.exit(0)

    print(f'found the following managed timezones :{managed_time_zones_codes}')

    # loading TimeZone.dat into dictionary
    em_time_zone_dat_path = em_time_zone_dat_path_getter()
    timezone_dat_dict = get_time_zone_data(em_time_zone_dat_path)

    tz_code_to_iana_id = {}

    # checking if there are invalid timezones
    invalid_time_zones = []
    duplicated_selected_time_zones = []

    for managed_timeZones_code in managed_time_zones_codes:
        if managed_timeZones_code not in timezone_dat_dict:
            invalid_time_zones.append(managed_timeZones_code)

    if len(invalid_time_zones) > 0:
        print(
            f"Error. TimeZone codes {invalid_time_zones} which exist in Jobs not exist in [{em_time_zone_dat_path}]")
        print('Update missing TimeZone code in Job, or make sure TimeZone code appears in TimeZone.dat file.')
        sys.exit(1)

    is_tbd_id_chosen = False

    for managed_timeZones_code in managed_time_zones_codes:
        # suggesting user Time Zones with matching GMT from TimeZone.json and getting user choose
        chosen_tz_data = suggest_IANA_tz_and_choose_by_TZ_GMT(timezone_dat_dict[managed_timeZones_code])

        # saving flag indicating TBD timeZone was chosen
        if chosen_tz_data.id == TBD_ID_STR:
            is_tbd_id_chosen = True

        duplicated_selected_time_zones.append(chosen_tz_data.id)

        # saving mapping of tz_code to TimeZoneJsonData
        tz_code_to_iana_id[managed_timeZones_code] = chosen_tz_data.id

    backup_file(generated_timezone_mapping_file_name)
    # writing the dictionary contains mapping between tz code to  IANA is
    MappingFileUtil.write_dictionary_to_file(generated_timezone_mapping_file_name, tz_code_to_iana_id)

    print(f"\nSuccessfully wrote Mappings to {generated_timezone_mapping_file_name}")

    if is_tbd_id_chosen:
        print(
            '\nBefore running generate please make sure that all timezones currently mapped to TBD ,are mapped to valid timezone')

    return tz_code_to_iana_id


def map_managed_code_to_IANA_Id():
    db_access = get_db_access()
    managed_time_zones_codes = get_time_zone_codes(db_access)
    return inner_map_managed_code_to_IANA_Id(managed_time_zones_codes, get_em_time_zone_dat_path)


def get_em_time_zone_dat_path():
    return get_time_zone_dat_path(em_data)


def get_db_access():
    if is_develop:
        return EmDBAccessLayerStub()
    else:
        return EmDBAccessLayer()

def backup_file(file_path):
    now = datetime.now()

    timestamp = now.strftime("%y%m%d_%H%M%S")

    # Define the filename with the timestamp
    # Split the file path into directory, base filename, and extension
    dir_name, base_name = os.path.split(file_path)
    file_name, ext = os.path.splitext(base_name)

    # Construct the new file path with ".backup" added before the extension
    new_file_name = f"{file_name}_{timestamp}{ext}"
    new_file_path = os.path.join(dir_name, new_file_name)
    if os.path.exists(file_path):
        shutil.copyfile(file_path, new_file_path)


def is_short_version_equals_greater(version, compared_version):
    return version >= compared_version


def is_version_equals_grater(version, compared_version):
    def version_to_tuple(version_str):
        return tuple(map(int, version_str.split('.')))

    # We compare these tuples directly using the > operator, which works because tuples are compared element by element.
    return version_to_tuple(version) >= version_to_tuple(compared_version)


def generate_mapping(mapping_file_path: str, output_directory: str):
    mapping_file_path_calculated = get_mapping_file_path(mapping_file_path)
    if mapping_file_path_calculated is None:
        print(f'Error! mapping file [{mapping_file_path}]  not exist.')
        sys.exit(1)

    mapped_ctm_code_to_iana = MappingFileUtil.file_to_dictionary(mapping_file_path_calculated)

    result_tz_json = inner_generate_mapping(mapped_ctm_code_to_iana,
                                            TimeZoneJson.from_file(get_standard_iana_tz_path()))
    output_directory_name = create_output_directory(output_directory)
    time_zone_json_path = get_time_zone_json_path(output_directory_name)
    result_tz_json.to_file(time_zone_json_path)
    shutil.copyfile(get_em_time_zone_dat_path(), get_local_time_zone_dat_path(output_directory_name))
    print(f"Successfully created {time_zone_json_path}")


def inner_generate_mapping(mapped_ctm_code_to_iana, current_tz_json):
    ctm_code_to_tz_data = current_tz_json.get_timezones_data_dictionary_by_ctm_code()
    iana_id_to_tz_data = current_tz_json.get_timezones_data_dictionary_by_IANA_id()
    ctm_codes_set = set(current_tz_json.get_ctm_codes_set())
    free_codes_calculator = FreeCodesCalculator(ctm_codes_set)

    # checking if there are  mapped iana codes which not exist in JSON file
    validate_mapping(iana_id_to_tz_data, mapped_ctm_code_to_iana)

    for ctm_code, iana_code in mapped_ctm_code_to_iana.items():
        if is_defined_in_iana(current_tz_json, ctm_code, iana_code):
            continue
        has_iana_id = ctm_code in ctm_code_to_tz_data and iana_code != ctm_code_to_tz_data[ctm_code].id
        if has_iana_id:
            # when allowing duplicate ctm codes,then create a copy of the existing tz data with same iana id and add it to the JSON file

            copy_of_item_with_same_iana_id = copy.deepcopy(iana_id_to_tz_data[iana_code])
            copy_of_item_with_same_iana_id.ctm_code = ctm_code
            current_tz_json.add_tz_data(copy_of_item_with_same_iana_id)
            free_ctm_code = free_codes_calculator.get_next_free_code()

            # replacing the code of tz data with ctm_code to free_ctm_code
            using_tz_data = ctm_code_to_tz_data[ctm_code]
            using_tz_data.ctm_code = free_ctm_code
            if is_verbose:
                print(f"Replaced [{using_tz_data.id}] CTM-CODE from [{ctm_code}] to new code[{free_ctm_code}]")

        should_update_items_with_same_data = not is_defined_in_iana(current_tz_json, ctm_code, iana_code)
        if should_update_items_with_same_data:
            replace_ctm_code_of_item_with_same_ctm_code(ctm_code, current_tz_json, free_codes_calculator, iana_code)

            replace_iana_id_of_item_with_same_ctm_code(ctm_code, ctm_code_to_tz_data, iana_code, iana_id_to_tz_data)

        free_codes_calculator.add_used_code(ctm_code)

    return current_tz_json


def replace_iana_id_of_item_with_same_ctm_code(ctm_code, ctm_code_to_tz_data, iana_code, iana_id_to_tz_data):
    tz_data_with_matching_iana_id = iana_id_to_tz_data[iana_code]

    old_ctm_code = tz_data_with_matching_iana_id.ctm_code
    ctm_code_to_tz_data[ctm_code] = ctm_code_to_tz_data.pop(old_ctm_code, None)
    tz_data_with_matching_iana_id.ctm_code = ctm_code
    if is_verbose:
        print(f"Updated [{tz_data_with_matching_iana_id.id}] CTM-CODE to [{ctm_code}] ")


def replace_ctm_code_of_item_with_same_ctm_code(ctm_code, current_tz_json, free_codes_calculator: FreeCodesCalculator,
                                                iana_code):
    tz_data_with_matching_ctm_code = current_tz_json.get_timezones_data_dictionary_by_ctm_code().get(ctm_code, None)
    if tz_data_with_matching_ctm_code is not None:
        tz_data_with_matching_ctm_code.ctm_code = free_codes_calculator.get_next_free_code()
        if is_verbose:
            print(f"Updated [{ctm_code}] IANA ID to [{iana_code}] ")


def validate_mapping(iana_id_to_tz_data, mapped_ctm_code_to_iana):
    wrong_mapping_dictionary = {}
    for ctm_code, iana_code in mapped_ctm_code_to_iana.items():
        if not iana_code in iana_id_to_tz_data:
            wrong_mapping_dictionary[ctm_code] = iana_code

    # if mapping file contain mapping to invalid IANA_IDS like TBD
    if len(wrong_mapping_dictionary) > 0:
        print('Error. The followingTime zone codes  are still not mapped to a valid Iana ID :')
        for ctm_code, iana_code in wrong_mapping_dictionary.items():
            print(f'{ctm_code}={iana_code}')
        print('Please fix the mapping file , or run the utility with -map flag again.')
        sys.exit(1)
    for _, iana_code in mapped_ctm_code_to_iana.items():
        if not iana_code in iana_id_to_tz_data:
            # if mapping file contain IANA ID which is not valid (like TBD activate cannot run
            print(f"Error!!!. IANA ID [{iana_code}] is not valid please update your mapping file.")
            sys.exit(1)


def is_defined_in_iana(current_tz_json, ctm_code, iana_code):
    for tz_data in current_tz_json.data:
        if tz_data.ctm_code == ctm_code and tz_data.id == iana_code:
            return True
    return False

def create_output_directory(output_directory_name):
    if not os.path.exists(output_directory_name):
        os.makedirs(output_directory_name)
    return output_directory_name


def get_local_time_zone_dat_path(output_directory_name):
    output_directory = os.path.join('.', output_directory_name)
    return get_time_zone_dat_path(output_directory)


def translate_tz_json_to_tz_dat(tz_json: TimeZoneJson):
    tz_dat_lines = []
    for tz_json_line in tz_json.data:
        if tz_json_line.dst_from_day is None:
            tz_from = None
        else:
            tz_from = TzDateTime(day=tz_json_line.dst_from_day,
                                 month=tz_json_line.dst_from_month,
                                 hours=tz_json_line.dst_from_hour,
                                 minutes=tz_json_line.dst_to_minute)
        tz_to = None if tz_json_line.dst_to_day is None else TzDateTime(day=tz_json_line.dst_to_day,
                                                                        month=tz_json_line.dst_to_month,
                                                                        hours=tz_json_line.dst_to_hour,
                                                                        minutes=tz_json_line.dst_to_minute)
        dst_offset = None if tz_json_line.dst_gmt_offset_hour is None else GmtOffset(
            sign=tz_json_line.dst_gmt_offset_sign,
            hour=tz_json_line.dst_gmt_offset_hour,
            minute=tz_json_line.dst_gmt_offset_minute)

        tz_dat_line = TimeZoneDatLine(code=tz_json_line.ctm_code,
                                      gmt_offset=GmtOffset(tz_json_line.offset_sign, tz_json_line.offset_hour,
                                                           tz_json_line.offset_minute),
                                      tz_from=tz_from, tz_to=tz_to, dst_offset=dst_offset)
        tz_dat_lines.append(tz_dat_line)
    return TimeZoneDat(tz_dat_lines)


def load_arguments_parser():
    parser = MyArgumentParser()
    # Define the arguments
    # activate parameter is boolean without value
    parser.add_argument('-generate', action='store_true', help="Generate the timezone mapping.")
    # mapping_file parameter contains parameter of file
    parser.add_argument('-mapping_file', type=str,
                        help="Path to the timezone mapping file.(MUST appear after -generate)")

    # map parameter is boolean without value
    parser.add_argument('-map', action='store_true', help="Map the Managed timezones to map")

    # map_and_generate parameter is boolean without value
    parser.add_argument('-map_and_generate', action='store_true', help="Map the Managed timezones to map. And Generate the timezone mapping")

    parser.add_argument('-output', type=str, help="Output folder for generated Timezone Mapping files")

    parser.add_argument('-develop', action='store_true',
                        help='Develop mode works with local files and stub DB Default is false')

    parser.add_argument('-verbose', action='store_true', help='Add detailed information')

    # Parse the arguments
    return parser


def get_is_windows():
    return sys.platform == "win32" or os.name == "nt"


def get_em_data():
    global em_data
    em_home = os.getenv('EM_HOME')
    if em_home is None:
        print("Environment variable EM_HOME not defined. please define EM_HOME and run again ")
        sys.exit(1)
    em_data = em_home

    if get_is_windows():
        data_path_folders = ['Data']
    else:
        data_path_folders = ['etc', 'cli', 'data']

    for folder in data_path_folders:
        em_data = os.path.join(em_data, folder)
    if is_verbose:
        print(f"em_data folder is [{em_data}]")


def get_mapping_file_path(mapping_file_path):
    file_path = Path(mapping_file_path)
    # checking  if file exist in given user place
    if file_path.exists():
        return file_path

    if is_develop:
        return None

    # if not found trying to check maybe user gave only file name and file exist in EM data folder
    joined_path = os.path.join(em_data, mapping_file_path)
    file_path = Path(joined_path)

    if file_path.exists():
        return file_path
    return None


def confirm_action(prompt):
    while True:
        response = get_input(prompt).strip().lower()  # Convert input to lowercase
        if response in ['y', 'n']:
            return response == 'y'  # Return True for 'y', False for 'n'
        else:
            print("Invalid input. Please enter 'Y' or 'N'.")


def main():
    global is_develop
    global is_verbose
    parser = load_arguments_parser()
    args = parser.parse_args()
    is_develop = args.develop
    is_verbose = args.verbose
    if is_develop:
        # copying managed file of 24 timezones to be used by the program
        shutil.copyfile(get_time_zone_managed_for_test(), get_em_time_zone_dat_path())

    output_directory_name = 'output'
    if args.output is not None:
        output_directory_name = args.output

    if not is_develop:
        get_em_data()
    if args.map:
        map_managed_code_to_IANA_Id()

    elif args.map_and_generate:
        map_managed_code_to_IANA_Id()
        generate_mapping(generated_timezone_mapping_file_name, output_directory_name)

    elif args.generate:
        if args.mapping_file is None:
            print(f"Invalid arguments -generate without -mapping_file")
            parser.print_usage()
            sys.exit(1)
        generate_mapping(args.mapping_file, output_directory_name)
    else:
        print(f"Invalid arguments")
        parser.print_usage()
        sys.exit(1)


if __name__ == "__main__":
    main()

    # try:
    #     main()
    # except Exception as ex:
    #     print("\nFailed to convert TimeZone file.")
    #     print(f"Error: {ex}")
    #     sys.exit(1)
