import hashlib
import json
import os
import shutil
from typing import List, Tuple, Dict

from dv_launcher.data.constants import Constants
from dv_launcher.services.logging.custom_logger import CustomLogger

logger = CustomLogger()


def check_config_changes(constants: Constants) -> Tuple[
    bool, Dict[str, str]]:
    # Get modification dates
    env_file_modified_time = os.path.getmtime(constants.ENV_FILE)
    dockerfile_file_modified_time = os.path.getmtime(constants.DOCKERFILE_FILE)

    # Read the addons' cache file, if any error occurs, return an empty dict
    cached_config_json = {}
    try:
        with open(constants.CACHE_CONFIG_FILE, "r") as f:
            cached_config_json = json.load(f)
    except Exception as e:
        logger.print_warning(f"Error reading config cache file: {e}. New cache file will be created.")
        # Assign the new values to the JSON config
        cached_config_json['env_file_modified_time'] = env_file_modified_time
        cached_config_json['dockerfile_file_modified_time'] = dockerfile_file_modified_time
        return True, cached_config_json

    # Verify if the values match
    if cached_config_json.get('env_file_modified_time', '') != env_file_modified_time or cached_config_json.get(
            'dockerfile_file_modified_time', '') != dockerfile_file_modified_time:

        # Assign new values to the JSON cache
        cached_config_json['env_file_modified_time'] = env_file_modified_time
        cached_config_json['dockerfile_file_modified_time'] = dockerfile_file_modified_time
        return True, cached_config_json
    else:
        return False, cached_config_json


def replace_cache_file(cached_config_json: Dict[str, str], base_cache_dir: str, config_cache_file: str) -> None:
    """
    Replace the cache file with the new data.
    :param cached_config_json: JSON containing the new data.
    :param config_cache_file: path to the cache file.
    :param base_cache_dir: path to the base cache directory, needed to create the cache directory if it doesn't exist.
    :return:
    """

    # Create the cache directory if it doesn't exist
    if not os.path.exists(base_cache_dir):
        os.makedirs(base_cache_dir)

    # Write the JSON data to the document
    json.dump(cached_config_json, open(config_cache_file, "w"))


def calculate_addon_hash(addon_path: str) -> str:
    """
    Calculate MD5 hash for all files within an addon directory.

    :param addon_path: Path to the addon directory
    :return: Combined MD5 hash of all files in the addon
    """
    file_hashes = {}

    # Walk through all files in the addon directory
    for root, dirs, files in os.walk(addon_path):
        for file in files:
            file_path = os.path.join(root, file)
            relative_path = os.path.relpath(file_path, addon_path)

            try:
                with open(file_path, 'rb') as f:
                    file_hash = hashlib.md5(f.read()).hexdigest()
                    file_hashes[relative_path] = file_hash
            except Exception as e:
                logger.print_warning(f"Error reading file {file_path}: {e}")
                continue

    # Create a combined hash from all file hashes
    if file_hashes:
        combined = ''.join(sorted(file_hashes.values()))
        return hashlib.md5(combined.encode()).hexdigest()
    else:
        # Return empty hash if no files found
        return hashlib.md5(b'').hexdigest()


def list_updated_addons(addons_folder: list[str], addons_cache_file: str) -> Tuple[
    List[str], Dict[str, Dict[str, str]]]:
    """
    Lists updated addons in the provided addons folder. The function checks if
    the given folder exists and scans for directories representing addons.
    It checks the content hash of each addon folder to detect changes.
    :param addons_folder: The path to the folder containing addon directories.
    :type addons_folder: str
    :param addons_cache_file: The path to the file where addon metadata is cached.
    :type addons_cache_file: str
    :return: A tuple containing a list of updated addon names and the updated cache dictionary.
    :rtype: Tuple[List[str], Dict[str, Dict[str, str]]]
    :raises Exception: If the provided addons folder does not exist.
    """
    # Read the addons' cache file, if any error occurs, start with an empty dict
    cached_addons = {}
    try:
        with open(addons_cache_file, "r") as f:
            cached_addons = json.load(f)
    except Exception as e:
        logger.print_warning(f"Error reading addons cache file: {e}. New cache file will be created.")
    if not cached_addons:
        cached_addons = {}

    to_update_list = []
    # Track all addon names seen across all folders to detect removals later
    all_seen_addons = []

    for addon_folder in addons_folder:
        logger.print_status(f"Fetching addons to be updated from folder {addon_folder}")

        # Get the list of valid addon directories (must contain __manifest__.py)
        addon_list = [
            item for item in os.listdir(addon_folder)
            if os.path.isdir(os.path.join(addon_folder, item))
               and not item.startswith('.')
               and "__manifest__.py" in os.listdir(os.path.join(addon_folder, item))
        ]

        # Accumulate addon names across all folders to use in removal check later
        all_seen_addons.extend(addon_list)

        for addon in addon_list:
            addon_path = os.path.join(addon_folder, addon)
            current_hash = calculate_addon_hash(addon_path)

            if addon in cached_addons:
                # Addon exists in cache, compare hashes to detect changes
                cached_hash = cached_addons[addon].get('content_hash', '')
                if cached_hash != current_hash:
                    # Hash changed, addon needs update
                    cached_addons[addon]['content_hash'] = current_hash
                    to_update_list.append(addon)
                    logger.print_status(f"Addon '{addon}' content changed, marked for update.")
            else:
                # New addon not in cache, add it and mark for update
                cached_addons[addon] = {
                    'content_hash': current_hash
                }
                to_update_list.append(addon)
                logger.print_status(f"New addon '{addon}' detected, marked for update.")

    # After processing all folders, remove from cache any addon that no longer
    # exists in any of the scanned folders. This is done outside the loop to
    # avoid incorrectly deleting addons from other folders during iteration.
    for cached_addon in list(cached_addons.keys()):
        if cached_addon not in all_seen_addons:
            del cached_addons[cached_addon]
            logger.print_status(f"Addon '{cached_addon}' no longer exists, removed from cache.")

    if not to_update_list:
        logger.print_success("No addons found to be updated")

    return to_update_list, cached_addons


def update_addons_cache(addons_json, addons_cache_file):
    json.dump(addons_json, open(addons_cache_file, "w"))
