# [DEF:ConfigManagerModule:Module]
#
# @SEMANTICS: config, manager, persistence, json
# @PURPOSE:   Manages application configuration, including loading/saving to JSON and CRUD for environments.
# @LAYER:     Core
# @RELATION:  DEPENDS_ON -> ConfigModels
# @RELATION:  CALLS -> logger
# @RELATION:  WRITES_TO -> config.json
#
# @INVARIANT: Configuration must always be valid according to AppConfig model.
# @PUBLIC_API: ConfigManager

# [SECTION: IMPORTS]
import json
import os
from pathlib import Path
from typing import Optional, List
from .config_models import AppConfig, Environment, GlobalSettings
from .logger import logger, configure_logger, belief_scope
# [/SECTION]

# [DEF:ConfigManager:Class]
# @PURPOSE: A class to handle application configuration persistence and management.
# @RELATION: WRITES_TO -> config.json
class ConfigManager:
    
    # [DEF:__init__:Function]
    # @PURPOSE: Initializes the ConfigManager.
    # @PRE: isinstance(config_path, str) and len(config_path) > 0
    # @POST: self.config is an instance of AppConfig
    # @PARAM: config_path (str) - Path to the configuration file.
    def __init__(self, config_path: str = "config.json"):
        with belief_scope("__init__"):
            # 1. Runtime check of @PRE
            assert isinstance(config_path, str) and config_path, "config_path must be a non-empty string"
            
            logger.info(f"[ConfigManager][Entry] Initializing with {config_path}")
            
            # 2. Logic implementation
            self.config_path = Path(config_path)
            self.config: AppConfig = self._load_config()

            # Configure logger with loaded settings
            configure_logger(self.config.settings.logging)

            # 3. Runtime check of @POST
            assert isinstance(self.config, AppConfig), "self.config must be an instance of AppConfig"

            logger.info(f"[ConfigManager][Exit] Initialized")
    # [/DEF:__init__:Function]

    # [DEF:_load_config:Function]
    # @PURPOSE: Loads the configuration from disk or creates a default one.
    # @PRE: self.config_path is set.
    # @POST: isinstance(return, AppConfig)
    # @RETURN: AppConfig - The loaded or default configuration.
    def _load_config(self) -> AppConfig:
        with belief_scope("_load_config"):
            logger.debug(f"[_load_config][Entry] Loading from {self.config_path}")
        
        if not self.config_path.exists():
            logger.info(f"[_load_config][Action] Config file not found. Creating default.")
            default_config = AppConfig(
                environments=[],
                settings=GlobalSettings()
            )
            self._save_config_to_disk(default_config)
            return default_config
        try:
            with open(self.config_path, "r") as f:
                data = json.load(f)
                
                # Check for deprecated field
                if "settings" in data and "backup_path" in data["settings"]:
                    del data["settings"]["backup_path"]

                config = AppConfig(**data)
                logger.info(f"[_load_config][Coherence:OK] Configuration loaded")
                return config
        except Exception as e:
            logger.error(f"[_load_config][Coherence:Failed] Error loading config: {e}")
            # Fallback but try to preserve existing settings if possible?
            # For now, return default to be safe, but log the error prominently.
            return AppConfig(
                environments=[],
                settings=GlobalSettings(storage=StorageConfig())
            )
    # [/DEF:_load_config:Function]

    # [DEF:_save_config_to_disk:Function]
    # @PURPOSE: Saves the provided configuration object to disk.
    # @PRE: isinstance(config, AppConfig)
    # @POST: Configuration saved to disk.
    # @PARAM: config (AppConfig) - The configuration to save.
    def _save_config_to_disk(self, config: AppConfig):
        with belief_scope("_save_config_to_disk"):
            logger.debug(f"[_save_config_to_disk][Entry] Saving to {self.config_path}")
        
        # 1. Runtime check of @PRE
        assert isinstance(config, AppConfig), "config must be an instance of AppConfig"
        
        # 2. Logic implementation
        try:
            with open(self.config_path, "w") as f:
                json.dump(config.dict(), f, indent=4)
            logger.info(f"[_save_config_to_disk][Action] Configuration saved")
        except Exception as e:
            logger.error(f"[_save_config_to_disk][Coherence:Failed] Failed to save: {e}")
    # [/DEF:_save_config_to_disk:Function]

    # [DEF:save:Function]
    # @PURPOSE: Saves the current configuration state to disk.
    # @PRE: self.config is set.
    # @POST: self._save_config_to_disk called.
    def save(self):
        with belief_scope("save"):
            self._save_config_to_disk(self.config)
    # [/DEF:save:Function]

    # [DEF:get_config:Function]
    # @PURPOSE: Returns the current configuration.
    # @PRE: self.config is set.
    # @POST: Returns self.config.
    # @RETURN: AppConfig - The current configuration.
    def get_config(self) -> AppConfig:
        with belief_scope("get_config"):
            return self.config
    # [/DEF:get_config:Function]

    # [DEF:update_global_settings:Function]
    # @PURPOSE: Updates the global settings and persists the change.
    # @PRE: isinstance(settings, GlobalSettings)
    # @POST: self.config.settings updated and saved.
    # @PARAM: settings (GlobalSettings) - The new global settings.
    def update_global_settings(self, settings: GlobalSettings):
        with belief_scope("update_global_settings"):
            logger.info(f"[update_global_settings][Entry] Updating settings")
        
        # 1. Runtime check of @PRE
        assert isinstance(settings, GlobalSettings), "settings must be an instance of GlobalSettings"
        
        # 2. Logic implementation
        self.config.settings = settings
        self.save()

        # Reconfigure logger with new settings
        configure_logger(settings.logging)

        logger.info(f"[update_global_settings][Exit] Settings updated")
    # [/DEF:update_global_settings:Function]

    # [DEF:validate_path:Function]
    # @PURPOSE: Validates if a path exists and is writable.
    # @PRE: path is a string.
    # @POST: Returns (bool, str) status.
    # @PARAM: path (str) - The path to validate.
    # @RETURN: tuple (bool, str) - (is_valid, message)
    def validate_path(self, path: str) -> tuple[bool, str]:
        with belief_scope("validate_path"):
            p = os.path.abspath(path)
        if not os.path.exists(p):
            try:
                os.makedirs(p, exist_ok=True)
            except Exception as e:
                return False, f"Path does not exist and could not be created: {e}"
        
        if not os.access(p, os.W_OK):
            return False, "Path is not writable"
            
        return True, "Path is valid and writable"
    # [/DEF:validate_path:Function]

    # [DEF:get_environments:Function]
    # @PURPOSE: Returns the list of configured environments.
    # @PRE: self.config is set.
    # @POST: Returns list of environments.
    # @RETURN: List[Environment] - List of environments.
    def get_environments(self) -> List[Environment]:
        with belief_scope("get_environments"):
            return self.config.environments
    # [/DEF:get_environments:Function]

    # [DEF:has_environments:Function]
    # @PURPOSE: Checks if at least one environment is configured.
    # @PRE: self.config is set.
    # @POST: Returns boolean indicating if environments exist.
    # @RETURN: bool - True if at least one environment exists.
    def has_environments(self) -> bool:
        with belief_scope("has_environments"):
            return len(self.config.environments) > 0
    # [/DEF:has_environments:Function]

    # [DEF:get_environment:Function]
    # @PURPOSE: Returns a single environment by ID.
    # @PRE: self.config is set and isinstance(env_id, str) and len(env_id) > 0.
    # @POST: Returns Environment object if found, None otherwise.
    # @PARAM: env_id (str) - The ID of the environment to retrieve.
    # @RETURN: Optional[Environment] - The environment with the given ID, or None.
    def get_environment(self, env_id: str) -> Optional[Environment]:
        with belief_scope("get_environment"):
            for env in self.config.environments:
                if env.id == env_id:
                    return env
            return None
    # [/DEF:get_environment:Function]

    # [DEF:add_environment:Function]
    # @PURPOSE: Adds a new environment to the configuration.
    # @PRE: isinstance(env, Environment)
    # @POST: Environment added or updated in self.config.environments.
    # @PARAM: env (Environment) - The environment to add.
    def add_environment(self, env: Environment):
        with belief_scope("add_environment"):
            logger.info(f"[add_environment][Entry] Adding environment {env.id}")
        
        # 1. Runtime check of @PRE
        assert isinstance(env, Environment), "env must be an instance of Environment"
        
        # 2. Logic implementation
        # Check for duplicate ID and remove if exists
        self.config.environments = [e for e in self.config.environments if e.id != env.id]
        self.config.environments.append(env)
        self.save()
        
        logger.info(f"[add_environment][Exit] Environment added")
    # [/DEF:add_environment:Function]

    # [DEF:update_environment:Function]
    # @PURPOSE: Updates an existing environment.
    # @PRE: isinstance(env_id, str) and len(env_id) > 0 and isinstance(updated_env, Environment)
    # @POST: Returns True if environment was found and updated.
    # @PARAM: env_id (str) - The ID of the environment to update.
    # @PARAM: updated_env (Environment) - The updated environment data.
    # @RETURN: bool - True if updated, False otherwise.
    def update_environment(self, env_id: str, updated_env: Environment) -> bool:
        with belief_scope("update_environment"):
            logger.info(f"[update_environment][Entry] Updating {env_id}")
        
        # 1. Runtime check of @PRE
        assert env_id and isinstance(env_id, str), "env_id must be a non-empty string"
        assert isinstance(updated_env, Environment), "updated_env must be an instance of Environment"
        
        # 2. Logic implementation
        for i, env in enumerate(self.config.environments):
            if env.id == env_id:
                # If password is masked, keep the old one
                if updated_env.password == "********":
                    updated_env.password = env.password
                
                self.config.environments[i] = updated_env
                self.save()
                logger.info(f"[update_environment][Coherence:OK] Updated {env_id}")
                return True
        
        logger.warning(f"[update_environment][Coherence:Failed] Environment {env_id} not found")
        return False
    # [/DEF:update_environment:Function]

    # [DEF:delete_environment:Function]
    # @PURPOSE: Deletes an environment by ID.
    # @PRE: isinstance(env_id, str) and len(env_id) > 0
    # @POST: Environment removed from self.config.environments if it existed.
    # @PARAM: env_id (str) - The ID of the environment to delete.
    def delete_environment(self, env_id: str):
        with belief_scope("delete_environment"):
            logger.info(f"[delete_environment][Entry] Deleting {env_id}")
        
        # 1. Runtime check of @PRE
        assert env_id and isinstance(env_id, str), "env_id must be a non-empty string"
        
        # 2. Logic implementation
        original_count = len(self.config.environments)
        self.config.environments = [e for e in self.config.environments if e.id != env_id]
        
        if len(self.config.environments) < original_count:
            self.save()
            logger.info(f"[delete_environment][Action] Deleted {env_id}")
        else:
            logger.warning(f"[delete_environment][Coherence:Failed] Environment {env_id} not found")
    # [/DEF:delete_environment:Function]

# [/DEF:ConfigManager:Class]

# [/DEF:ConfigManagerModule:Module]
