from dataclasses import dataclass
import os
from functools import wraps
import inspect
from typing import Optional
from dotenv import dotenv_values
from actions_logging.app_logging import logger
from aws.constants import ENVS, STG_ENVS, PROD_ENVS, STAGING, PRODUCTION, STG_PROD_ENVS
from common.common import compare_dict_keys_and_values
from env_files.constants import DEFAULT_ENV_FILES_PATH, GLOBAL_FILE, REMOVED_KEY, VALUE_CHANGED, NEW_KEY


@dataclass
class KeyChanges:
    sign: str
    current_s3_key: Optional[str]
    new_generated_key: Optional[str]
    change: str

    def __init__(self, current_s3_key: Optional[str], new_generated_key: Optional[str], change: str):
        self.current_s3_key = current_s3_key
        self.new_generated_key = new_generated_key
        self.change = change
        self.sign = self.get_sign_by_change()

    def get_sign_by_change(self):
        if self.change == VALUE_CHANGED:
            return ":warning:"
        elif self.change == REMOVED_KEY:
            return ":x:"
        elif self.change == NEW_KEY:
            return ":green_circle:"
        else:
            return ""


def get_all_env_files_names(env_files_base_path: str) -> list:
    """
    this function will get all the env files names in the env_files_base_path
    :param env_files_base_path:
    :return: list of all the env files names
    """
    try:
        env_files = []
        for root, _, files in os.walk(env_files_base_path):
            for file in files:
                if file.endswith(".json") and not file in [GLOBAL_FILE, 'template.json']:
                    env_files.append(os.path.join(root, file))
        return env_files
    except Exception as e:
        raise RuntimeError(f"An error occurred in get_all_env_files_names as {e}")


def get_svc_env_files_for_env_level(svc_name: str, is_multi_service_repo: bool, env_level: str) -> list:
    """
    this function will get all the env files names for an env level (dev, staging, production)
    :param svc_name: service name
    :param is_multi_service_repo: is multi service repo
    :param env_level: env level - dev staging or production
    :return: list of all the env files names
    """
    try:
        env_files = []
        env_files_base_path = os.path.join(DEFAULT_ENV_FILES_PATH, env_level)
        if is_multi_service_repo:
            env_files_base_path = os.path.join(DEFAULT_ENV_FILES_PATH, svc_name, env_level)

        for root, _, files in os.walk(env_files_base_path):
            for file in files:
                if file.endswith(".json") and not file in [GLOBAL_FILE, 'template.json']:
                    env_files.append(os.path.join(root, file))
        return env_files
    except Exception as e:
        raise RuntimeError(f"An error occurred in get_all_env_files_names as {e}")


def get_sub_dir_for_env_file(env_name):
    logger.info(f"will get sub dir for env file for env: {env_name}")
    if env_name in STG_ENVS:
        return STAGING
    elif env_name in PROD_ENVS:
        return PRODUCTION
    elif env_name in ENVS:
        return "dev"
    else:
        logger.error(f"env: {env_name} not found in any of the envs")


def get_high_env_file_paths(svc_name=''):
    """
    Dynamically generate environment file paths for each environment.
    Uses constants like PROD_ENVS, STG_ENVS, etc., to generate paths for environments.
    In case of single service repo, svc_name will be empty
    """
    # Generate file paths for each environment based on its type (production or staging)
    # env_file_paths = {
    #     env: os.path.join(DEFAULT_ENV_FILES_PATH, svc_name, PRODUCTION if env.startswith(PRODUCTION) else STAGING, f"{env}.json")
    #     for env in STG_PROD_ENVS
    # }
    env_file_paths = {}
    for env in STG_PROD_ENVS:
        sub_dir = get_sub_dir_for_env_file(env)
        env_file_paths[str(os.path.join(DEFAULT_ENV_FILES_PATH, svc_name, sub_dir, f"{env}.json"))] = env
    return env_file_paths


def get_multi_service_repo(svc_type: str):
    if svc_type == 'core-ecs':
        return True
    return os.getenv('IS_MULTI_SERVICE_REPO', 'false') == 'true'


def route_core_ecs(func):
    """
    This decorator will route the function to all core-ecs services if the SVC_TYPE variable == core-ecs
    all core-ecs services are defined by the list of files in DEFAULT_ENV_FILES_PATH folder
    if a function called returns a value, and the svc_type == core-ecs the first call will return, rest will be ignored
    in other cases the called function behavior is unchanged
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        svc_type = os.getenv("SVC_TYPE")
        sig = inspect.signature(func)
        param_names = list(sig.parameters.keys())
        if not svc_type:
            raise ValueError("SVC_TYPE variable is required for this decorator to be able to handle core-ecs logic.")

        if svc_type != "core-ecs":
            return func(*args, **kwargs)

        core_services = os.listdir(DEFAULT_ENV_FILES_PATH)
        result = None
        first_call = True
        for svc in core_services:
            logger.info("handling core-ecs service: " + svc)
            new_args = list(args)
            idx = None
            for i, name in enumerate(param_names):
                if name == "svc_name":
                    idx = i
                    break
            if idx is not None and idx < len(new_args):
                new_args[idx] = svc

            if first_call:
                result = func(*new_args, **kwargs)
                first_call = False
            else:
                func(*new_args, **kwargs)
        return result

    return wrapper


def get_data_from_env_file(file_name: str) -> dict:
    """
    get data from env file
    :param file_name:
    :return: dict of key value of the env file
    """
    try:
        logger.info(f"getting data from env file: {file_name}")
        file_data = dotenv_values(file_name)
        logger.info(f"got data with {len(file_data)} keys from env file: {file_name}")
        return file_data
    except Exception as e:
        raise RuntimeError(f"An error occurred in get_data_from_env_file: {e}")


def log_the_diff_between_2_dicts(base_data: dict, head_data: dict) -> list[KeyChanges]:
    """
    This function will log the diff between 2 dictionaries.
    It will print a table with column names: base_key, head_key, changes.
    In the values of the tables, it will print the key that changed and the change. Change can be one of 2:
    new key or value changed.
    If it's a new key, the row in the table will be filled just with the column of the dict this key exists and the other column will be empty.
    If it's value changed, the row will be filled with the key in both columns of the base and head.
    :param base_data: The base dictionary.
    :param head_data: The head dictionary.
    :return: list of KeyChanges objects for the step summary table
    """
    try:
        logger.info("comparing the 2 env files keys")
        changes = compare_dict_keys_and_values(base_data, head_data)
        changes_list = []
        logger.debug(f"will iterate the {len(changes)} changes and log them")
        logger.info_blue(f"{'current_key':<45} {'new_key':<45} {'change':<45}")
        logger.info_blue("-" * 135)
        for base_key, head_key, change in changes:
            change_instance = KeyChanges(base_key, head_key, change)
            changes_list.append(change_instance)
            if change == VALUE_CHANGED:
                logger.info_red(f"{str(base_key):<45} {str(head_key):<45} {change:<45}")
            elif change == REMOVED_KEY:
                logger.info_yellow(f"{str(base_key):<45} {str(head_key):<45} {change:<45}")
            else:
                logger.info_blue(f"{str(base_key):<45} {str(head_key):<45} {change:<45}")
        return changes_list
    except Exception as e:
        raise RuntimeError(f"An error occurred in log_the_diff_between_2_dicts: {e}")
