"""ZLayer SDK for building WASM plugins in Python.

This package provides helper functions that wrap the generated WIT bindings
for easier access to ZLayer host capabilities including configuration,
key-value storage, logging, secrets, and metrics.

The actual WIT bindings are generated by componentize-py at build time.
This module provides ergonomic Python wrappers around those generated bindings.

Example usage:
    from zlayer import (
        get_config,
        kv_get_str,
        log_info,
        ZLayerPlugin,
        PluginInfo,
    )

    class MyPlugin(ZLayerPlugin):
        def init(self) -> None:
            api_key = get_config("api_key")
            log_info(f"Plugin initialized with key: {api_key[:4]}...")

        def info(self) -> PluginInfo:
            return PluginInfo(
                id="mycompany:my-plugin",
                name="My Plugin",
                version="1.0.0",
                description="A sample ZLayer plugin",
                author="My Company",
            )

        def handle(self, request: PluginRequest) -> HandleResult:
            return HandleResult.pass_through()
"""

from __future__ import annotations

import json
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import IntEnum
from typing import Any, Callable, TypeVar

__version__ = "0.10.42"
__all__ = [
    # Version
    "__version__",
    # Config helpers
    "get_config",
    "get_config_required",
    "get_config_bool",
    "get_config_int",
    "get_config_float",
    "get_config_many",
    "get_config_prefix",
    "config_exists",
    # KV helpers
    "kv_get",
    "kv_get_str",
    "kv_set",
    "kv_set_str",
    "kv_set_with_ttl",
    "kv_delete",
    "kv_keys",
    "kv_exists",
    "kv_increment",
    "kv_compare_and_swap",
    # Logging
    "LogLevel",
    "log",
    "log_trace",
    "log_debug",
    "log_info",
    "log_warn",
    "log_error",
    "log_structured",
    "is_log_enabled",
    # Secrets
    "get_secret",
    "get_secret_required",
    "secret_exists",
    "list_secret_names",
    # Metrics
    "counter_inc",
    "counter_inc_labeled",
    "gauge_set",
    "gauge_set_labeled",
    "gauge_add",
    "histogram_observe",
    "histogram_observe_labeled",
    "record_duration",
    "record_duration_labeled",
    # Error types
    "ZLayerError",
    "ConfigError",
    "KVError",
    "KVErrorKind",
    "SecretError",
    # Plugin types
    "PluginInfo",
    "Version",
    "KeyValue",
    "Capabilities",
    "HttpMethod",
    "PluginRequest",
    "PluginResponse",
    "HandleResult",
    "InitError",
    "InitErrorKind",
    # Plugin base class
    "ZLayerPlugin",
    # Decorators
    "require_capability",
]


# =============================================================================
# Type Variables
# =============================================================================

T = TypeVar("T")


# =============================================================================
# Error Types
# =============================================================================


class ZLayerError(Exception):
    """Base exception for all ZLayer errors."""

    def __init__(self, message: str, code: str = "UNKNOWN") -> None:
        super().__init__(message)
        self.code = code
        self.message = message

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(code={self.code!r}, message={self.message!r})"


class ConfigError(ZLayerError):
    """Error accessing configuration values."""

    def __init__(self, message: str, key: str | None = None) -> None:
        super().__init__(message, code="CONFIG_ERROR")
        self.key = key


class KVErrorKind(IntEnum):
    """Kind of key-value error."""

    NOT_FOUND = 0
    VALUE_TOO_LARGE = 1
    QUOTA_EXCEEDED = 2
    INVALID_KEY = 3
    STORAGE = 4


class KVError(ZLayerError):
    """Error accessing key-value storage."""

    def __init__(
        self,
        message: str,
        kind: KVErrorKind = KVErrorKind.STORAGE,
        key: str | None = None,
    ) -> None:
        super().__init__(message, code="KV_ERROR")
        self.kind = kind
        self.key = key

    @classmethod
    def not_found(cls, key: str) -> KVError:
        """Create a not found error."""
        return cls(f"Key not found: {key}", KVErrorKind.NOT_FOUND, key)

    @classmethod
    def value_too_large(cls, key: str, size: int) -> KVError:
        """Create a value too large error."""
        return cls(f"Value too large for key {key}: {size} bytes", KVErrorKind.VALUE_TOO_LARGE, key)

    @classmethod
    def quota_exceeded(cls) -> KVError:
        """Create a quota exceeded error."""
        return cls("Storage quota exceeded", KVErrorKind.QUOTA_EXCEEDED)

    @classmethod
    def invalid_key(cls, key: str) -> KVError:
        """Create an invalid key error."""
        return cls(f"Invalid key format: {key}", KVErrorKind.INVALID_KEY, key)


class SecretError(ZLayerError):
    """Error accessing secrets."""

    def __init__(self, message: str, name: str | None = None) -> None:
        super().__init__(message, code="SECRET_ERROR")
        self.name = name


# =============================================================================
# Common Types
# =============================================================================


@dataclass(frozen=True, slots=True)
class KeyValue:
    """A key-value pair for metadata, headers, etc."""

    key: str
    value: str

    def to_tuple(self) -> tuple[str, str]:
        """Convert to a tuple."""
        return (self.key, self.value)

    @classmethod
    def from_tuple(cls, t: tuple[str, str]) -> KeyValue:
        """Create from a tuple."""
        return cls(key=t[0], value=t[1])

    @classmethod
    def from_dict(cls, d: dict[str, str]) -> list[KeyValue]:
        """Convert a dict to a list of KeyValue pairs."""
        return [cls(key=k, value=v) for k, v in d.items()]

    @staticmethod
    def to_dict(kvs: list[KeyValue]) -> dict[str, str]:
        """Convert a list of KeyValue pairs to a dict."""
        return {kv.key: kv.value for kv in kvs}


@dataclass(frozen=True, slots=True)
class Version:
    """Plugin version following semver."""

    major: int
    minor: int
    patch: int
    pre_release: str | None = None

    def __str__(self) -> str:
        version = f"{self.major}.{self.minor}.{self.patch}"
        if self.pre_release:
            version += f"-{self.pre_release}"
        return version

    @classmethod
    def parse(cls, version_str: str) -> Version:
        """Parse a version string like '1.2.3' or '1.2.3-beta.1'."""
        pre_release = None
        if "-" in version_str:
            version_str, pre_release = version_str.split("-", 1)

        parts = version_str.split(".")
        if len(parts) != 3:
            raise ValueError(f"Invalid version format: {version_str}")

        return cls(
            major=int(parts[0]),
            minor=int(parts[1]),
            patch=int(parts[2]),
            pre_release=pre_release,
        )


class Capabilities:
    """Plugin capabilities that can be requested.

    This is a flags class where multiple capabilities can be combined.
    """

    CONFIG = 1 << 0
    KEYVALUE = 1 << 1
    LOGGING = 1 << 2
    SECRETS = 1 << 3
    METRICS = 1 << 4
    HTTP_CLIENT = 1 << 5

    def __init__(self, flags: int = 0) -> None:
        self._flags = flags

    def __or__(self, other: Capabilities | int) -> Capabilities:
        if isinstance(other, Capabilities):
            return Capabilities(self._flags | other._flags)
        return Capabilities(self._flags | other)

    def __and__(self, other: Capabilities | int) -> Capabilities:
        if isinstance(other, Capabilities):
            return Capabilities(self._flags & other._flags)
        return Capabilities(self._flags & other)

    def __contains__(self, cap: int) -> bool:
        return bool(self._flags & cap)

    def __repr__(self) -> str:
        caps = []
        if self.CONFIG in self:
            caps.append("CONFIG")
        if self.KEYVALUE in self:
            caps.append("KEYVALUE")
        if self.LOGGING in self:
            caps.append("LOGGING")
        if self.SECRETS in self:
            caps.append("SECRETS")
        if self.METRICS in self:
            caps.append("METRICS")
        if self.HTTP_CLIENT in self:
            caps.append("HTTP_CLIENT")
        return f"Capabilities({' | '.join(caps)})"

    @classmethod
    def all(cls) -> Capabilities:
        """Create a Capabilities object with all capabilities enabled."""
        return Capabilities(
            cls.CONFIG | cls.KEYVALUE | cls.LOGGING | cls.SECRETS | cls.METRICS | cls.HTTP_CLIENT
        )

    @classmethod
    def none(cls) -> Capabilities:
        """Create a Capabilities object with no capabilities."""
        return Capabilities(0)


# =============================================================================
# Plugin Metadata
# =============================================================================


@dataclass(slots=True)
class PluginInfo:
    """Plugin information returned by info().

    Attributes:
        id: Unique plugin identifier (e.g., "zlayer:auth-jwt")
        name: Human-readable name
        version: Plugin version (string or Version object)
        description: Brief description of plugin functionality
        author: Plugin author or organization
        license: License identifier (e.g., "MIT", "Apache-2.0")
        homepage: Homepage or repository URL
        metadata: Additional metadata as key-value pairs
    """

    id: str
    name: str
    version: str | Version
    description: str
    author: str
    license: str | None = None
    homepage: str | None = None
    metadata: dict[str, str] = field(default_factory=dict)

    def get_version(self) -> Version:
        """Get the version as a Version object."""
        if isinstance(self.version, Version):
            return self.version
        return Version.parse(self.version)


# =============================================================================
# Request/Response Types
# =============================================================================


class HttpMethod(IntEnum):
    """HTTP method enum."""

    GET = 0
    POST = 1
    PUT = 2
    DELETE = 3
    PATCH = 4
    HEAD = 5
    OPTIONS = 6
    CONNECT = 7
    TRACE = 8

    def __str__(self) -> str:
        return self.name


@dataclass(slots=True)
class PluginRequest:
    """Incoming request to be processed by a plugin.

    Attributes:
        request_id: Unique request identifier for tracing
        path: Request path (e.g., "/api/users/123")
        method: HTTP method
        query: Query string (without leading ?)
        headers: Request headers as KeyValue list
        body: Request body as bytes
        timestamp: Request timestamp in nanoseconds since epoch
        context: Additional context from the host
    """

    request_id: str
    path: str
    method: HttpMethod
    query: str | None
    headers: list[KeyValue]
    body: bytes
    timestamp: int
    context: list[KeyValue]

    def get_header(self, name: str) -> str | None:
        """Get a header value by name (case-insensitive)."""
        name_lower = name.lower()
        for kv in self.headers:
            if kv.key.lower() == name_lower:
                return kv.value
        return None

    def get_headers(self, name: str) -> list[str]:
        """Get all header values for a name (case-insensitive)."""
        name_lower = name.lower()
        return [kv.value for kv in self.headers if kv.key.lower() == name_lower]

    def get_context(self, key: str) -> str | None:
        """Get a context value by key."""
        for kv in self.context:
            if kv.key == key:
                return kv.value
        return None

    def body_str(self, encoding: str = "utf-8") -> str:
        """Get the body as a string."""
        return self.body.decode(encoding)

    def body_json(self) -> Any:
        """Parse the body as JSON."""
        return json.loads(self.body)


@dataclass(slots=True)
class PluginResponse:
    """Plugin response returned to the host.

    Attributes:
        status: HTTP status code (200, 404, 500, etc.)
        headers: Response headers
        body: Response body
    """

    status: int
    headers: list[KeyValue]
    body: bytes

    @classmethod
    def ok(cls, body: bytes | str = b"", headers: dict[str, str] | None = None) -> PluginResponse:
        """Create a 200 OK response."""
        if isinstance(body, str):
            body = body.encode("utf-8")
        return cls(
            status=200,
            headers=KeyValue.from_dict(headers or {}),
            body=body,
        )

    @classmethod
    def json(
        cls,
        data: Any,
        status: int = 200,
        headers: dict[str, str] | None = None,
    ) -> PluginResponse:
        """Create a JSON response."""
        h = headers.copy() if headers else {}
        h["Content-Type"] = "application/json"
        return cls(
            status=status,
            headers=KeyValue.from_dict(h),
            body=json.dumps(data).encode("utf-8"),
        )

    @classmethod
    def error(
        cls,
        status: int,
        message: str,
        headers: dict[str, str] | None = None,
    ) -> PluginResponse:
        """Create an error response."""
        h = headers.copy() if headers else {}
        h["Content-Type"] = "application/json"
        return cls(
            status=status,
            headers=KeyValue.from_dict(h),
            body=json.dumps({"error": message}).encode("utf-8"),
        )

    @classmethod
    def not_found(cls, message: str = "Not Found") -> PluginResponse:
        """Create a 404 Not Found response."""
        return cls.error(404, message)

    @classmethod
    def bad_request(cls, message: str = "Bad Request") -> PluginResponse:
        """Create a 400 Bad Request response."""
        return cls.error(400, message)

    @classmethod
    def unauthorized(cls, message: str = "Unauthorized") -> PluginResponse:
        """Create a 401 Unauthorized response."""
        return cls.error(401, message)

    @classmethod
    def forbidden(cls, message: str = "Forbidden") -> PluginResponse:
        """Create a 403 Forbidden response."""
        return cls.error(403, message)

    @classmethod
    def internal_error(cls, message: str = "Internal Server Error") -> PluginResponse:
        """Create a 500 Internal Server Error response."""
        return cls.error(500, message)


class HandleResult:
    """Result of handling a request.

    This is a tagged union with three variants:
    - response: Plugin handled the request, return the response
    - pass_through: Plugin chose not to handle, continue to next handler
    - error: Plugin encountered an error
    """

    __slots__ = ("_kind", "_value")

    RESPONSE = 0
    PASS_THROUGH = 1
    ERROR = 2

    def __init__(self, kind: int, value: Any = None) -> None:
        self._kind = kind
        self._value = value

    @classmethod
    def response(cls, resp: PluginResponse) -> HandleResult:
        """Create a response result."""
        return cls(cls.RESPONSE, resp)

    @classmethod
    def pass_through(cls) -> HandleResult:
        """Create a pass-through result."""
        return cls(cls.PASS_THROUGH)

    @classmethod
    def error(cls, message: str) -> HandleResult:
        """Create an error result."""
        return cls(cls.ERROR, message)

    def is_response(self) -> bool:
        """Check if this is a response result."""
        return self._kind == self.RESPONSE

    def is_pass_through(self) -> bool:
        """Check if this is a pass-through result."""
        return self._kind == self.PASS_THROUGH

    def is_error(self) -> bool:
        """Check if this is an error result."""
        return self._kind == self.ERROR

    def get_response(self) -> PluginResponse:
        """Get the response (raises if not a response result)."""
        if self._kind != self.RESPONSE:
            raise ValueError("Not a response result")
        return self._value

    def get_error(self) -> str:
        """Get the error message (raises if not an error result)."""
        if self._kind != self.ERROR:
            raise ValueError("Not an error result")
        return self._value

    def __repr__(self) -> str:
        if self._kind == self.RESPONSE:
            return f"HandleResult.response({self._value!r})"
        elif self._kind == self.PASS_THROUGH:
            return "HandleResult.pass_through()"
        else:
            return f"HandleResult.error({self._value!r})"


class InitErrorKind(IntEnum):
    """Kind of initialization error."""

    CONFIG_MISSING = 0
    CONFIG_INVALID = 1
    CAPABILITY_UNAVAILABLE = 2
    FAILED = 3


@dataclass(slots=True)
class InitError(Exception):
    """Plugin initialization error."""

    kind: InitErrorKind
    message: str

    def __str__(self) -> str:
        return f"{self.kind.name}: {self.message}"

    @classmethod
    def config_missing(cls, key: str) -> InitError:
        """Create a config missing error."""
        return cls(InitErrorKind.CONFIG_MISSING, f"Required configuration missing: {key}")

    @classmethod
    def config_invalid(cls, key: str, reason: str) -> InitError:
        """Create a config invalid error."""
        return cls(InitErrorKind.CONFIG_INVALID, f"Invalid configuration for {key}: {reason}")

    @classmethod
    def capability_unavailable(cls, capability: str) -> InitError:
        """Create a capability unavailable error."""
        return cls(
            InitErrorKind.CAPABILITY_UNAVAILABLE,
            f"Required capability not available: {capability}",
        )

    @classmethod
    def failed(cls, reason: str) -> InitError:
        """Create a generic failure error."""
        return cls(InitErrorKind.FAILED, reason)


# =============================================================================
# Logging
# =============================================================================


class LogLevel(IntEnum):
    """Log severity levels matching WIT enum."""

    TRACE = 0
    DEBUG = 1
    INFO = 2
    WARN = 3
    ERROR = 4


# Placeholder for the actual binding - will be replaced by componentize-py generated code
_logging_binding: Any = None


def _get_logging_binding() -> Any:
    """Get the logging binding, importing it lazily."""
    global _logging_binding
    if _logging_binding is None:
        try:
            # This import path is generated by componentize-py
            from zlayer.bindings.zlayer.host import logging as binding

            _logging_binding = binding
        except ImportError:
            # Not yet generated, use stub
            _logging_binding = _LoggingStub()
    return _logging_binding


class _LoggingStub:
    """Stub for logging when bindings aren't available."""

    def log(self, level: int, message: str) -> None:
        level_name = LogLevel(level).name
        print(f"[{level_name}] {message}")

    def log_structured(self, level: int, message: str, fields: list[tuple[str, str]]) -> None:
        level_name = LogLevel(level).name
        fields_str = " ".join(f"{k}={v}" for k, v in fields)
        print(f"[{level_name}] {message} {fields_str}")

    def trace(self, message: str) -> None:
        self.log(LogLevel.TRACE, message)

    def debug(self, message: str) -> None:
        self.log(LogLevel.DEBUG, message)

    def info(self, message: str) -> None:
        self.log(LogLevel.INFO, message)

    def warn(self, message: str) -> None:
        self.log(LogLevel.WARN, message)

    def error(self, message: str) -> None:
        self.log(LogLevel.ERROR, message)

    def is_enabled(self, level: int) -> bool:
        return True


def log(level: LogLevel, msg: str) -> None:
    """Emit a log message at the specified level.

    Args:
        level: The log level (TRACE, DEBUG, INFO, WARN, ERROR)
        msg: The message to log
    """
    _get_logging_binding().log(level, msg)


def log_trace(msg: str) -> None:
    """Emit a TRACE level log message."""
    _get_logging_binding().trace(msg)


def log_debug(msg: str) -> None:
    """Emit a DEBUG level log message."""
    _get_logging_binding().debug(msg)


def log_info(msg: str) -> None:
    """Emit an INFO level log message."""
    _get_logging_binding().info(msg)


def log_warn(msg: str) -> None:
    """Emit a WARN level log message."""
    _get_logging_binding().warn(msg)


def log_error(msg: str) -> None:
    """Emit an ERROR level log message."""
    _get_logging_binding().error(msg)


def log_structured(level: LogLevel, msg: str, fields: dict[str, str]) -> None:
    """Emit a structured log with key-value fields.

    Args:
        level: The log level
        msg: The message to log
        fields: Additional structured fields to include
    """
    field_tuples = [(k, v) for k, v in fields.items()]
    _get_logging_binding().log_structured(level, msg, field_tuples)


def is_log_enabled(level: LogLevel) -> bool:
    """Check if a log level is enabled.

    Use this before expensive log message construction.

    Args:
        level: The log level to check

    Returns:
        True if the level is enabled
    """
    return _get_logging_binding().is_enabled(level)


# =============================================================================
# Configuration
# =============================================================================

_config_binding: Any = None


def _get_config_binding() -> Any:
    """Get the config binding, importing it lazily."""
    global _config_binding
    if _config_binding is None:
        try:
            from zlayer.bindings.zlayer.host import config as binding

            _config_binding = binding
        except ImportError:
            _config_binding = _ConfigStub()
    return _config_binding


class _ConfigStub:
    """Stub for config when bindings aren't available."""

    _data: dict[str, str] = {}

    def get(self, key: str) -> str | None:
        return self._data.get(key)

    def get_required(self, key: str) -> str:
        if key not in self._data:
            raise ConfigError(f"Required config key not found: {key}", key)
        return self._data[key]

    def get_many(self, keys: list[str]) -> list[tuple[str, str]]:
        return [(k, v) for k in keys if (v := self._data.get(k)) is not None]

    def get_prefix(self, prefix: str) -> list[tuple[str, str]]:
        return [(k, v) for k, v in self._data.items() if k.startswith(prefix)]

    def exists(self, key: str) -> bool:
        return key in self._data

    def get_bool(self, key: str) -> bool | None:
        val = self._data.get(key)
        if val is None:
            return None
        return val.lower() in ("true", "1", "yes")

    def get_int(self, key: str) -> int | None:
        val = self._data.get(key)
        if val is None:
            return None
        return int(val)

    def get_float(self, key: str) -> float | None:
        val = self._data.get(key)
        if val is None:
            return None
        return float(val)


def get_config(key: str) -> str | None:
    """Get a configuration value by key.

    Args:
        key: The configuration key

    Returns:
        The configuration value, or None if not found
    """
    return _get_config_binding().get(key)


def get_config_required(key: str) -> str:
    """Get a required configuration value by key.

    Args:
        key: The configuration key

    Returns:
        The configuration value

    Raises:
        ConfigError: If the key is not found
    """
    try:
        return _get_config_binding().get_required(key)
    except Exception as e:
        raise ConfigError(str(e), key) from e


def get_config_bool(key: str) -> bool | None:
    """Get a configuration value as a boolean.

    Recognizes: "true", "false", "1", "0", "yes", "no"

    Args:
        key: The configuration key

    Returns:
        The boolean value, or None if not found
    """
    return _get_config_binding().get_bool(key)


def get_config_int(key: str) -> int | None:
    """Get a configuration value as an integer.

    Args:
        key: The configuration key

    Returns:
        The integer value, or None if not found
    """
    return _get_config_binding().get_int(key)


def get_config_float(key: str) -> float | None:
    """Get a configuration value as a float.

    Args:
        key: The configuration key

    Returns:
        The float value, or None if not found
    """
    return _get_config_binding().get_float(key)


def get_config_many(keys: list[str]) -> dict[str, str]:
    """Get multiple configuration values at once.

    Args:
        keys: List of configuration keys to retrieve

    Returns:
        Dictionary of key-value pairs for keys that exist
    """
    result = _get_config_binding().get_many(keys)
    return dict(result)


def get_config_prefix(prefix: str) -> dict[str, str]:
    """Get all configuration keys with a given prefix.

    Example:
        get_config_prefix("database.") returns all database.* keys

    Args:
        prefix: The prefix to match

    Returns:
        Dictionary of matching key-value pairs
    """
    result = _get_config_binding().get_prefix(prefix)
    return dict(result)


def config_exists(key: str) -> bool:
    """Check if a configuration key exists.

    Args:
        key: The configuration key

    Returns:
        True if the key exists
    """
    return _get_config_binding().exists(key)


# =============================================================================
# Key-Value Storage
# =============================================================================

_kv_binding: Any = None


def _get_kv_binding() -> Any:
    """Get the keyvalue binding, importing it lazily."""
    global _kv_binding
    if _kv_binding is None:
        try:
            from zlayer.bindings.zlayer.host import keyvalue as binding

            _kv_binding = binding
        except ImportError:
            _kv_binding = _KVStub()
    return _kv_binding


class _KVStub:
    """Stub for keyvalue when bindings aren't available."""

    _data: dict[str, bytes] = {}

    def get(self, key: str) -> bytes | None:
        return self._data.get(key)

    def get_string(self, key: str) -> str | None:
        val = self._data.get(key)
        return val.decode("utf-8") if val else None

    def set(self, key: str, value: bytes) -> None:
        self._data[key] = value

    def set_string(self, key: str, value: str) -> None:
        self._data[key] = value.encode("utf-8")

    def set_with_ttl(self, key: str, value: bytes, ttl_ns: int) -> None:
        self._data[key] = value

    def delete(self, key: str) -> bool:
        if key in self._data:
            del self._data[key]
            return True
        return False

    def exists(self, key: str) -> bool:
        return key in self._data

    def list_keys(self, prefix: str) -> list[str]:
        return [k for k in self._data.keys() if k.startswith(prefix)]

    def increment(self, key: str, delta: int) -> int:
        current = self._data.get(key, b"0")
        new_val = int(current.decode("utf-8")) + delta
        self._data[key] = str(new_val).encode("utf-8")
        return new_val

    def compare_and_swap(
        self, key: str, expected: bytes | None, new_value: bytes
    ) -> bool:
        current = self._data.get(key)
        if current == expected:
            self._data[key] = new_value
            return True
        return False


def kv_get(key: str) -> bytes | None:
    """Get a value by key.

    Args:
        key: The key to retrieve

    Returns:
        The value as bytes, or None if not found

    Raises:
        KVError: On storage errors
    """
    try:
        return _get_kv_binding().get(key)
    except Exception as e:
        raise KVError(str(e), key=key) from e


def kv_get_str(key: str) -> str | None:
    """Get a value as a string.

    Args:
        key: The key to retrieve

    Returns:
        The value as a string, or None if not found

    Raises:
        KVError: On storage errors
    """
    try:
        return _get_kv_binding().get_string(key)
    except Exception as e:
        raise KVError(str(e), key=key) from e


def kv_set(key: str, value: bytes) -> None:
    """Set a value.

    Args:
        key: The key to set
        value: The value as bytes

    Raises:
        KVError: On storage errors
    """
    try:
        _get_kv_binding().set(key, value)
    except Exception as e:
        raise KVError(str(e), key=key) from e


def kv_set_str(key: str, value: str) -> None:
    """Set a string value.

    Args:
        key: The key to set
        value: The value as a string

    Raises:
        KVError: On storage errors
    """
    try:
        _get_kv_binding().set_string(key, value)
    except Exception as e:
        raise KVError(str(e), key=key) from e


def kv_set_with_ttl(key: str, value: bytes, ttl_ns: int) -> None:
    """Set a value with a TTL (time-to-live).

    Args:
        key: The key to set
        value: The value as bytes
        ttl_ns: Time-to-live in nanoseconds

    Raises:
        KVError: On storage errors
    """
    try:
        _get_kv_binding().set_with_ttl(key, value, ttl_ns)
    except Exception as e:
        raise KVError(str(e), key=key) from e


def kv_delete(key: str) -> bool:
    """Delete a key.

    Args:
        key: The key to delete

    Returns:
        True if the key was deleted, False if it didn't exist

    Raises:
        KVError: On storage errors
    """
    try:
        return _get_kv_binding().delete(key)
    except Exception as e:
        raise KVError(str(e), key=key) from e


def kv_keys(prefix: str) -> list[str]:
    """List all keys with a given prefix.

    Args:
        prefix: The prefix to match

    Returns:
        List of matching keys

    Raises:
        KVError: On storage errors
    """
    try:
        return _get_kv_binding().list_keys(prefix)
    except Exception as e:
        raise KVError(str(e)) from e


def kv_exists(key: str) -> bool:
    """Check if a key exists.

    Args:
        key: The key to check

    Returns:
        True if the key exists
    """
    return _get_kv_binding().exists(key)


def kv_increment(key: str, delta: int = 1) -> int:
    """Increment a numeric value atomically.

    Args:
        key: The key to increment
        delta: The amount to add (can be negative)

    Returns:
        The new value after increment

    Raises:
        KVError: On storage errors
    """
    try:
        return _get_kv_binding().increment(key, delta)
    except Exception as e:
        raise KVError(str(e), key=key) from e


def kv_compare_and_swap(key: str, expected: bytes | None, new_value: bytes) -> bool:
    """Compare and swap - set value only if current value matches expected.

    Args:
        key: The key to set
        expected: The expected current value (None for key not existing)
        new_value: The new value to set

    Returns:
        True if swap succeeded, False if current value didn't match

    Raises:
        KVError: On storage errors
    """
    try:
        return _get_kv_binding().compare_and_swap(key, expected, new_value)
    except Exception as e:
        raise KVError(str(e), key=key) from e


# =============================================================================
# Secrets
# =============================================================================

_secrets_binding: Any = None


def _get_secrets_binding() -> Any:
    """Get the secrets binding, importing it lazily."""
    global _secrets_binding
    if _secrets_binding is None:
        try:
            from zlayer.bindings.zlayer.host import secrets as binding

            _secrets_binding = binding
        except ImportError:
            _secrets_binding = _SecretsStub()
    return _secrets_binding


class _SecretsStub:
    """Stub for secrets when bindings aren't available."""

    _data: dict[str, str] = {}

    def get(self, name: str) -> str | None:
        return self._data.get(name)

    def get_required(self, name: str) -> str:
        if name not in self._data:
            raise SecretError(f"Required secret not found: {name}", name)
        return self._data[name]

    def exists(self, name: str) -> bool:
        return name in self._data

    def list_names(self) -> list[str]:
        return list(self._data.keys())


def get_secret(name: str) -> str | None:
    """Get a secret by name.

    Secrets are write-once by the deployment, read-only by plugins.

    Args:
        name: The secret name

    Returns:
        The secret value, or None if not found

    Raises:
        SecretError: On access errors
    """
    try:
        return _get_secrets_binding().get(name)
    except Exception as e:
        raise SecretError(str(e), name) from e


def get_secret_required(name: str) -> str:
    """Get a required secret, error if not found.

    Args:
        name: The secret name

    Returns:
        The secret value

    Raises:
        SecretError: If the secret is not found or on access errors
    """
    try:
        return _get_secrets_binding().get_required(name)
    except Exception as e:
        raise SecretError(str(e), name) from e


def secret_exists(name: str) -> bool:
    """Check if a secret exists.

    Args:
        name: The secret name

    Returns:
        True if the secret exists
    """
    return _get_secrets_binding().exists(name)


def list_secret_names() -> list[str]:
    """List available secret names (not values).

    Useful for diagnostics without exposing sensitive data.

    Returns:
        List of secret names
    """
    return _get_secrets_binding().list_names()


# =============================================================================
# Metrics
# =============================================================================

_metrics_binding: Any = None


def _get_metrics_binding() -> Any:
    """Get the metrics binding, importing it lazily."""
    global _metrics_binding
    if _metrics_binding is None:
        try:
            from zlayer.bindings.zlayer.host import metrics as binding

            _metrics_binding = binding
        except ImportError:
            _metrics_binding = _MetricsStub()
    return _metrics_binding


class _MetricsStub:
    """Stub for metrics when bindings aren't available."""

    def counter_inc(self, name: str, value: int) -> None:
        pass

    def counter_inc_labeled(
        self, name: str, value: int, labels: list[tuple[str, str]]
    ) -> None:
        pass

    def gauge_set(self, name: str, value: float) -> None:
        pass

    def gauge_set_labeled(
        self, name: str, value: float, labels: list[tuple[str, str]]
    ) -> None:
        pass

    def gauge_add(self, name: str, delta: float) -> None:
        pass

    def histogram_observe(self, name: str, value: float) -> None:
        pass

    def histogram_observe_labeled(
        self, name: str, value: float, labels: list[tuple[str, str]]
    ) -> None:
        pass

    def record_duration(self, name: str, duration_ns: int) -> None:
        pass

    def record_duration_labeled(
        self, name: str, duration_ns: int, labels: list[tuple[str, str]]
    ) -> None:
        pass


def counter_inc(name: str, value: int = 1) -> None:
    """Increment a counter metric.

    Args:
        name: The metric name
        value: The value to add (default 1)
    """
    _get_metrics_binding().counter_inc(name, value)


def counter_inc_labeled(name: str, value: int, labels: dict[str, str]) -> None:
    """Increment a counter metric with labels.

    Args:
        name: The metric name
        value: The value to add
        labels: Additional labels for the metric
    """
    label_tuples = [(k, v) for k, v in labels.items()]
    _get_metrics_binding().counter_inc_labeled(name, value, label_tuples)


def gauge_set(name: str, value: float) -> None:
    """Set a gauge metric to a value.

    Args:
        name: The metric name
        value: The value to set
    """
    _get_metrics_binding().gauge_set(name, value)


def gauge_set_labeled(name: str, value: float, labels: dict[str, str]) -> None:
    """Set a gauge metric with labels.

    Args:
        name: The metric name
        value: The value to set
        labels: Additional labels for the metric
    """
    label_tuples = [(k, v) for k, v in labels.items()]
    _get_metrics_binding().gauge_set_labeled(name, value, label_tuples)


def gauge_add(name: str, delta: float) -> None:
    """Add to a gauge metric value (can be negative).

    Args:
        name: The metric name
        delta: The value to add (can be negative)
    """
    _get_metrics_binding().gauge_add(name, delta)


def histogram_observe(name: str, value: float) -> None:
    """Record a histogram observation.

    Args:
        name: The metric name
        value: The observed value
    """
    _get_metrics_binding().histogram_observe(name, value)


def histogram_observe_labeled(name: str, value: float, labels: dict[str, str]) -> None:
    """Record a histogram observation with labels.

    Args:
        name: The metric name
        value: The observed value
        labels: Additional labels for the metric
    """
    label_tuples = [(k, v) for k, v in labels.items()]
    _get_metrics_binding().histogram_observe_labeled(name, value, label_tuples)


def record_duration(name: str, duration_ns: int) -> None:
    """Record request duration in nanoseconds.

    Convenience method that records to a standard histogram.

    Args:
        name: The metric name
        duration_ns: Duration in nanoseconds
    """
    _get_metrics_binding().record_duration(name, duration_ns)


def record_duration_labeled(
    name: str, duration_ns: int, labels: dict[str, str]
) -> None:
    """Record request duration with labels.

    Args:
        name: The metric name
        duration_ns: Duration in nanoseconds
        labels: Additional labels for the metric
    """
    label_tuples = [(k, v) for k, v in labels.items()]
    _get_metrics_binding().record_duration_labeled(name, duration_ns, label_tuples)


# =============================================================================
# Plugin Base Class
# =============================================================================


def require_capability(capability: int) -> Callable[[Callable[..., T]], Callable[..., T]]:
    """Decorator to mark a method as requiring a specific capability.

    This is a documentation/validation decorator that can be used to
    clearly indicate which capabilities a method needs.

    Args:
        capability: The capability flag (e.g., Capabilities.KEYVALUE)

    Returns:
        The decorated function

    Example:
        @require_capability(Capabilities.KEYVALUE)
        def store_data(self, key: str, value: bytes) -> None:
            kv_set(key, value)
    """

    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        func._required_capability = capability  # type: ignore
        return func

    return decorator


class ZLayerPlugin(ABC):
    """Base class for ZLayer plugins.

    Subclass this to create a plugin. Override the abstract methods
    to define your plugin's behavior.

    Example:
        class MyPlugin(ZLayerPlugin):
            def init(self) -> Capabilities:
                log_info("MyPlugin initializing")
                return Capabilities.LOGGING | Capabilities.CONFIG

            def info(self) -> PluginInfo:
                return PluginInfo(
                    id="mycompany:my-plugin",
                    name="My Plugin",
                    version="1.0.0",
                    description="A sample plugin",
                    author="My Company",
                )

            def handle(self, request: PluginRequest) -> HandleResult:
                if request.path == "/health":
                    return HandleResult.response(PluginResponse.ok("healthy"))
                return HandleResult.pass_through()

            def shutdown(self) -> None:
                log_info("MyPlugin shutting down")
    """

    def init(self) -> Capabilities:
        """Initialize the plugin.

        Called once when the plugin is loaded. The plugin should:
        1. Read any required configuration
        2. Initialize internal state
        3. Return the capabilities it needs

        Returns:
            The capabilities required by the plugin

        Raises:
            InitError: If initialization fails
        """
        return Capabilities.LOGGING

    @abstractmethod
    def info(self) -> PluginInfo:
        """Return plugin metadata.

        Called after successful initialization. This information
        is used for logging, metrics, and management.

        Returns:
            Plugin information
        """
        ...

    @abstractmethod
    def handle(self, request: PluginRequest) -> HandleResult:
        """Handle an incoming request.

        Called for each request that matches the plugin's routing rules.
        The plugin should:
        1. Examine the request
        2. Process it or decide to pass through
        3. Return an appropriate result

        Args:
            request: The incoming request

        Returns:
            The result of handling:
            - HandleResult.response(): Plugin handled the request
            - HandleResult.pass_through(): Continue to next handler
            - HandleResult.error(): Plugin encountered an error
        """
        ...

    def shutdown(self) -> None:
        """Graceful shutdown hook.

        Called when the plugin is being unloaded. Plugins should
        clean up any resources. This is a best-effort call - plugins
        may be terminated without shutdown if they don't respond in time.
        """
        pass

    def get_required_capabilities(self) -> Capabilities:
        """Get capabilities required by this plugin.

        Scans methods decorated with @require_capability to determine
        what capabilities the plugin needs.

        Returns:
            Combined capabilities from all decorated methods
        """
        caps = Capabilities.none()
        for name in dir(self):
            method = getattr(self, name, None)
            if callable(method):
                cap = getattr(method, "_required_capability", None)
                if cap is not None:
                    caps = caps | cap
        return caps
