# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  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 json
import os
import tempfile
import time
from typing import Any, Dict, List, Optional, Union

# ── 분리된 모듈 re-import ────────────────────────────────────────────────────
from ._config_infra import (                                     # noqa: F401
    ConfigError, ConfigFileNotFoundError, ConfigPathNotFoundError,
    ConfigLockTimeoutError, ConfigValidationError, ConfigParseError,
    ConfigIntegrityError, ConfigSecurityError, ConfigStaleLockError,
    ConfigMandatoryFieldError, ConfigRecoveryError,
    PathParser, ConfigCache, FileLock, BackupManager,
    YamlParser, JsonParser, PathValidator, SizeValidator,
    ChecksumManager, AuditLogger,
    _IS_WINDOWS, YAML_AVAILABLE,
)

# Optional YAML (GConfig._load_file에서 사용)
if YAML_AVAILABLE:
    import yaml


# ==============================================================================
# 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
        """
        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 Exception:
                        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), lineno=e.lineno)

        # All retries failed
        if last_error:
            if isinstance(last_error, (FileNotFoundError, PermissionError, IOError, OSError)):
                raise ConfigFileNotFoundError(self._filepath)
            lineno = getattr(last_error, 'lineno', 0) or 0
            raise ConfigParseError(self._filepath, str(last_error), lineno=lineno)

    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 _IS_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 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:
                # 친절한 에러 메시지 출력
                from .task_error import config_mandatory_field_error
                config_mandatory_field_error(field, self._filepath)
                raise ConfigMandatoryFieldError(field)


# ==============================================================================
# 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',
]
