"""Multi-tenant support for OCLAWMA.

This module provides tenant isolation with:
- Tenant model with configuration and quotas
- TenantManager for CRUD operations
- Namespace isolation via tenant_id prefixes
- Per-tenant quotas: max_jobs, max_workers, storage_limit
- Tenant-specific access controls
"""

from __future__ import annotations

import json
import sqlite3
import threading
from contextlib import contextmanager
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any

from pydantic import BaseModel, Field, field_validator


class TenantStatus(str, Enum):
    """Tenant status enumeration."""

    ACTIVE = "active"
    SUSPENDED = "suspended"
    DISABLED = "disabled"


class TenantQuotaExceededError(Exception):
    """Raised when a tenant exceeds their quota."""

    pass


class TenantNotFoundError(Exception):
    """Raised when a tenant is not found."""

    pass


class TenantError(Exception):
    """Raised when there's a general tenant-related error."""

    pass


class TenantAccessDeniedError(Exception):
    """Raised when access is denied for a tenant."""

    pass


class TenantQuotas(BaseModel):
    """Quota configuration for a tenant.

    Attributes:
        max_jobs: Maximum number of concurrent jobs
        max_workers: Maximum number of workers allowed
        storage_limit_mb: Storage limit in megabytes
        max_requests_per_minute: Rate limit for API requests
        max_concurrent_sessions: Maximum concurrent user sessions
    """

    max_jobs: int = Field(default=100, ge=1, description="Maximum concurrent jobs")
    max_workers: int = Field(default=10, ge=1, description="Maximum workers")
    storage_limit_mb: int = Field(default=1000, ge=1, description="Storage limit in MB")
    max_requests_per_minute: int = Field(
        default=1000, ge=1, description="API rate limit per minute"
    )
    max_concurrent_sessions: int = Field(
        default=50, ge=1, description="Maximum concurrent sessions"
    )

    def to_dict(self) -> dict[str, int]:
        """Convert quotas to dictionary."""
        return self.model_dump()

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> TenantQuotas:
        """Create quotas from dictionary."""
        return cls.model_validate(data)

    @classmethod
    def from_json(cls, json_str: str) -> TenantQuotas:
        """Create quotas from JSON string."""
        return cls.model_validate_json(json_str)


class TenantConfig(BaseModel):
    """Configuration for a tenant.

    Attributes:
        features: Enabled feature flags
        settings: Custom tenant settings
        metadata: Arbitrary metadata for the tenant
    """

    features: dict[str, bool] = Field(
        default_factory=lambda: {
            "web_chat": True,
            "batch_jobs": True,
            "encryption": True,
            "rate_limiting": True,
            "distributed_locking": False,
            "priority_preemption": True,
        },
        description="Enabled features for the tenant",
    )
    settings: dict[str, Any] = Field(default_factory=dict, description="Custom tenant settings")
    metadata: dict[str, Any] = Field(default_factory=dict, description="Tenant metadata")
    allowed_origins: list[str] = Field(default_factory=list, description="Allowed CORS origins")
    api_keys: list[str] = Field(default_factory=list, description="Valid API keys for this tenant")

    @field_validator("features", mode="before")
    @classmethod
    def ensure_default_features(cls, v: dict[str, bool] | None) -> dict[str, bool]:
        """Ensure default features are present."""
        if v is None:
            v = {}
        defaults = {
            "web_chat": True,
            "batch_jobs": True,
            "encryption": True,
            "rate_limiting": True,
            "distributed_locking": False,
            "priority_preemption": True,
        }
        defaults.update(v)
        return defaults

    @classmethod
    def from_json(cls, json_str: str) -> TenantConfig:
        """Create config from JSON string."""
        return cls.model_validate_json(json_str)


class Tenant(BaseModel):
    """Represents a tenant in the system.

    Attributes:
        id: Unique tenant identifier (e.g., "tenant-123")
        name: Human-readable tenant name
        description: Optional tenant description
        status: Current tenant status
        quotas: Resource quotas for this tenant
        config: Tenant-specific configuration
        created_at: When the tenant was created
        updated_at: When the tenant was last updated
    """

    id: str = Field(..., description="Unique tenant identifier")
    name: str = Field(..., min_length=1, max_length=100, description="Tenant name")
    description: str | None = Field(default=None, description="Tenant description")
    status: TenantStatus = Field(default=TenantStatus.ACTIVE, description="Tenant status")
    quotas: TenantQuotas = Field(default_factory=TenantQuotas, description="Resource quotas")
    config: TenantConfig = Field(default_factory=TenantConfig, description="Tenant configuration")
    created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation timestamp")
    updated_at: datetime = Field(
        default_factory=datetime.utcnow, description="Last update timestamp"
    )

    def is_active(self) -> bool:
        """Check if tenant is active."""
        return self.status == TenantStatus.ACTIVE

    def can_use_feature(self, feature: str) -> bool:
        """Check if tenant can use a specific feature."""
        return self.config.features.get(feature, False)

    def validate_api_key(self, api_key: str) -> bool:
        """Validate an API key for this tenant."""
        if not self.config.api_keys:
            return True  # No keys configured, allow all
        return api_key in self.config.api_keys

    def check_quota(self, quota_name: str, current_value: int) -> bool:
        """Check if current value is within quota.

        Args:
            quota_name: Name of the quota to check
            current_value: Current usage value

        Returns:
            True if within quota, False if exceeded
        """
        quota_value = getattr(self.quotas, quota_name, None)
        if quota_value is None:
            return True  # No quota set
        return current_value < quota_value

    def prefix_job_id(self, job_id: int | str) -> str:
        """Prefix a job ID with tenant namespace.

        Args:
            job_id: The job ID to prefix

        Returns:
            Namespaced job ID (e.g., "tenant-123:job-456")
        """
        return f"{self.id}:{job_id}"

    def extract_job_id(self, namespaced_id: str) -> str:
        """Extract the job ID from a namespaced ID.

        Args:
            namespaced_id: The namespaced job ID

        Returns:
            The job ID without tenant prefix

        Raises:
            TenantError: If the ID doesn't belong to this tenant
        """
        prefix = f"{self.id}:"
        if not namespaced_id.startswith(prefix):
            raise TenantError(f"Job ID {namespaced_id} does not belong to tenant {self.id}")
        return namespaced_id[len(prefix) :]

    def model_dump_json(self, **kwargs: Any) -> str:
        """Serialize tenant to JSON string."""
        data = self.model_dump(mode="json")
        return json.dumps(data)

    @classmethod
    def from_json(cls, json_str: str) -> Tenant:
        """Create Tenant from JSON string."""
        data = json.loads(json_str)
        return cls.model_validate(data)


# =============================================================================
# Tenant Manager
# =============================================================================

TENANT_SCHEMA_SQL = """
CREATE TABLE IF NOT EXISTS tenants (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT,
    status TEXT NOT NULL DEFAULT 'active',
    quotas TEXT NOT NULL,
    config TEXT NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,

    CHECK (status IN ('active', 'suspended', 'disabled'))
);

CREATE INDEX IF NOT EXISTS idx_tenants_status ON tenants(status);
CREATE INDEX IF NOT EXISTS idx_tenants_name ON tenants(name);

-- Table for tracking tenant resource usage
CREATE TABLE IF NOT EXISTS tenant_usage (
    tenant_id TEXT PRIMARY KEY,
    job_count INTEGER NOT NULL DEFAULT 0,
    worker_count INTEGER NOT NULL DEFAULT 0,
    storage_mb INTEGER NOT NULL DEFAULT 0,
    session_count INTEGER NOT NULL DEFAULT 0,
    request_count INTEGER NOT NULL DEFAULT 0,
    last_updated TEXT NOT NULL,

    FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_tenant_usage ON tenant_usage(tenant_id);
"""


@dataclass
class TenantUsage:
    """Resource usage for a tenant."""

    tenant_id: str
    job_count: int = 0
    worker_count: int = 0
    storage_mb: int = 0
    session_count: int = 0
    request_count: int = 0
    last_updated: datetime = field(default_factory=datetime.utcnow)


class TenantManager:
    """Manages tenants with CRUD operations and access control.

    This class provides:
    - Tenant CRUD operations
    - Resource usage tracking
    - Quota enforcement
    - Access control validation

    Example:
        >>> manager = TenantManager("/path/to/tenants.db")
        >>> tenant = manager.create(
        ...     tenant_id="acme-corp",
        ...     name="Acme Corporation",
        ...     quotas=TenantQuotas(max_jobs=500, max_workers=20)
        ... )
        >>> if manager.check_quota(tenant.id, "max_jobs", current=450):
        ...     # Allow job creation
    """

    def __init__(self, db_path: str | Path = ":memory:") -> None:
        """Initialize the tenant manager.

        Args:
            db_path: Path to SQLite database. Use ":memory:" for in-memory.
        """
        self.db_path = str(db_path)
        self._local = threading.local()
        self._lock = threading.RLock()
        self._init_db()

    def _get_connection(self) -> sqlite3.Connection:
        """Get thread-local database connection."""
        if not hasattr(self._local, "conn") or self._local.conn is None:
            self._local.conn = sqlite3.connect(
                self.db_path,
                check_same_thread=False,
            )
            self._local.conn.row_factory = sqlite3.Row
            self._local.conn.execute("PRAGMA foreign_keys = ON")
            self._local.conn.execute("PRAGMA journal_mode = WAL")
        return self._local.conn

    @contextmanager
    def _transaction(self):
        """Context manager for database transactions."""
        conn = self._get_connection()
        cursor = conn.cursor()
        try:
            yield cursor
            conn.commit()
        except Exception:
            conn.rollback()
            raise

    def _init_db(self) -> None:
        """Initialize database schema."""
        conn = self._get_connection()
        conn.executescript(TENANT_SCHEMA_SQL)
        conn.commit()

    def create(
        self,
        tenant_id: str,
        name: str,
        description: str | None = None,
        quotas: TenantQuotas | None = None,
        config: TenantConfig | None = None,
        status: TenantStatus = TenantStatus.ACTIVE,
    ) -> Tenant:
        """Create a new tenant.

        Args:
            tenant_id: Unique identifier for the tenant
            name: Human-readable name
            description: Optional description
            quotas: Resource quotas (uses defaults if not provided)
            config: Tenant configuration (uses defaults if not provided)
            status: Initial tenant status

        Returns:
            The created tenant

        Raises:
            TenantError: If tenant already exists or creation fails
        """
        with self._lock:
            # Check if tenant already exists
            if self._exists(tenant_id):
                raise TenantError(f"Tenant '{tenant_id}' already exists")

            tenant = Tenant(
                id=tenant_id,
                name=name,
                description=description,
                status=status,
                quotas=quotas or TenantQuotas(),
                config=config or TenantConfig(),
            )

            try:
                with self._transaction() as cursor:
                    cursor.execute(
                        """
                        INSERT INTO tenants (id, name, description, status, quotas, config, created_at, updated_at)
                        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                        """,
                        (
                            tenant.id,
                            tenant.name,
                            tenant.description,
                            tenant.status.value,
                            tenant.quotas.model_dump_json(),
                            tenant.config.model_dump_json(),
                            tenant.created_at.isoformat(),
                            tenant.updated_at.isoformat(),
                        ),
                    )

                    # Initialize usage tracking
                    cursor.execute(
                        """
                        INSERT INTO tenant_usage (tenant_id, job_count, worker_count, storage_mb, session_count, request_count, last_updated)
                        VALUES (?, 0, 0, 0, 0, 0, ?)
                        """,
                        (tenant.id, datetime.utcnow().isoformat()),
                    )

                return tenant
            except sqlite3.Error as e:
                raise TenantError(f"Failed to create tenant: {e}") from e

    def get(self, tenant_id: str) -> Tenant:
        """Get a tenant by ID.

        Args:
            tenant_id: The tenant identifier

        Returns:
            The tenant

        Raises:
            TenantNotFoundError: If tenant not found
        """
        try:
            with self._transaction() as cursor:
                cursor.execute("SELECT * FROM tenants WHERE id = ?", (tenant_id,))
                row = cursor.fetchone()

            if row is None:
                raise TenantNotFoundError(f"Tenant '{tenant_id}' not found")

            return self._row_to_tenant(dict(row))
        except sqlite3.Error as e:
            raise TenantError(f"Failed to get tenant: {e}") from e

    def update(self, tenant: Tenant) -> Tenant:
        """Update an existing tenant.

        Args:
            tenant: The tenant to update

        Returns:
            The updated tenant

        Raises:
            TenantNotFoundError: If tenant not found
        """
        with self._lock:
            tenant.updated_at = datetime.utcnow()

            try:
                with self._transaction() as cursor:
                    cursor.execute(
                        """
                        UPDATE tenants SET
                            name = ?,
                            description = ?,
                            status = ?,
                            quotas = ?,
                            config = ?,
                            updated_at = ?
                        WHERE id = ?
                        """,
                        (
                            tenant.name,
                            tenant.description,
                            tenant.status.value,
                            tenant.quotas.model_dump_json(),
                            tenant.config.model_dump_json(),
                            tenant.updated_at.isoformat(),
                            tenant.id,
                        ),
                    )

                    if cursor.rowcount == 0:
                        raise TenantNotFoundError(f"Tenant '{tenant.id}' not found")

                return tenant
            except sqlite3.Error as e:
                raise TenantError(f"Failed to update tenant: {e}") from e

    def delete(self, tenant_id: str) -> bool:
        """Delete a tenant.

        Args:
            tenant_id: The tenant to delete

        Returns:
            True if deleted, False if not found
        """
        with self._lock:
            try:
                with self._transaction() as cursor:
                    cursor.execute("DELETE FROM tenants WHERE id = ?", (tenant_id,))
                    return cursor.rowcount > 0
            except sqlite3.Error as e:
                raise TenantError(f"Failed to delete tenant: {e}") from e

    def list_all(
        self, status: TenantStatus | None = None, limit: int | None = None
    ) -> list[Tenant]:
        """List all tenants.

        Args:
            status: Filter by status
            limit: Maximum number of tenants to return

        Returns:
            List of tenants
        """
        try:
            with self._transaction() as cursor:
                if status:
                    sql = "SELECT * FROM tenants WHERE status = ? ORDER BY name"
                    params: tuple[Any, ...] = (status.value,)
                else:
                    sql = "SELECT * FROM tenants ORDER BY name"
                    params = ()

                if limit:
                    sql += " LIMIT ?"
                    params = params + (limit,)

                cursor.execute(sql, params)
                rows = cursor.fetchall()

            return [self._row_to_tenant(dict(row)) for row in rows]
        except sqlite3.Error as e:
            raise TenantError(f"Failed to list tenants: {e}") from e

    def _exists(self, tenant_id: str) -> bool:
        """Check if a tenant exists."""
        try:
            with self._transaction() as cursor:
                cursor.execute("SELECT 1 FROM tenants WHERE id = ?", (tenant_id,))
                return cursor.fetchone() is not None
        except sqlite3.Error:
            return False

    def _row_to_tenant(self, row: dict[str, Any]) -> Tenant:
        """Convert a database row to a Tenant object."""
        return Tenant(
            id=row["id"],
            name=row["name"],
            description=row["description"],
            status=TenantStatus(row["status"]),
            quotas=TenantQuotas.from_json(row["quotas"]),
            config=TenantConfig.from_json(row["config"]),
            created_at=datetime.fromisoformat(row["created_at"]),
            updated_at=datetime.fromisoformat(row["updated_at"]),
        )

    # =============================================================================
    # Usage Tracking
    # =============================================================================

    def get_usage(self, tenant_id: str) -> TenantUsage:
        """Get resource usage for a tenant.

        Args:
            tenant_id: The tenant identifier

        Returns:
            Current resource usage

        Raises:
            TenantNotFoundError: If tenant not found
        """
        try:
            with self._transaction() as cursor:
                cursor.execute("SELECT * FROM tenant_usage WHERE tenant_id = ?", (tenant_id,))
                row = cursor.fetchone()

            if row is None:
                raise TenantNotFoundError(f"Tenant '{tenant_id}' not found")

            return TenantUsage(
                tenant_id=row["tenant_id"],
                job_count=row["job_count"],
                worker_count=row["worker_count"],
                storage_mb=row["storage_mb"],
                session_count=row["session_count"],
                request_count=row["request_count"],
                last_updated=datetime.fromisoformat(row["last_updated"]),
            )
        except sqlite3.Error as e:
            raise TenantError(f"Failed to get usage: {e}") from e

    def update_usage(
        self,
        tenant_id: str,
        job_count: int | None = None,
        worker_count: int | None = None,
        storage_mb: int | None = None,
        session_count: int | None = None,
        request_count: int | None = None,
    ) -> TenantUsage:
        """Update resource usage for a tenant.

        Args:
            tenant_id: The tenant to update
            job_count: New job count (or None to keep current)
            worker_count: New worker count (or None to keep current)
            storage_mb: New storage usage in MB (or None to keep current)
            session_count: New session count (or None to keep current)
            request_count: New request count (or None to keep current)

        Returns:
            Updated usage
        """
        with self._lock:
            # Get current usage
            current = self.get_usage(tenant_id)

            # Update fields
            if job_count is not None:
                current.job_count = job_count
            if worker_count is not None:
                current.worker_count = worker_count
            if storage_mb is not None:
                current.storage_mb = storage_mb
            if session_count is not None:
                current.session_count = session_count
            if request_count is not None:
                current.request_count = request_count

            current.last_updated = datetime.utcnow()

            try:
                with self._transaction() as cursor:
                    cursor.execute(
                        """
                        UPDATE tenant_usage SET
                            job_count = ?,
                            worker_count = ?,
                            storage_mb = ?,
                            session_count = ?,
                            request_count = ?,
                            last_updated = ?
                        WHERE tenant_id = ?
                        """,
                        (
                            current.job_count,
                            current.worker_count,
                            current.storage_mb,
                            current.session_count,
                            current.request_count,
                            current.last_updated.isoformat(),
                            tenant_id,
                        ),
                    )

                return current
            except sqlite3.Error as e:
                raise TenantError(f"Failed to update usage: {e}") from e

    def increment_usage(
        self,
        tenant_id: str,
        jobs: int = 0,
        workers: int = 0,
        storage_mb: int = 0,
        sessions: int = 0,
        requests: int = 0,
    ) -> TenantUsage:
        """Increment resource usage counters.

        Args:
            tenant_id: The tenant to update
            jobs: Number of jobs to add
            workers: Number of workers to add
            storage_mb: Storage to add in MB
            sessions: Number of sessions to add
            requests: Number of requests to add

        Returns:
            Updated usage
        """
        current = self.get_usage(tenant_id)
        return self.update_usage(
            tenant_id,
            job_count=current.job_count + jobs,
            worker_count=current.worker_count + workers,
            storage_mb=current.storage_mb + storage_mb,
            session_count=current.session_count + sessions,
            request_count=current.request_count + requests,
        )

    # =============================================================================
    # Quota Management
    # =============================================================================

    def check_quota(self, tenant_id: str, quota_name: str, value: int) -> bool:
        """Check if a value is within quota for a tenant.

        Args:
            tenant_id: The tenant to check
            quota_name: Name of the quota (e.g., 'max_jobs')
            value: The value to check

        Returns:
            True if within quota, False if exceeded
        """
        try:
            tenant = self.get(tenant_id)
            return tenant.check_quota(quota_name, value)
        except TenantNotFoundError:
            return False

    def validate_quota(self, tenant_id: str, quota_name: str, value: int) -> None:
        """Validate that a value is within quota, raising if exceeded.

        Args:
            tenant_id: The tenant to check
            quota_name: Name of the quota
            value: The value to check

        Raises:
            TenantQuotaExceededError: If quota is exceeded
            TenantNotFoundError: If tenant not found
        """
        tenant = self.get(tenant_id)
        quota_value = getattr(tenant.quotas, quota_name, None)

        if quota_value is not None and value > quota_value:
            raise TenantQuotaExceededError(
                f"Tenant '{tenant_id}' quota exceeded for '{quota_name}': "
                f"{value} > {quota_value}"
            )

    def get_quota_status(self, tenant_id: str) -> dict[str, dict[str, int]]:
        """Get quota status for a tenant.

        Args:
            tenant_id: The tenant to check

        Returns:
            Dictionary with quota names mapped to {limit, used, remaining}
        """
        tenant = self.get(tenant_id)
        usage = self.get_usage(tenant_id)

        quotas = {
            "max_jobs": (tenant.quotas.max_jobs, usage.job_count),
            "max_workers": (tenant.quotas.max_workers, usage.worker_count),
            "storage_limit_mb": (tenant.quotas.storage_limit_mb, usage.storage_mb),
            "max_concurrent_sessions": (
                tenant.quotas.max_concurrent_sessions,
                usage.session_count,
            ),
        }

        result = {}
        for name, (limit, used) in quotas.items():
            result[name] = {
                "limit": limit,
                "used": used,
                "remaining": max(0, limit - used),
                "percent": round((used / limit) * 100, 2) if limit > 0 else 0,
            }

        return result

    # =============================================================================
    # Access Control
    # =============================================================================

    def validate_access(
        self,
        tenant_id: str,
        api_key: str | None = None,
        require_active: bool = True,
    ) -> Tenant:
        """Validate access to a tenant.

        Args:
            tenant_id: The tenant identifier
            api_key: Optional API key to validate
            require_active: Whether to require active status

        Returns:
            The tenant if access is valid

        Raises:
            TenantNotFoundError: If tenant not found
            TenantAccessDeniedError: If access is denied
            TenantError: If tenant is not active and require_active is True
        """
        tenant = self.get(tenant_id)

        if require_active and not tenant.is_active():
            raise TenantError(f"Tenant '{tenant_id}' is not active (status: {tenant.status.value})")

        if api_key and not tenant.validate_api_key(api_key):
            raise TenantAccessDeniedError(f"Invalid API key for tenant '{tenant_id}'")

        return tenant

    def authenticate(self, tenant_id: str, api_key: str) -> Tenant:
        """Authenticate with a tenant using API key.

        Args:
            tenant_id: The tenant identifier
            api_key: The API key to validate

        Returns:
            The tenant if authentication succeeds

        Raises:
            TenantNotFoundError: If tenant not found
            TenantAccessDeniedError: If authentication fails
        """
        return self.validate_access(tenant_id, api_key=api_key)

    # =============================================================================
    # Status Management
    # =============================================================================

    def suspend(self, tenant_id: str) -> Tenant:
        """Suspend a tenant.

        Args:
            tenant_id: The tenant to suspend

        Returns:
            The updated tenant
        """
        tenant = self.get(tenant_id)
        tenant.status = TenantStatus.SUSPENDED
        return self.update(tenant)

    def activate(self, tenant_id: str) -> Tenant:
        """Activate a tenant.

        Args:
            tenant_id: The tenant to activate

        Returns:
            The updated tenant
        """
        tenant = self.get(tenant_id)
        tenant.status = TenantStatus.ACTIVE
        return self.update(tenant)

    def disable(self, tenant_id: str) -> Tenant:
        """Disable a tenant.

        Args:
            tenant_id: The tenant to disable

        Returns:
            The updated tenant
        """
        tenant = self.get(tenant_id)
        tenant.status = TenantStatus.DISABLED
        return self.update(tenant)

    # =============================================================================
    # Context Manager
    # =============================================================================

    def close(self) -> None:
        """Close the tenant manager and release resources."""
        if hasattr(self._local, "conn") and self._local.conn:
            self._local.conn.close()
            self._local.conn = None

    def __enter__(self) -> TenantManager:
        """Context manager entry."""
        return self

    def __exit__(self, *args: Any) -> None:
        """Context manager exit."""
        self.close()


# =============================================================================
# Tenant Context
# =============================================================================


class TenantContext:
    """Context manager for tenant-scoped operations.

    This class provides a way to execute operations within a tenant context,
    automatically handling namespace prefixing and quota checks.

    Example:
        >>> manager = TenantManager("/path/to/tenants.db")
        >>> with TenantContext(manager, "acme-corp") as ctx:
        ...     # All operations are scoped to acme-corp tenant
        ...     job_id = ctx.prefix_job_id(123)  # "acme-corp:123"
    """

    def __init__(
        self,
        manager: TenantManager,
        tenant_id: str,
        api_key: str | None = None,
        validate: bool = True,
    ) -> None:
        """Initialize tenant context.

        Args:
            manager: The TenantManager instance
            tenant_id: The tenant identifier
            api_key: Optional API key for validation
            validate: Whether to validate tenant on entry
        """
        self.manager = manager
        self.tenant_id = tenant_id
        self.api_key = api_key
        self._tenant: Tenant | None = None
        self._validate = validate

    def __enter__(self) -> TenantContext:
        """Enter tenant context."""
        if self._validate:
            self._tenant = self.manager.validate_access(self.tenant_id, api_key=self.api_key)
        else:
            self._tenant = self.manager.get(self.tenant_id)
        return self

    def __exit__(self, *args: Any) -> None:
        """Exit tenant context."""
        self._tenant = None

    @property
    def tenant(self) -> Tenant:
        """Get the current tenant."""
        if self._tenant is None:
            raise TenantError("Not in tenant context")
        return self._tenant

    def prefix_job_id(self, job_id: int | str) -> str:
        """Prefix a job ID with tenant namespace."""
        return self.tenant.prefix_job_id(job_id)

    def extract_job_id(self, namespaced_id: str) -> str:
        """Extract job ID from namespaced ID."""
        return self.tenant.extract_job_id(namespaced_id)

    def check_feature(self, feature: str) -> bool:
        """Check if a feature is enabled for this tenant."""
        return self.tenant.can_use_feature(feature)

    def require_feature(self, feature: str) -> None:
        """Require a feature to be enabled, raising if not."""
        if not self.check_feature(feature):
            raise TenantAccessDeniedError(
                f"Feature '{feature}' is not enabled for tenant '{self.tenant_id}'"
            )

    def check_quota(self, quota_name: str, value: int) -> bool:
        """Check if value is within quota."""
        return self.tenant.check_quota(quota_name, value)

    def validate_quota(self, quota_name: str, value: int) -> None:
        """Validate quota, raising if exceeded."""
        self.manager.validate_quota(self.tenant_id, quota_name, value)

    def get_usage(self) -> TenantUsage:
        """Get current usage for this tenant."""
        return self.manager.get_usage(self.tenant_id)

    def update_usage(self, **kwargs: Any) -> TenantUsage:
        """Update usage for this tenant."""
        return self.manager.update_usage(self.tenant_id, **kwargs)

    def increment_usage(self, **kwargs: int) -> TenantUsage:
        """Increment usage counters for this tenant."""
        return self.manager.increment_usage(self.tenant_id, **kwargs)
