system/lib/config.py
"""
Platform Configuration Loader

Loads configuration from multiple .env files in order:
1. config/.env.platform    - Global platform settings
2. projects/{name}/.env    - Project-specific settings
3. .env.local              - Local overrides (gitignored)

Usage:
    from system.lib.config import Config

    # Load platform config only
    cfg = Config()
    print(cfg.NOCODB_URL)

    # Load with project config
    cfg = Config(project="pirotehnika")
    print(cfg.OZON_O1_API_KEY)

    # Get OZON accounts as dict
    accounts = cfg.get_ozon_accounts()

    # Get path with DATASPACE
    path = cfg.data_path("prices/import")

Version: 1.1.0
Date: 2025-12-16
"""

import os
from pathlib import Path
from typing import Optional, Dict, Any
from dataclasses import dataclass, field

# Markers that identify workspace root (must be unique to root, not subdirs)
WORKSPACE_MARKERS = ['config/.env.platform', 'system/lib/config.py', 'architect/']


def find_workspace_root(start_path: Path = None) -> Optional[Path]:
    """
    Find workspace root by walking up from start_path.
    Looks for marker files/directories.

    Returns None if not found (will use env var or default).
    """
    if start_path is None:
        start_path = Path(__file__).resolve()

    current = start_path if start_path.is_dir() else start_path.parent

    # Walk up max 10 levels
    for _ in range(10):
        for marker in WORKSPACE_MARKERS:
            if (current / marker).exists():
                return current

        parent = current.parent
        if parent == current:  # reached root
            break
        current = parent

    return None


def get_workspace() -> str:
    """Get workspace path from env, auto-detection, or default"""
    # 1. Environment variable (highest priority)
    if os.environ.get('WORKSPACE'):
        return os.environ['WORKSPACE']

    # 2. Auto-detect from this file's location
    detected = find_workspace_root()
    if detected:
        return str(detected)

    # 3. Default fallback
    return '/opt/claude-workspace'


def get_dataspace() -> str:
    """Get dataspace path from env or derive from workspace"""
    if os.environ.get('DATASPACE'):
        return os.environ['DATASPACE']

    # Derive: /opt/claude-workspace -> /opt/claude-dataspace
    ws = get_workspace()
    if 'workspace' in ws:
        return ws.replace('workspace', 'dataspace')

    return '/opt/claude-dataspace'


def _load_env_file(filepath: Path) -> Dict[str, str]:
    """Load .env file into dict"""
    result = {}
    if not filepath.exists():
        return result

    with open(filepath, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            # Skip comments and empty lines
            if not line or line.startswith('#'):
                continue
            # Parse KEY=VALUE
            if '=' in line:
                key, _, value = line.partition('=')
                key = key.strip()
                value = value.strip()
                # Remove quotes if present
                if value and value[0] in '"\'':
                    value = value[1:-1] if len(value) > 1 and value[-1] == value[0] else value[1:]
                result[key] = value

    return result


@dataclass
class Config:
    """Platform configuration with layered loading"""

    project: Optional[str] = None
    _data: Dict[str, str] = field(default_factory=dict, repr=False)

    def __post_init__(self):
        # Determine workspace root (auto-detection)
        self._workspace = get_workspace()
        self._dataspace = get_dataspace()

        # Load configs in order (later overrides earlier)
        self._load_configs()

    def _load_configs(self):
        """Load all config files"""
        workspace = Path(self._workspace)

        # 1. Platform config
        platform_env = workspace / 'config' / '.env.platform'
        self._data.update(_load_env_file(platform_env))

        # 2. Project config (if specified)
        if self.project:
            project_env = workspace / 'projects' / self.project / '.env'
            self._data.update(_load_env_file(project_env))

        # 3. Local overrides
        local_env = workspace / '.env.local'
        self._data.update(_load_env_file(local_env))

        # Always set paths from environment or defaults
        self._data['WORKSPACE'] = self._workspace
        self._data['DATASPACE'] = self._dataspace

    def __getattr__(self, name: str) -> str:
        """Get config value as attribute"""
        if name.startswith('_'):
            return object.__getattribute__(self, name)

        value = self._data.get(name)
        if value is None:
            # Also check environment
            value = os.environ.get(name)

        if value is None:
            raise AttributeError(f"Config key not found: {name}")

        return value

    def get(self, key: str, default: str = None) -> Optional[str]:
        """Get config value with default"""
        return self._data.get(key, os.environ.get(key, default))

    def get_int(self, key: str, default: int = 0) -> int:
        """Get config value as int"""
        value = self.get(key)
        return int(value) if value else default

    def get_bool(self, key: str, default: bool = False) -> bool:
        """Get config value as bool"""
        value = self.get(key, '').lower()
        if value in ('true', '1', 'yes', 'on'):
            return True
        if value in ('false', '0', 'no', 'off'):
            return False
        return default

    # ==========================================
    # Convenience methods
    # ==========================================

    def workspace_path(self, *parts) -> Path:
        """Get path relative to WORKSPACE"""
        return Path(self._workspace).joinpath(*parts)

    def data_path(self, *parts) -> Path:
        """Get path relative to DATASPACE"""
        return Path(self._dataspace).joinpath(*parts)

    def project_path(self, *parts) -> Path:
        """Get path relative to project in WORKSPACE"""
        if not self.project:
            raise ValueError("Project not specified")
        return self.workspace_path('projects', self.project, *parts)

    def project_data_path(self, *parts) -> Path:
        """Get path relative to project in DATASPACE"""
        if not self.project:
            raise ValueError("Project not specified")
        return self.data_path('projects', self.project, *parts)

    def get_ozon_accounts(self) -> Dict[str, Dict[str, str]]:
        """Get all OZON accounts as dict"""
        accounts = {}

        for acc_id in ['O1', 'O2', 'O3']:
            client_id = self.get(f'OZON_{acc_id}_CLIENT_ID')
            api_key = self.get(f'OZON_{acc_id}_API_KEY')
            name = self.get(f'OZON_{acc_id}_NAME', acc_id)

            if client_id and api_key:
                accounts[acc_id] = {
                    'client_id': client_id,
                    'api_key': api_key,
                    'name': name
                }

        return accounts

    def get_nocodb_config(self) -> Dict[str, str]:
        """Get NocoDB connection config"""
        return {
            'base_url': self.get('NOCODB_URL', 'http://localhost:8080'),
            'email': self.get('NOCODB_EMAIL', 'admin@0kt.ru'),
            'password': self.get('NOCODB_PASSWORD', ''),
            'base_id': self.get('NOCODB_BASE_ID', ''),
        }

    def get_postgres_config(self) -> Dict[str, Any]:
        """Get PostgreSQL connection config"""
        return {
            'host': self.get('POSTGRES_HOST', 'localhost'),
            'port': self.get_int('POSTGRES_PORT', 5432),
            'user': self.get('POSTGRES_USER', 'postgres'),
            'password': self.get('POSTGRES_PASSWORD', ''),
            'database': self.get('POSTGRES_DB', 'nocodb'),
        }

    def to_dict(self) -> Dict[str, str]:
        """Export all config as dict"""
        return dict(self._data)


# Singleton for convenience
_default_config: Optional[Config] = None


def get_config(project: str = None) -> Config:
    """Get config instance (cached if no project specified)"""
    global _default_config

    if project:
        return Config(project=project)

    if _default_config is None:
        _default_config = Config()

    return _default_config


# Shortcut functions
def workspace_path(*parts) -> Path:
    """Get path relative to WORKSPACE"""
    return get_config().workspace_path(*parts)


def data_path(*parts) -> Path:
    """Get path relative to DATASPACE"""
    return get_config().data_path(*parts)