# File: cpp_python_debug/core/config_manager.py
# Manages application settings, loading from and saving to a JSON file.

import json
import os
import logging
import appdirs  # Mantenuto per user_data_dir nel default di default_diag_dump_dir
import time
from typing import Dict, Any, Optional, List, Union

# NOTA: frozen_utils non è importato qui perché APP_BASE_PATH verrà passato al costruttore.
# Se non venisse passato, allora AppSettings dovrebbe importare get_app_base_path.

logger = logging.getLogger(__name__)

CONFIG_SUBDIR_NAME = "config"  # Sottocartella per il file di configurazione
LOG_SUBDIR_NAME = "logs"  # Sottocartella per i log e i dump (coerente con __main__.py)
CONFIG_FILE_NAME = "gdb_debug_gui_settings.v2.json"
# APP_NAME_FOR_DIRS e APP_AUTHOR_FOR_DIRS non sono più usati per il percorso del file di config principale.
# Potrebbero ancora servire se si usa appdirs per *altri* percorsi utente specifici.


class AppSettings:
    """
    Manages application settings, including loading from and saving to a JSON file.
    Settings are stored in a structured dictionary.
    The configuration file is now stored relative to the application base path.
    """

    def __init__(self, app_base_path_override: Optional[str] = None):
        """
        Initializes AppSettings.

        Args:
            app_base_path_override: The absolute base path of the application.
                                    If None, an attempt will be made to determine it,
                                    though it's best if this is provided by the caller (e.g., main_window).
        """
        if app_base_path_override and os.path.isdir(app_base_path_override):
            self._app_base_path = os.path.normpath(app_base_path_override)
        else:
            # Fallback se non fornito (meno ideale, GDBGui dovrebbe sempre passarlo)
            try:
                from cpp_python_debug.core.frozen_utils import get_app_base_path

                self._app_base_path = get_app_base_path()
                logger.warning(
                    f"app_base_path_override not provided to AppSettings. Determined base path: {self._app_base_path}"
                )
            except ImportError:  # In caso di problemi con l'import circolare o setup
                self._app_base_path = os.path.abspath(".")  # Fallback a CWD
                logger.error(
                    f"Could not import get_app_base_path. AppSettings using CWD as base: {self._app_base_path}"
                )

        self.config_dir: str = os.path.join(self._app_base_path, CONFIG_SUBDIR_NAME)
        self.config_filepath: str = os.path.join(self.config_dir, CONFIG_FILE_NAME)

        logger.debug(f"AppSettings using app_base_path: {self._app_base_path}")
        logger.debug(f"Configuration file path set to: {self.config_filepath}")

        self._settings: Dict[str, Any] = self._get_default_settings()
        self._ensure_config_dir_exists()
        self._load_settings()

    def _ensure_config_dir_exists(self) -> None:
        """Ensures the application's local configuration directory exists."""
        if not os.path.exists(self.config_dir):
            try:
                os.makedirs(self.config_dir, exist_ok=True)
                logger.info(
                    f"Application local configuration directory created: {self.config_dir}"
                )
            except OSError as e:
                logger.error(
                    f"Could not create application local configuration directory {self.config_dir}: {e}",
                    exc_info=True,
                )

    def _get_default_settings(self) -> Dict[str, Any]:
        """
        Returns a dictionary of default settings.
        Paths for dumps are now relative to the application's log directory.
        """
        # La directory base dei log sarà APP_BASE_PATH/logs
        # Questa viene creata da __main__.py e passata a GDBGui,
        # e GDBGui la usa per il suo log handler.
        # Qui, per i default, costruiamo percorsi relativi a quella struttura.
        # Assumiamo che self._app_base_path sia già impostato quando questo viene chiamato
        # la prima volta per popolare self._settings.

        log_dir_base = os.path.join(self._app_base_path, LOG_SUBDIR_NAME)

        default_diag_dump_dir = os.path.join(log_dir_base, "gdb_dumper_diagnostics")
        default_general_dump_dir = os.path.join(log_dir_base, "profile_dumps")

        # Default per il percorso dello script dumper GDB
        # Idealmente, questo script è parte del pacchetto dell'applicazione.
        # Se self._app_base_path è la directory che contiene il pacchetto 'cpp_python_debug'
        default_gdb_dumper_script_path = os.path.join(
            self._app_base_path, "cpp_python_debug", "core", "gdb_dumper.py"
        )
        # Rendi il percorso più generico o lascialo vuoto se deve essere sempre configurato dall'utente
        # Se lo script è parte del pacchetto, PyInstaller dovrebbe includerlo.
        # Per ora, lascio vuoto per forzare la configurazione utente o un meccanismo di bundling.
        # Se sai che sarà sempre lì, puoi usare il path calcolato.
        # Per maggiore robustezza in caso di compilazione, è meglio se questo path viene
        # determinato a runtime quando l'app è frozen (usando frozen_utils).
        # Ma per i *default* nel file di config, un path relativo o vuoto è ok.
        # Lasciamolo vuoto, l'utente o la logica dell'app (es. in GDBGui) lo popolerà.

        return {
            "general": {
                "gdb_executable_path": "gdb",
                "gdb_dumper_script_path": "",  # Lasciare vuoto, l'utente configura o l'app lo trova
                "last_target_executable_path": "",
                "default_breakpoint": "main",
                "default_variable_to_dump": "",
                "default_program_parameters": "",
                "default_dump_output_dir": default_general_dump_dir,
            },
            "timeouts": {
                "gdb_start": 30,
                "gdb_command": 30,
                "program_run_continue": 120,
                "dump_variable": 60,
                "kill_program": 20,
                "gdb_quit": 10,
            },
            "dumper_options": {
                "max_array_elements": 100,
                "max_recursion_depth": 10,
                "max_string_length": 2048,
                "dump_diagnostic_json_to_file": False,
                "diagnostic_json_output_dir": default_diag_dump_dir,
            },
            "gui": {
                "main_window_geometry": "850x800",
                "config_window_geometry": "700x620",  # Aumentata altezza
                "profile_manager_geometry": "1050x750",
            },
            "profiles": [],
        }

    def _load_settings(self) -> None:
        # (Implementazione come nel messaggio precedente, con gestione migrazione e merge)
        logger.info(f"Attempting to load settings from: {self.config_filepath}")
        if not os.path.isfile(self.config_filepath):
            logger.info(
                "Settings file not found. Using default settings. New file will be created on save."
            )
            self._settings = (
                self._get_default_settings()
            )  # Assicura che i default siano quelli attuali
            return

        try:
            with open(self.config_filepath, "r", encoding="utf-8") as f:
                loaded_settings_from_file = json.load(f)

            if not isinstance(loaded_settings_from_file, dict):
                logger.error(
                    f"Settings file at {self.config_filepath} is not a valid JSON object. Using defaults."
                )
                self._backup_corrupted_settings_file("not_a_dict")
                self._settings = self._get_default_settings()
                return

            if "dumper_options" in loaded_settings_from_file and isinstance(
                loaded_settings_from_file["dumper_options"], dict
            ):
                d_opts = loaded_settings_from_file["dumper_options"]
                if (
                    "dump_raw_gdb_output_to_file" in d_opts
                    and "dump_diagnostic_json_to_file" not in d_opts
                ):
                    d_opts["dump_diagnostic_json_to_file"] = d_opts.pop(
                        "dump_raw_gdb_output_to_file"
                    )
                    logger.info(
                        "Migrated 'dump_raw_gdb_output_to_file' -> 'dump_diagnostic_json_to_file'."
                    )
                if (
                    "raw_gdb_output_dir" in d_opts
                    and "diagnostic_json_output_dir" not in d_opts
                ):
                    d_opts["diagnostic_json_output_dir"] = d_opts.pop(
                        "raw_gdb_output_dir"
                    )
                    logger.info(
                        "Migrated 'raw_gdb_output_dir' -> 'diagnostic_json_output_dir'."
                    )

            current_defaults = self._get_default_settings()
            self._settings = self._recursive_update(
                current_defaults, loaded_settings_from_file
            )
            logger.info("Settings loaded and merged with defaults successfully.")

        except json.JSONDecodeError:
            logger.error(
                f"Error decoding JSON from settings: {self.config_filepath}. Using defaults.",
                exc_info=True,
            )
            self._backup_corrupted_settings_file("json_decode_error")
            self._settings = self._get_default_settings()
        except IOError as e_io:
            logger.error(
                f"IOError reading settings {self.config_filepath}: {e_io}. Using defaults.",
                exc_info=True,
            )
            self._settings = self._get_default_settings()
        except Exception as e_unexpected:
            logger.error(
                f"Unexpected error loading settings {self.config_filepath}: {e_unexpected}. Using defaults.",
                exc_info=True,
            )
            self._settings = self._get_default_settings()

    def _backup_corrupted_settings_file(
        self, corruption_type_suffix: str = "corrupted"
    ) -> None:
        # (Implementazione come nel messaggio precedente)
        if os.path.exists(self.config_filepath):
            try:
                timestamp = time.strftime("%Y%m%d%H%M%S")
                corrupted_backup_path = (
                    f"{self.config_filepath}.{corruption_type_suffix}.{timestamp}"
                )
                counter = 0
                temp_path = corrupted_backup_path
                while os.path.exists(temp_path):
                    counter += 1
                    temp_path = f"{corrupted_backup_path}_{counter}"
                corrupted_backup_path = temp_path
                os.rename(self.config_filepath, corrupted_backup_path)
                logger.info(
                    f"Backed up settings file (reason: {corruption_type_suffix}) to: {corrupted_backup_path}"
                )
            except Exception as backup_e:
                logger.error(
                    f"Could not backup settings file '{self.config_filepath}': {backup_e}",
                    exc_info=True,
                )

    def _recursive_update(
        self, defaults_dict: Dict[str, Any], updates_dict: Dict[str, Any]
    ) -> Dict[str, Any]:
        # (Implementazione come nel messaggio precedente, con gestione specifica per "profiles")
        merged = defaults_dict.copy()
        for key, value_from_updates in updates_dict.items():
            if isinstance(merged.get(key), dict) and isinstance(
                value_from_updates, dict
            ):
                merged[key] = self._recursive_update(merged[key], value_from_updates)
            elif key == "profiles" and isinstance(
                value_from_updates, list
            ):  # Sostituisci la lista dei profili
                merged[key] = value_from_updates
            else:
                merged[key] = value_from_updates
        return merged

    def save_settings(self) -> bool:
        # (Implementazione come nel messaggio precedente)
        logger.info(f"Attempting to save settings to: {self.config_filepath}")
        self._ensure_config_dir_exists()
        try:
            settings_to_save = self._settings.copy()
            with open(self.config_filepath, "w", encoding="utf-8") as f:
                json.dump(settings_to_save, f, indent=4, ensure_ascii=False)
            logger.info("Settings saved successfully.")
            return True
        except Exception as e:
            logger.error(
                f"Error saving settings to {self.config_filepath}: {e}", exc_info=True
            )
            return False

    def get_setting(
        self, category: str, key: str, default_value: Optional[Any] = None
    ) -> Any:
        # (Implementazione come nel messaggio precedente)
        category_data = self._settings.get(category)
        if isinstance(category_data, dict):
            return category_data.get(key, default_value)
        else:
            # Se la categoria non esiste, ma esiste nei default, usa il default della chiave da lì
            default_settings_for_category = self._get_default_settings().get(category)
            if isinstance(default_settings_for_category, dict):
                # logger.warning(f"Settings category '{category}' not found in current settings, using default value for key '{key}'.")
                return default_settings_for_category.get(key, default_value)
            # logger.warning(f"Settings category '{category}' not found. Returning provided default for key '{key}'.")
            return default_value

    def get_category_settings(
        self, category: str, default_if_missing: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        # (Implementazione come nel messaggio precedente)
        category_data = self._settings.get(category)
        if isinstance(category_data, dict):
            return category_data.copy()
        effective_default = (
            default_if_missing if isinstance(default_if_missing, dict) else {}
        )
        # logger.warning(f"Settings category '{category}' not found or not a dict. Returning default.")
        return effective_default.copy()

    def get_profiles(self) -> List[Dict[str, Any]]:
        # (Implementazione come nel messaggio precedente)
        profiles_data = self._settings.get("profiles")
        if not isinstance(profiles_data, list):
            logger.warning(
                f"Profiles data in settings is not a list. Resetting to empty list."
            )
            self._settings["profiles"] = []
            return []
        valid_profiles: List[Dict[str, Any]] = []
        needs_correction = False
        for i, profile_item in enumerate(profiles_data):
            if isinstance(profile_item, dict):
                try:
                    valid_profiles.append(
                        json.loads(json.dumps(profile_item))
                    )  # Deep copy
                except Exception as e_copy:
                    logger.error(
                        f"Could not deep copy profile at index {i}, skipping: {e_copy}"
                    )
                    needs_correction = True
            else:
                logger.warning(
                    f"Item at index {i} in profiles is not a dict, skipping."
                )
                needs_correction = True
        if needs_correction:
            self._settings["profiles"] = valid_profiles
        return valid_profiles

    def set_profiles(self, profiles_list: List[Dict[str, Any]]) -> None:
        # (Implementazione come nel messaggio precedente)
        if not isinstance(profiles_list, list) or not all(
            isinstance(p, dict) for p in profiles_list
        ):
            logger.error("Attempted to set profiles with invalid data. Aborting.")
            return
        try:
            self._settings["profiles"] = json.loads(json.dumps(profiles_list))
        except Exception as e_copy:
            logger.error(f"Could not deep copy new profiles list: {e_copy}")
            return
        logger.info(
            f"Profiles list updated in settings. New count: {len(self._settings['profiles'])}"
        )

    def get_all_settings(self) -> Dict[str, Any]:
        # (Implementazione come nel messaggio precedente)
        try:
            return json.loads(json.dumps(self._settings))
        except Exception as e_copy:
            logger.error(f"Could not deep copy all settings: {e_copy}")
            return self._settings.copy()

    def set_setting(self, category: str, key: str, value: Any) -> None:
        # (Implementazione come nel messaggio precedente)
        if category == "profiles" and not isinstance(value, list):
            logger.error(
                "Error: 'profiles' category must be a list. Use set_profiles()."
            )
            return
        if category not in self._settings or not isinstance(
            self._settings.get(category), dict
        ):
            if category != "profiles":
                self._settings[category] = {}
                logger.info(f"Created new settings category: '{category}'")
        if category != "profiles":
            if key not in self._settings[category]:
                logger.info(f"Adding new key '{key}' to category '{category}'.")
            self._settings[category][key] = value
        elif category == "profiles":
            logger.warning(
                f"Attempt to use set_setting for 'profiles' key '{key}'. Use set_profiles()."
            )

    def update_settings_bulk(self, new_settings_dict: Dict[str, Any]) -> None:
        # (Implementazione come nel messaggio precedente)
        profiles_to_set = new_settings_dict.get("profiles")
        if "profiles" in new_settings_dict:
            updates_without_profiles = {
                k: v for k, v in new_settings_dict.items() if k != "profiles"
            }
            self._settings = self._recursive_update(
                self._settings, updates_without_profiles
            )
            if isinstance(profiles_to_set, list):
                self.set_profiles(profiles_to_set)
            elif profiles_to_set is not None:
                logger.warning("Bulk update 'profiles' not a list. Not updated.")
        else:
            self._settings = self._recursive_update(self._settings, new_settings_dict)
        logger.info("Settings updated in bulk.")
