"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.0 - GConfig                                                       ║
║  Global Configuration Management                                             ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.0.1                                                             ║
║  Date    : 2026-02-07                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    멀티프로세스 환경 전역 설정 관리자                                        ║
║    - YAML/JSON 설정 파일 지원                                                ║
║    - mtime 기반 메모리 캐시 갱신                                             ║
║    - 파일 락을 통한 멀티프로세스 동기화                                      ║
║    - 자동 백업/복구, 보안 강화 (경로 검증, 체크섬, 감사 로그)                ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Classes:                                                                    ║
║    GConfig        - 전역 설정 관리자 (Singleton)                             ║
║    ConfigCache    - 메모리 캐시                                              ║
║    FileLock       - 크로스 플랫폼 파일 락                                    ║
║    BackupManager  - 백업 생성/로테이션                                       ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

from __future__ import annotations

import hashlib
import json
import os
import platform
import shutil
import sys
import tempfile
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Union

# Conditional imports for file locking
if platform.system() == 'Windows':
    import msvcrt
else:
    import fcntl

# Optional YAML support
try:
    import yaml
    YAML_AVAILABLE = True
except ImportError:
    YAML_AVAILABLE = False


# ==============================================================================
# Exception Classes
# ==============================================================================

class ConfigError(Exception):
    """Base exception for configuration errors."""

    def __init__(self, message: str, path: Optional[str] = None):
        self.message = message
        self.path = path
        super().__init__(message)


class ConfigFileNotFoundError(ConfigError):
    """Raised when configuration file does not exist."""

    def __init__(self, filepath: str):
        super().__init__(f"Configuration file not found: {filepath}", filepath)


class ConfigPathNotFoundError(ConfigError):
    """Raised when path does not exist (during set operation)."""

    def __init__(self, path: str):
        super().__init__(f"Path not found, cannot create new path: {path}", path)


class ConfigLockTimeoutError(ConfigError):
    """Raised when file lock acquisition times out."""

    def __init__(self, filepath: str, timeout: float):
        self.timeout = timeout
        super().__init__(f"Lock timeout after {timeout}s: {filepath}", filepath)


class ConfigValidationError(ConfigError):
    """Raised when data validation fails."""
    pass


class ConfigParseError(ConfigError):
    """Raised when configuration file parsing fails."""

    def __init__(self, filepath: str, reason: str):
        self.reason = reason
        super().__init__(f"Parse error in {filepath}: {reason}", filepath)


class ConfigIntegrityError(ConfigError):
    """Raised when checksum verification fails."""

    def __init__(self, filepath: str, expected: str, actual: str):
        self.expected = expected
        self.actual = actual
        super().__init__(
            f"Checksum mismatch for {filepath}: expected {expected[:8]}..., got {actual[:8]}...",
            filepath
        )


class ConfigSecurityError(ConfigError):
    """Raised when security violation is detected."""

    PATH_TRAVERSAL = "PATH_TRAVERSAL"
    SYMLINK_ATTACK = "SYMLINK_ATTACK"
    SIZE_EXCEEDED = "SIZE_EXCEEDED"
    INVALID_PATH = "INVALID_PATH"

    def __init__(self, message: str, violation_type: str, path: Optional[str] = None):
        self.violation_type = violation_type
        super().__init__(message, path)


class ConfigStaleLockError(ConfigError):
    """Raised when stale lock is detected."""

    def __init__(self, filepath: str, pid: int):
        self.pid = pid
        super().__init__(f"Stale lock detected for {filepath} (PID: {pid})", filepath)


class ConfigMandatoryFieldError(ConfigError):
    """Raised when mandatory field is missing."""

    APP_INFO_NAME = "app_info.name"
    APP_INFO_VERSION = "app_info.version"
    APP_INFO_ID = "app_info.id"
    TASK_CONFIG = "task_config"

    MANDATORY_FIELDS = [APP_INFO_NAME, APP_INFO_VERSION, APP_INFO_ID, TASK_CONFIG]

    def __init__(self, field: str, reason: str = "Mandatory field missing"):
        self.field = field
        self.reason = reason
        super().__init__(f"{reason}: {field}", field)


class ConfigRecoveryError(ConfigError):
    """Raised when recovery from backup fails."""

    def __init__(self, filepath: str, tried_backups: List[str]):
        self.tried_backups = tried_backups
        super().__init__(
            f"Recovery failed for {filepath}, tried {len(tried_backups)} backups",
            filepath
        )


# ==============================================================================
# PathParser - Static utility class for dot notation path parsing
# ==============================================================================

class PathParser:
    """Static utility class for parsing dot notation paths."""

    @staticmethod
    def parse(path: str) -> List[str]:
        """Parse path string into list of keys."""
        if not path:
            return []
        return path.split('.')

    @staticmethod
    def validate(path: str, max_depth: int = 10) -> bool:
        """Validate path string."""
        if not path:
            return False
        if not isinstance(path, str):
            return False

        parts = path.split('.')
        if len(parts) > max_depth:
            return False

        for part in parts:
            if not part:
                return False
            if part.startswith('_'):
                return False
            if '..' in path:
                return False

        return True

    @staticmethod
    def get_parent(path: str) -> str:
        """Get parent path."""
        parts = path.rsplit('.', 1)
        return parts[0] if len(parts) > 1 else ""

    @staticmethod
    def get_key(path: str) -> str:
        """Get the last key from path."""
        parts = path.rsplit('.', 1)
        return parts[-1] if parts else ""


# ==============================================================================
# ConfigCache - Memory cache management
# ==============================================================================

class ConfigCache:
    """Memory cache for configuration data."""

    __slots__ = ('_data', '_mtime', '_filepath')

    def __init__(self, filepath: str):
        self._data: Dict[str, Any] = {}
        self._mtime: float = 0.0
        self._filepath = filepath

    def get(self, path: str, default: Any = None) -> Any:
        """Get value from cache using dot notation path."""
        keys = PathParser.parse(path)
        return self._get_nested(self._data, keys, default)

    def set(self, path: str, value: Any) -> None:
        """Set value in cache using dot notation path."""
        keys = PathParser.parse(path)
        self._set_nested(self._data, keys, value)

    def is_stale(self) -> bool:
        """Check if cache is stale (file has been modified)."""
        if not os.path.exists(self._filepath):
            return True
        try:
            current_mtime = os.path.getmtime(self._filepath)
            return current_mtime > self._mtime
        except OSError:
            return True

    def refresh(self, data: Dict[str, Any], mtime: float) -> None:
        """Refresh cache with new data."""
        self._data = data
        self._mtime = mtime

    def to_dict(self) -> Dict[str, Any]:
        """Return copy of all cached data."""
        return dict(self._data)

    def _get_nested(self, data: Dict, keys: List[str], default: Any) -> Any:
        """Get value from nested dictionary."""
        current = data
        for key in keys:
            if isinstance(current, dict) and key in current:
                current = current[key]
            else:
                return default
        return current

    def _set_nested(self, data: Dict, keys: List[str], value: Any) -> None:
        """Set value in nested dictionary."""
        current = data
        for key in keys[:-1]:
            if key not in current:
                raise ConfigPathNotFoundError('.'.join(keys))
            if not isinstance(current[key], dict):
                raise ConfigPathNotFoundError('.'.join(keys))
            current = current[key]

        if keys:
            final_key = keys[-1]
            if final_key not in current and len(keys) > 1:
                parent_path = '.'.join(keys[:-1])
                if not isinstance(current, dict):
                    raise ConfigPathNotFoundError(parent_path)
            current[final_key] = value


# ==============================================================================
# FileLock - Process-safe file locking
# ==============================================================================

class FileLock:
    """Cross-platform file locking for multiprocess synchronization."""

    __slots__ = ('_path', '_fd', '_locked', '_timeout', '_retry_interval')

    def __init__(self, filepath: str, timeout: float = 5.0, retry_interval: float = 0.1):
        self._path = filepath + '.lock'
        self._fd: Optional[int] = None
        self._locked = False
        self._timeout = timeout
        self._retry_interval = retry_interval

    def acquire(self, timeout: Optional[float] = None) -> bool:
        """Acquire file lock."""
        if timeout is None:
            timeout = self._timeout

        start_time = time.time()

        while True:
            try:
                if self._try_lock():
                    self._write_lock_info()
                    self._locked = True
                    return True
            except (IOError, OSError):
                pass

            elapsed = time.time() - start_time
            if elapsed >= timeout:
                raise ConfigLockTimeoutError(self._path, timeout)

            time.sleep(self._retry_interval)

        return False

    def release(self) -> None:
        """Release file lock."""
        if not self._locked:
            return

        try:
            if self._fd is not None:
                if platform.system() == 'Windows':
                    msvcrt.locking(self._fd, msvcrt.LK_UNLCK, 1)
                else:
                    fcntl.flock(self._fd, fcntl.LOCK_UN)
                os.close(self._fd)
                self._fd = None

            if os.path.exists(self._path):
                os.remove(self._path)
        except (IOError, OSError):
            pass
        finally:
            self._locked = False

    def _try_lock(self) -> bool:
        """Try to acquire lock once."""
        if self._check_stale_lock():
            self._cleanup_stale_lock()

        self._fd = os.open(self._path, os.O_CREAT | os.O_RDWR)

        try:
            if platform.system() == 'Windows':
                msvcrt.locking(self._fd, msvcrt.LK_NBLCK, 1)
            else:
                fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            return True
        except (IOError, OSError):
            os.close(self._fd)
            self._fd = None
            return False

    def _write_lock_info(self) -> None:
        """Write lock owner information."""
        lock_info = {
            "pid": os.getpid(),
            "timestamp": time.time(),
            "hostname": platform.node()
        }
        try:
            with open(self._path, 'w') as f:
                json.dump(lock_info, f)
        except (IOError, OSError):
            pass

    def _check_stale_lock(self) -> bool:
        """Check if existing lock is stale."""
        if not os.path.exists(self._path):
            return False

        try:
            with open(self._path, 'r') as f:
                lock_info = json.load(f)

            pid = lock_info.get('pid')
            if pid is None:
                return True

            if not self._is_process_alive(pid):
                return True

            timestamp = lock_info.get('timestamp', 0)
            if time.time() - timestamp > 300:
                return True

            return False
        except (IOError, OSError, json.JSONDecodeError):
            return True

    def _cleanup_stale_lock(self) -> None:
        """Remove stale lock file."""
        try:
            if os.path.exists(self._path):
                os.remove(self._path)
        except (IOError, OSError):
            pass

    @staticmethod
    def _is_process_alive(pid: int) -> bool:
        """Check if process is alive."""
        try:
            if platform.system() == 'Windows':
                import ctypes
                kernel32 = ctypes.windll.kernel32
                handle = kernel32.OpenProcess(0x1000, False, pid)
                if handle:
                    kernel32.CloseHandle(handle)
                    return True
                return False
            else:
                os.kill(pid, 0)
                return True
        except (OSError, AttributeError):
            return False

    def __enter__(self) -> 'FileLock':
        self.acquire()
        return self

    def __exit__(self, *args) -> None:
        self.release()


# ==============================================================================
# BackupManager - Backup creation and rotation
# ==============================================================================

class BackupManager:
    """Manages backup creation and rotation."""

    __slots__ = ('_filepath', '_max_count')

    def __init__(self, filepath: str, max_count: int = 5):
        self._filepath = filepath
        self._max_count = max_count

    def create_backup(self) -> Optional[str]:
        """Create a new backup file."""
        if not os.path.exists(self._filepath):
            return None

        self._rotate()

        backup_path = f"{self._filepath}.bak.1"
        try:
            shutil.copy2(self._filepath, backup_path)
            return backup_path
        except (IOError, OSError):
            return None

    def _rotate(self) -> None:
        """Rotate existing backup files."""
        for i in range(self._max_count, 0, -1):
            old_path = f"{self._filepath}.bak.{i}"
            new_path = f"{self._filepath}.bak.{i + 1}"

            if os.path.exists(old_path):
                if i >= self._max_count:
                    os.remove(old_path)
                else:
                    shutil.move(old_path, new_path)

    def get_backups(self) -> List[str]:
        """Get list of backup files in order."""
        backups = []
        for i in range(1, self._max_count + 1):
            backup_path = f"{self._filepath}.bak.{i}"
            if os.path.exists(backup_path):
                backups.append(backup_path)
        return backups

    def restore(self, number: int = 1) -> bool:
        """Restore from backup number."""
        backup_path = f"{self._filepath}.bak.{number}"
        if not os.path.exists(backup_path):
            return False

        try:
            shutil.copy2(backup_path, self._filepath)
            return True
        except (IOError, OSError):
            return False

    def cleanup_old_backups(self) -> None:
        """Remove backups exceeding max_count."""
        i = self._max_count + 1
        while True:
            backup_path = f"{self._filepath}.bak.{i}"
            if os.path.exists(backup_path):
                os.remove(backup_path)
                i += 1
            else:
                break


# ==============================================================================
# Parsers - YAML and JSON parsing
# ==============================================================================

class YamlParser:
    """Static class for YAML parsing."""

    @staticmethod
    def parse(content: str) -> Dict[str, Any]:
        """Parse YAML content."""
        if not YAML_AVAILABLE:
            raise ConfigParseError("", "PyYAML is not installed")
        return yaml.safe_load(content) or {}

    @staticmethod
    def dump(data: Dict[str, Any]) -> str:
        """Dump data to YAML string."""
        if not YAML_AVAILABLE:
            raise ConfigParseError("", "PyYAML is not installed")
        return yaml.dump(data, default_flow_style=False, allow_unicode=True)


class JsonParser:
    """Static class for JSON parsing."""

    @staticmethod
    def parse(content: str) -> Dict[str, Any]:
        """Parse JSON content."""
        return json.loads(content)

    @staticmethod
    def dump(data: Dict[str, Any]) -> str:
        """Dump data to JSON string."""
        return json.dumps(data, indent=2, ensure_ascii=False)


# ==============================================================================
# Security Classes (Hardening)
# ==============================================================================

class PathValidator:
    """Validates file paths for security."""

    DANGEROUS_PATTERNS = ['..', '~', '$', '%', '|', '>', '<', '`']

    @staticmethod
    def validate(filepath: str, base_dir: Optional[str] = None) -> bool:
        """Validate file path for security issues."""
        if not filepath:
            return False

        # Ensure filepath is string
        filepath_str = str(filepath)

        for pattern in PathValidator.DANGEROUS_PATTERNS:
            if pattern in filepath_str:
                raise ConfigSecurityError(
                    f"Path contains dangerous pattern: {pattern}",
                    ConfigSecurityError.PATH_TRAVERSAL,
                    filepath
                )

        path = Path(filepath)
        if path.is_symlink():
            raise ConfigSecurityError(
                "Symbolic links are not allowed",
                ConfigSecurityError.SYMLINK_ATTACK,
                filepath
            )

        if base_dir:
            try:
                resolved = path.resolve()
                base_resolved = Path(base_dir).resolve()
                if not str(resolved).startswith(str(base_resolved)):
                    raise ConfigSecurityError(
                        "Path traversal detected",
                        ConfigSecurityError.PATH_TRAVERSAL,
                        filepath
                    )
            except (OSError, ValueError):
                return False

        return True


class SizeValidator:
    """Validates sizes for security limits."""

    DEFAULT_MAX_VALUE_SIZE = 1048576
    DEFAULT_MAX_FILE_SIZE = 10485760

    @staticmethod
    def validate_value(value: Any, max_size: int = DEFAULT_MAX_VALUE_SIZE) -> bool:
        """Validate value size."""
        try:
            serialized = json.dumps(value, ensure_ascii=False)
            if len(serialized.encode('utf-8')) > max_size:
                raise ConfigSecurityError(
                    f"Value size exceeds limit: {max_size} bytes",
                    ConfigSecurityError.SIZE_EXCEEDED
                )
            return True
        except (TypeError, ValueError):
            return True

    @staticmethod
    def validate_file(filepath: str, max_size: int = DEFAULT_MAX_FILE_SIZE) -> bool:
        """Validate file size."""
        if not os.path.exists(filepath):
            return True

        size = os.path.getsize(filepath)
        if size > max_size:
            raise ConfigSecurityError(
                f"File size {size} exceeds limit: {max_size} bytes",
                ConfigSecurityError.SIZE_EXCEEDED,
                filepath
            )
        return True


class ChecksumManager:
    """Manages file checksums for integrity verification."""

    __slots__ = ('_filepath', '_algorithm')

    def __init__(self, filepath: str, algorithm: str = 'sha256'):
        self._filepath = filepath
        self._algorithm = algorithm

    def calculate(self) -> str:
        """Calculate file checksum."""
        if not os.path.exists(self._filepath):
            return ""

        hasher = hashlib.new(self._algorithm)
        with open(self._filepath, 'rb') as f:
            for chunk in iter(lambda: f.read(8192), b''):
                hasher.update(chunk)
        return hasher.hexdigest()

    def verify(self) -> bool:
        """Verify file checksum against stored value."""
        stored = self.load()
        if not stored:
            return True

        current = self.calculate()
        if current != stored:
            raise ConfigIntegrityError(self._filepath, stored, current)
        return True

    def save(self) -> None:
        """Save current checksum."""
        checksum = self.calculate()
        checksum_file = self._filepath + '.checksum'

        data = {
            "algorithm": self._algorithm,
            "checksum": checksum,
            "timestamp": time.time(),
            "size": os.path.getsize(self._filepath) if os.path.exists(self._filepath) else 0
        }

        with open(checksum_file, 'w') as f:
            json.dump(data, f)

    def load(self) -> Optional[str]:
        """Load stored checksum."""
        checksum_file = self._filepath + '.checksum'
        if not os.path.exists(checksum_file):
            return None

        try:
            with open(checksum_file, 'r') as f:
                data = json.load(f)
            return data.get('checksum')
        except (IOError, json.JSONDecodeError):
            return None


class AuditLogger:
    """Logs configuration changes for auditing."""

    __slots__ = ('_filepath', '_mask_sensitive')

    SENSITIVE_KEYWORDS = ['password', 'secret', 'key', 'token', 'credential', 'auth']

    def __init__(self, filepath: str, mask_sensitive: bool = True):
        self._filepath = filepath
        self._mask_sensitive = mask_sensitive

    def log(self, action: str, path: str, old_value: Any = None, new_value: Any = None) -> None:
        """Log an audit entry."""
        log_file = self._filepath + '.audit.log'

        if self._mask_sensitive:
            old_value = self._mask_value(path, old_value)
            new_value = self._mask_value(path, new_value)

        entry = {
            "timestamp": datetime.now().isoformat(),
            "action": action,
            "path": path,
            "old_value": str(old_value) if old_value is not None else None,
            "new_value": str(new_value) if new_value is not None else None,
            "pid": os.getpid()
        }

        try:
            with open(log_file, 'a') as f:
                f.write(json.dumps(entry, ensure_ascii=False) + '\n')
        except (IOError, OSError):
            pass

    def _mask_value(self, path: str, value: Any) -> Any:
        """Mask sensitive values."""
        if value is None:
            return None

        path_lower = path.lower()
        for keyword in self.SENSITIVE_KEYWORDS:
            if keyword in path_lower:
                value_str = str(value)
                if len(value_str) <= 4:
                    return "****"
                return value_str[:2] + "****" + value_str[-2:]

        return value


# ==============================================================================
# GConfig - Main configuration manager (Singleton)
# ==============================================================================

class GConfig:
    """Global configuration manager with multiprocess support."""

    __slots__ = (
        '_filepath', '_cache', '_lock', '_backup', '_encoding',
        '_auto_refresh', '_lock_timeout', '_backup_count',
        '_max_path_depth', '_max_value_size', '_max_file_size',
        '_enable_checksum', '_enable_audit_log', '_mask_sensitive',
        '_checksum_manager', '_audit_logger'
    )

    _instance: Optional['GConfig'] = None

    def __new__(cls) -> 'GConfig':
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._init_defaults()
        return cls._instance

    def _init_defaults(self) -> None:
        """Initialize default values."""
        self._filepath: Optional[str] = None
        self._cache: Optional[ConfigCache] = None
        self._lock: Optional[FileLock] = None
        self._backup: Optional[BackupManager] = None
        self._encoding: str = 'utf-8'
        self._auto_refresh: bool = True
        self._lock_timeout: float = 5.0
        self._backup_count: int = 5
        self._max_path_depth: int = 10
        self._max_value_size: int = 1048576
        self._max_file_size: int = 10485760
        self._enable_checksum: bool = False
        self._enable_audit_log: bool = False
        self._mask_sensitive: bool = True
        self._checksum_manager: Optional[ChecksumManager] = None
        self._audit_logger: Optional[AuditLogger] = None

    def configure(self, **options) -> None:
        """Configure GConfig options."""
        if 'lock_timeout' in options:
            self._lock_timeout = float(options['lock_timeout'])
        if 'backup_count' in options:
            self._backup_count = int(options['backup_count'])
        if 'auto_refresh' in options:
            self._auto_refresh = bool(options['auto_refresh'])
        if 'max_path_depth' in options:
            self._max_path_depth = int(options['max_path_depth'])
        if 'max_value_size' in options:
            self._max_value_size = int(options['max_value_size'])
        if 'max_file_size' in options:
            self._max_file_size = int(options['max_file_size'])
        if 'enable_checksum' in options:
            self._enable_checksum = bool(options['enable_checksum'])
        if 'enable_audit_log' in options:
            self._enable_audit_log = bool(options['enable_audit_log'])
        if 'mask_sensitive' in options:
            self._mask_sensitive = bool(options['mask_sensitive'])
        if 'lock_retry_interval' in options and self._lock:
            self._lock._retry_interval = float(options['lock_retry_interval'])

        if self._backup:
            self._backup._max_count = self._backup_count

    def load(self, filepath: str, encoding: str = 'utf-8') -> 'GConfig':
        """Load configuration file.

        Returns:
            self for method chaining (e.g., gconfig.load("config.yaml").dump())
        """
        if not os.path.exists(filepath):
            raise ConfigFileNotFoundError(filepath)

        PathValidator.validate(filepath)
        SizeValidator.validate_file(filepath, self._max_file_size)

        self._filepath = os.path.abspath(filepath)
        self._encoding = encoding

        self._cache = ConfigCache(self._filepath)
        self._lock = FileLock(self._filepath, self._lock_timeout)
        self._backup = BackupManager(self._filepath, self._backup_count)

        if self._enable_checksum:
            self._checksum_manager = ChecksumManager(self._filepath)

        if self._enable_audit_log:
            self._audit_logger = AuditLogger(self._filepath, self._mask_sensitive)

        self._load_file()

        self.validate_mandatory_fields()

        if self._enable_audit_log and self._audit_logger:
            self._audit_logger.log("LOAD", self._filepath)

        return self

    def _load_file(self, max_retries: int = 5, retry_delay: float = 0.1) -> None:
        """Load and parse configuration file with retry logic for multiprocess safety."""
        last_error = None

        for attempt in range(max_retries):
            try:
                # Check if file exists (may be temporarily missing during atomic write)
                if not os.path.exists(self._filepath):
                    if attempt < max_retries - 1:
                        time.sleep(retry_delay)
                        continue
                    raise ConfigFileNotFoundError(self._filepath)

                with open(self._filepath, 'r', encoding=self._encoding) as f:
                    content = f.read()

                # Empty content may indicate file is being rewritten
                if not content.strip():
                    if attempt < max_retries - 1:
                        time.sleep(retry_delay)
                        continue
                    raise ConfigParseError(self._filepath, "Empty configuration file")

                if self._filepath.endswith('.yaml') or self._filepath.endswith('.yml'):
                    data = YamlParser.parse(content)
                elif self._filepath.endswith('.json'):
                    data = JsonParser.parse(content)
                else:
                    try:
                        data = YamlParser.parse(content)
                    except:
                        data = JsonParser.parse(content)

                mtime = os.path.getmtime(self._filepath)
                self._cache.refresh(data, mtime)

                if self._enable_checksum and self._checksum_manager:
                    self._checksum_manager.verify()

                return  # Success

            except (FileNotFoundError, PermissionError, IOError, OSError) as e:
                last_error = e
                if attempt < max_retries - 1:
                    time.sleep(retry_delay)
                    continue
            except (yaml.YAMLError if YAML_AVAILABLE else Exception) as e:
                last_error = e
                if attempt < max_retries - 1:
                    time.sleep(retry_delay)
                    continue
                raise ConfigParseError(self._filepath, str(e))
            except json.JSONDecodeError as e:
                last_error = e
                if attempt < max_retries - 1:
                    time.sleep(retry_delay)
                    continue
                raise ConfigParseError(self._filepath, str(e))

        # All retries failed
        if last_error:
            if isinstance(last_error, (FileNotFoundError, PermissionError, IOError, OSError)):
                raise ConfigFileNotFoundError(self._filepath)
            raise ConfigParseError(self._filepath, str(last_error))

    def data_get(self, path: str, default: Any = None) -> Any:
        """Get configuration value using dot notation path."""
        if self._cache is None:
            return default

        if self._auto_refresh and self._cache.is_stale():
            try:
                self._load_file()
            except (ConfigFileNotFoundError, ConfigParseError):
                # If refresh fails, use cached data (better than failing)
                # This can happen during concurrent writes
                pass

        return self._cache.get(path, default)

    def data_set(self, path: str, value: Any, max_retries: int = 3) -> None:
        """Set configuration value using dot notation path."""
        if self._cache is None:
            raise ConfigError("No configuration loaded")

        if not PathParser.validate(path, self._max_path_depth):
            raise ConfigSecurityError(
                f"Invalid path: {path}",
                ConfigSecurityError.INVALID_PATH,
                path
            )

        SizeValidator.validate_value(value, self._max_value_size)

        old_value = self._cache.get(path) if self._enable_audit_log else None

        last_error = None
        for attempt in range(max_retries):
            try:
                # Reload before setting to get latest data
                if self._auto_refresh and self._cache.is_stale():
                    try:
                        self._load_file()
                    except (ConfigFileNotFoundError, ConfigParseError):
                        # May happen during concurrent write, continue with cached data
                        pass

                self._cache.set(path, value)
                self._save_file(path, value)

                if self._enable_audit_log and self._audit_logger:
                    self._audit_logger.log("SET", path, old_value, value)

                return  # Success

            except ConfigLockTimeoutError:
                raise  # Don't retry lock timeouts
            except (IOError, OSError, PermissionError) as e:
                last_error = e
                if attempt < max_retries - 1:
                    time.sleep(0.1)
                    continue
                raise

        if last_error:
            raise last_error

    def _save_file(self, set_path: str = None, set_value: Any = None) -> None:
        """Save configuration to file with locking and backup."""
        with self._lock:
            # Reload file to get latest data, then re-apply our change
            try:
                self._load_file()
                # Re-apply the value we're setting (it was overwritten by reload)
                if set_path is not None:
                    self._cache.set(set_path, set_value)
            except (ConfigFileNotFoundError, ConfigParseError):
                pass

            self._backup.create_backup()

            data = self._cache.to_dict()

            if self._filepath.endswith('.yaml') or self._filepath.endswith('.yml'):
                content = YamlParser.dump(data)
            else:
                content = JsonParser.dump(data)

            dir_path = os.path.dirname(self._filepath)
            fd, temp_path = tempfile.mkstemp(dir=dir_path, suffix='.tmp')
            try:
                with os.fdopen(fd, 'w', encoding=self._encoding) as f:
                    f.write(content)
                    f.flush()
                    os.fsync(f.fileno())

                if platform.system() == 'Windows':
                    # Windows atomic replace with retry
                    max_attempts = 5
                    for attempt in range(max_attempts):
                        try:
                            if os.path.exists(self._filepath):
                                os.replace(temp_path, self._filepath)
                            else:
                                os.rename(temp_path, self._filepath)
                            break
                        except PermissionError:
                            if attempt < max_attempts - 1:
                                time.sleep(0.05)
                            else:
                                raise
                else:
                    os.rename(temp_path, self._filepath)

                mtime = os.path.getmtime(self._filepath)
                self._cache._mtime = mtime

                if self._enable_checksum and self._checksum_manager:
                    self._checksum_manager.save()

            except Exception:
                if os.path.exists(temp_path):
                    try:
                        os.remove(temp_path)
                    except OSError:
                        pass
                raise

    def save(self) -> None:
        """Explicitly save current configuration."""
        if self._cache is None:
            raise ConfigError("No configuration loaded")

        self._save_file(None, None)

        if self._enable_audit_log and self._audit_logger:
            self._audit_logger.log("SAVE", self._filepath)

    def dump(self, use_ascii: bool = None) -> str:
        """Display configuration as tree structure using box characters.

        Args:
            use_ascii: Use ASCII characters instead of Unicode.
                       None = auto-detect (default), True = ASCII, False = Unicode

        Returns:
            Tree-formatted string of configuration

        Example:
            gconfig.load("config.yaml").dump()
            # Output (Unicode):
            # ╔════════════════════════════════════════════════════════════╗
            # ║ config.yaml                                                ║
            # ╚════════════════════════════════════════════════════════════╝
            # ├─ app_info
            # │  ├─ name: "ALASKA"
            # │  ├─ version: "2.0.0"
            # │  └─ id: "alaska_01"
            # └─ task_config
            #    └─ worker1
            #       ├─ timeout: 5.0
            #       └─ retry: 3
        """
        if self._cache is None:
            raise ConfigError("No configuration loaded")

        data = self._cache.to_dict()
        lines = []

        # Header with filename
        filename = os.path.basename(self._filepath) if self._filepath else "config"

        # Auto-detect if we need ASCII (Windows console often can't handle Unicode)
        if use_ascii is None:
            try:
                encoding = getattr(sys.stdout, 'encoding', None) or 'utf-8'
                "╔═╗║╚╝├─└│".encode(encoding)
                use_ascii = False
            except (UnicodeEncodeError, LookupError):
                use_ascii = True

        if use_ascii:
            # ASCII box drawing
            lines.append(f"+{'-' * 60}+")
            lines.append(f"| {filename:<58} |")
            lines.append(f"+{'-' * 60}+")
            branch_mid = "+--"
            branch_last = "`--"
            vertical = "|  "
            space = "   "
        else:
            # Unicode box drawing (전각문자)
            lines.append(f"╔{'═' * 60}╗")
            lines.append(f"║ {filename:<58} ║")
            lines.append(f"╚{'═' * 60}╝")
            branch_mid = "├─"
            branch_last = "└─"
            vertical = "│  "
            space = "   "

        def _format_value(value: Any) -> str:
            """Format value for display."""
            if isinstance(value, str):
                return f'"{value}"'
            elif value is None:
                return "null"
            elif isinstance(value, bool):
                return "true" if value else "false"
            else:
                return str(value)

        def _build_tree(obj: Any, prefix: str = "", is_last: bool = True, key: str = "") -> None:
            """Recursively build tree lines."""
            branch = branch_last if is_last else branch_mid
            extension = space if is_last else vertical

            if isinstance(obj, dict):
                if key:
                    lines.append(f"{prefix}{branch} {key}")
                    new_prefix = prefix + extension
                else:
                    new_prefix = prefix

                items = list(obj.items())
                for i, (k, v) in enumerate(items):
                    is_last_item = (i == len(items) - 1)
                    _build_tree(v, new_prefix, is_last_item, k)

            elif isinstance(obj, list):
                if key:
                    lines.append(f"{prefix}{branch} {key}")
                    new_prefix = prefix + extension
                else:
                    new_prefix = prefix

                for i, item in enumerate(obj):
                    is_last_item = (i == len(obj) - 1)
                    _build_tree(item, new_prefix, is_last_item, f"[{i}]")

            else:
                # Leaf node
                formatted_value = _format_value(obj)
                lines.append(f"{prefix}{branch} {key}: {formatted_value}")

        # Build tree from root
        if data:
            items = list(data.items())
            for i, (k, v) in enumerate(items):
                is_last_item = (i == len(items) - 1)
                _build_tree(v, "", is_last_item, k)

        result = "\n".join(lines)
        print(result)
        return result

    def refresh(self) -> None:
        """Force refresh cache from file."""
        if self._filepath is None:
            raise ConfigError("No configuration loaded")

        self._load_file()

    def validate_mandatory_fields(self) -> None:
        """Validate that all mandatory fields exist."""
        if self._cache is None:
            raise ConfigError("No configuration loaded")

        for field in ConfigMandatoryFieldError.MANDATORY_FIELDS:
            value = self._cache.get(field)
            if value is None:
                raise ConfigMandatoryFieldError(field)

    def recover_from_backup(self) -> bool:
        """Attempt to recover configuration from backup files."""
        if self._backup is None:
            return False

        backups = self._backup.get_backups()
        if not backups:
            raise ConfigRecoveryError(self._filepath or "", [])

        tried_backups = []
        for backup_path in backups:
            tried_backups.append(backup_path)
            try:
                temp_cache = ConfigCache(backup_path)

                with open(backup_path, 'r', encoding=self._encoding) as f:
                    content = f.read()

                if backup_path.endswith('.yaml') or backup_path.endswith('.yml') or \
                   self._filepath.endswith('.yaml') or self._filepath.endswith('.yml'):
                    data = YamlParser.parse(content)
                else:
                    data = JsonParser.parse(content)

                temp_cache.refresh(data, os.path.getmtime(backup_path))

                for field in ConfigMandatoryFieldError.MANDATORY_FIELDS:
                    if temp_cache.get(field) is None:
                        raise ConfigMandatoryFieldError(field)

                shutil.copy2(backup_path, self._filepath)
                self._load_file()

                if self._enable_audit_log and self._audit_logger:
                    self._audit_logger.log("RECOVER", backup_path)

                return True

            except (ConfigError, IOError, OSError):
                continue

        raise ConfigRecoveryError(self._filepath or "", tried_backups)


# ==============================================================================
# Global instance
# ==============================================================================

gconfig = GConfig()


# ==============================================================================
# Module exports
# ==============================================================================

__all__ = [
    'gconfig',
    'GConfig',
    'ConfigError',
    'ConfigFileNotFoundError',
    'ConfigPathNotFoundError',
    'ConfigLockTimeoutError',
    'ConfigValidationError',
    'ConfigParseError',
    'ConfigIntegrityError',
    'ConfigSecurityError',
    'ConfigStaleLockError',
    'ConfigMandatoryFieldError',
    'ConfigRecoveryError',
    'PathParser',
    'ConfigCache',
    'FileLock',
    'BackupManager',
    'YamlParser',
    'JsonParser',
    'PathValidator',
    'SizeValidator',
    'ChecksumManager',
    'AuditLogger',
]
