from __future__ import annotations
import logging
from typing import Callable, Optional, TypeVar, ParamSpec, Any
from functools import wraps

log = logging.getLogger(__name__)

try:
    import System  # pythonnet; may be absent in tests
except Exception:  # pragma: no cover
    System = None  # type: ignore

T = TypeVar("T")
P = ParamSpec("P")


def ensure_not_none(value: Any, *, what: str, exc_type: type[BaseException], message: Optional[str] = None) -> None:
    """
    Raise `exc_type` if `value` is None. Standardizes native object presence checks.
    """
    if value is None:
        msg = message or f"{what} is not available."
        raise exc_type(msg)


def run_native(what: str, call: Callable[[], T], *, ensure: Optional[Callable[[], None]] = None) -> T:
    """
    Execute a native call with unified exception translation & optional pre-check.

    Parameters
    ----------
    what   : human-readable operation name, for logs and error context.
    call   : thunk that performs the actual native call and returns a result.
    ensure : optional callable that validates preconditions (e.g., native object present).

    Returns
    -------
    The result of `call()`.
    """
    # Import here to avoid import cycles if interop is imported early.
    from zempy.bridge import zemax_exceptions as _exc

    if ensure is not None:
        ensure()

    log.debug("%s", what)
    try:
        return call()
    except (OSError, RuntimeError) as e:
        log.warning("%s runtime/OS error: %s", what, e)
        raise _exc.ZemaxSystemError(f"{what} failed (runtime/OS): {e}") from e
    except Exception as e:
        if System is not None:
            try:
                # isinstance with CLR exception type, only if pythonnet present
                if isinstance(e, System.Exception):  # type: ignore[attr-defined]
                    log.warning("%s CLR exception: %s", what, e)
                    raise _exc.ZemaxSystemError(f"{what} failed (CLR): {e}") from e
            except Exception:
                # Defensive: if System.Exception lookup behaves oddly in some envs
                pass
        raise


def native_call(
    what: str,
    ensure: Optional[Callable[[], None]] = None,
):
    """
    Decorator form of `run_native`. Wraps a zero-arg function body.

    Usage
    -----
    @native_call("IMetadata.NumberOfKeys get", ensure=self._ensure_native)
    def _get_keys():
        return self._native_meta.NumberOfKeys
    """
    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        @wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            return run_native(what, lambda: func(*args, **kwargs), ensure=ensure)
        return wrapper
    return decorator
