# shared/config.py — SX-first config loader with YAML fallback
from __future__ import annotations

import asyncio
import os
from types import MappingProxyType
from typing import Any, Optional
import copy

# Default config paths (override with APP_CONFIG_FILE)
_DEFAULT_YAML_PATH = os.environ.get(
    "APP_CONFIG_FILE",
    os.path.join(os.getcwd(), "config/app-config.yaml"),
)
_DEFAULT_SX_PATH = os.environ.get(
    "APP_CONFIG_SX_FILE",
    os.path.join(os.getcwd(), "config/app-config.sx"),
)

# Module state
_init_lock = asyncio.Lock()
_data_frozen: Any = None   # read-only view (mappingproxy / tuples / frozensets)
_data_plain: Any = None    # plain builtins for pretty-print / logging

# ---------------- utils ----------------
def _freeze(obj: Any) -> Any:
    """Deep-freeze containers to read-only equivalents."""
    if isinstance(obj, dict):
        return MappingProxyType({k: _freeze(v) for k, v in obj.items()})
    if isinstance(obj, list):
        return tuple(_freeze(v) for v in obj)
    if isinstance(obj, set):
        return frozenset(_freeze(v) for v in obj)
    if isinstance(obj, tuple):
        return tuple(_freeze(v) for v in obj)
    return obj


def _sx_to_dict(expr: Any) -> Any:
    """Convert parsed SX config values to plain Python dicts/lists.

    - Keyword keys become strings (kebab-case preserved, also aliased to
      underscore form for backward compatibility with YAML consumers).
    - (env-get "VAR") calls are resolved to os.environ.
    - Lists become plain Python lists.
    - Everything else passes through as-is.
    """
    from shared.sx.types import Keyword, Symbol

    # (env-get "VAR") → os.environ.get("VAR")
    if isinstance(expr, list) and len(expr) == 2:
        head = expr[0]
        if isinstance(head, Symbol) and head.name == "env-get":
            var_name = str(expr[1])
            return os.environ.get(var_name)

    # dict with keyword keys
    if isinstance(expr, dict):
        result: dict[str, Any] = {}
        for k, v in expr.items():
            key = k if isinstance(k, str) else str(k)
            val = _sx_to_dict(v)
            result[key] = val
            # Alias kebab-case → underscore for backward compat
            underscore = key.replace("-", "_")
            if underscore != key:
                result[underscore] = val
        return result

    if isinstance(expr, list):
        # Check for (env-get ...) first (already handled above for len==2)
        return [_sx_to_dict(item) for item in expr]

    if isinstance(expr, tuple):
        return [_sx_to_dict(item) for item in expr]

    if isinstance(expr, Keyword):
        return str(expr)

    if isinstance(expr, Symbol):
        name = expr.name
        if name == "true":
            return True
        if name == "false":
            return False
        if name == "nil":
            return None
        return name

    return expr


def _load_sx_config(path: str) -> dict:
    """Load an SX config file and return a plain dict.

    Expects a single (defconfig name body) form.
    """
    from shared.sx.parser import parse_all

    with open(path, "r", encoding="utf-8") as f:
        source = f.read()

    exprs = parse_all(source)
    for expr in exprs:
        if (isinstance(expr, list) and len(expr) >= 3
                and hasattr(expr[0], 'name') and expr[0].name == "defconfig"):
            # (defconfig name {body})
            body = expr[2]
            return _sx_to_dict(body)

    raise ValueError(f"No (defconfig ...) form found in {path}")


# ---------------- API ----------------
async def init_config(path: Optional[str] = None, *, force: bool = False) -> None:
    """
    Load config and cache both a frozen (read-only) and a plain copy.

    Prefers SX config (app-config.sx) when available, falls back to YAML.
    Idempotent; pass force=True to reload.
    """
    global _data_frozen, _data_plain

    if _data_frozen is not None and not force:
        return

    async with _init_lock:
        if _data_frozen is not None and not force:
            return

        # Try SX first, then YAML
        sx_path = path if (path and path.endswith(".sx")) else _DEFAULT_SX_PATH
        yaml_path = path if (path and not path.endswith(".sx")) else _DEFAULT_YAML_PATH

        if os.path.exists(sx_path):
            raw = _load_sx_config(sx_path)
        elif os.path.exists(yaml_path):
            import yaml
            with open(yaml_path, "r", encoding="utf-8") as f:
                raw = yaml.safe_load(f)
        else:
            raise FileNotFoundError(
                f"No config found: tried {sx_path} and {yaml_path}")

        _data_plain = raw
        _data_frozen = _freeze(raw)

def config() -> Any:
    """
    Return the read-only (frozen) config. Call init_config() first.
    """
    if _data_frozen is None:
        raise RuntimeError("init_config() has not been awaited yet.")
    return _data_frozen

def as_plain() -> Any:
    """
    Return a deep copy of the plain config for safe external use/pretty printing.
    """
    if _data_plain is None:
        raise RuntimeError("init_config() has not been awaited yet.")
    return copy.deepcopy(_data_plain)

def pretty() -> str:
    """
    Pretty string for logging. Uses YAML if available, else pprint.
    """
    if _data_plain is None:
        raise RuntimeError("init_config() has not been awaited yet.")
    try:
        import yaml
        return yaml.safe_dump(_data_plain, sort_keys=False, allow_unicode=True)
    except ImportError:
        import pprint
        return pprint.pformat(_data_plain)
